Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

**Added**

- `Scribe.Encoder` protocol to derive a list of table headers from a struct.

**Changed**

- Bumped minimum Elixir version to 1.14.
- More descriptive exception message when generating a table for a non-map/struct.
- Better organization of documentation.

## v0.11.0 - 2024-08-31

**Added**
Expand Down
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2016-2024 Codedge LLC (https://www.codedge.io/)
Copyright (c) 2016-2025 Codedge LLC (https://www.codedge.io/)

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,6 @@ Git commit subjects use the [Karma style](http://karma-runner.github.io/5.0/dev/

## License

Copyright (c) 2016-2024 Codedge LLC (https://www.codedge.io/)
Copyright (c) 2016-2025 Codedge LLC (https://www.codedge.io/)

This library is MIT licensed. See the [LICENSE](https://github.com/codedge-llc/scribe/blob/main/LICENSE) for details.
3 changes: 1 addition & 2 deletions lib/scribe.ex
Original file line number Diff line number Diff line change
Expand Up @@ -280,8 +280,7 @@ defmodule Scribe do

defp fetch_keys(map) do
map
|> Map.keys()
|> Enum.sort()
|> Scribe.Encoder.headers()
|> process_headers()
end
end
98 changes: 98 additions & 0 deletions lib/scribe/encoder.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
defprotocol Scribe.Encoder do
@moduledoc """
A protocol for custom Scribe encoding of data structures.

If you have a struct, you can derive the implementation of this protocol
by specifying which fields should be encoded to Scribe:

@derive {Scribe.Encoder, only: [....]}
defstruct ...

It is also possible to encode all fields or skip some fields via the
`:except` option:

@derive Scribe.Encoder
defstruct ...

> #### Leaking Private Information {: .error}
>
> The `:except` approach should be used carefully to avoid
> accidentally leaking private information when new fields are added.

Finally, if you don't own the struct you want to encode to Scribe,
you may use `Protocol.derive/3` placed outside of any module:

Protocol.derive(Scribe.Encoder, NameOfTheStruct, only: [...])
Protocol.derive(Scribe.Encoder, NameOfTheStruct)
"""

@impl true
defmacro __deriving__(module, opts) do
fields = module |> Macro.struct_info!(__CALLER__) |> Enum.map(& &1.field)
fields = fields_to_encode(fields, opts)

quote do
defimpl Scribe.Encoder, for: unquote(module) do
def headers(term) do
unquote(fields)
end
end
end
end

defp fields_to_encode(fields, opts) do
cond do
only = Keyword.get(opts, :only) -> only(only, fields)
except = Keyword.get(opts, :except) -> except(except, fields)
true -> fields -- [:__struct__]
end
end

defp only(only, fields) do
case only -- fields do
[] -> only
error_keys -> raise ArgumentError, error_msg(error_keys, fields, :only)
end
end

defp except(except, fields) do
case except -- fields do
[] ->
fields -- [:__struct__ | except]

error_keys ->
raise ArgumentError, error_msg(error_keys, fields, :except)
end
end

defp error_msg(error_keys, fields, option) do
"unknown struct fields #{inspect(error_keys)} specified in :#{option}. Expected one of: " <>
"#{inspect(fields -- [:__struct__])}"
end

@doc """
A function invoked to derive a list of headers from given term.
"""
@fallback_to_any true
def headers(term)
end

defimpl Scribe.Encoder, for: Map do
def headers(value) do
value
|> Map.keys()
|> Enum.sort()
end
end

defimpl Scribe.Encoder, for: Any do
def headers(value) when is_struct(value) do
value
|> Map.keys()
|> Enum.sort()
end

def headers(value) do
raise ArgumentError, "expected a map or struct, got: #{inspect(value)}"
end
end
22 changes: 20 additions & 2 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Scribe.Mixfile do
use Mix.Project

@source_url "https://github.com/codedge-llc/scribe"
@version "0.11.0"
@version "0.12.0"

def project do
[
Expand All @@ -11,14 +11,19 @@ defmodule Scribe.Mixfile do
deps: deps(),
docs: docs(),
dialyzer: dialyzer(),
elixir: "~> 1.13",
elixir: "~> 1.14",
elixirc_paths: elixirc_paths(Mix.env()),
name: "Scribe",
package: package(),
start_permanent: Mix.env() == :prod,
version: @version
]
end

defp elixirc_paths(:dev), do: ["lib", "test/support"]
defp elixirc_paths(:test), do: ["lib", "test/support"]
defp elixirc_paths(_), do: ["lib"]

def application do
[extra_applications: [:logger, :pane]]
end
Expand Down Expand Up @@ -46,6 +51,19 @@ defmodule Scribe.Mixfile do
LICENSE: [title: "License"]
],
formatters: ["html"],
groups_for_modules: [
"Available Styles": [
Scribe.Style.Default,
Scribe.Style.GithubMarkdown,
Scribe.Style.NoBorder,
Scribe.Style.Pseudo,
Scribe.Style.Psql
],
"Custom Styles": [
Scribe.Border,
Scribe.Style
]
],
main: "Scribe",
skip_undefined_reference_warnings_on: ["CHANGELOG.md"],
source_ref: "v#{@version}",
Expand Down
8 changes: 8 additions & 0 deletions test/support/user.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
defmodule Scribe.User do
@moduledoc """
Example struct implementing the `Scribe.Encoder` protocol.
"""

@derive {Scribe.Encoder, except: [:inserted_at, :updated_at]}
defstruct [:id, :username, :email, :inserted_at, :updated_at]
end
Loading