AshToonEx
⚠️ EXPERIMENTAL / UNDER ACTIVE DEVELOPMENT ⚠️
Ash resource extension for implementing ToonEx.Encoder and ToonEx.Btoon.Encoder protocols.
Built on top of ToonEx — a high-performance TOON (Token-Oriented Object Notation) encoder/decoder for Elixir with support for both text (TOON) and binary (BTOON) formats.
Inspired by ash_jason.
Installation
Add to the deps:
def deps do
[
{:ash_toon_ex, "~> 0.3.0"},
]
end
Usage
Add AshToonEx.Resource to extensions list within use Ash.Resource options:
defmodule Example.Resource do
use Ash.Resource,
extensions: [AshToonEx.Resource]
end
Configuration
Producing a TOON object can have multiple steps:
- Picking keys from a record.
- Removing fields with unnecessary values (like nil).
- Merging some values.
- Renaming keys.
- Ordering keys.
- Customizing a result with a function.
By default only the picking step happens and it takes all non-private non-sensitive fields (attributes, relationships, aggregates, calculations) with loaded values from a record.
For adding and configuring those steps there is an optional toon dsl section:
defmodule Example.Resource do
use Ash.Resource,
extensions: [AshToonEx.Resource]
toon do
# options
end
end
All optional steps can be specified multiple times and are applied in the order they were defined in.
A result object on which those steps operate is a key-value list — not map, not keyword list.
- Unlike map the order is stable and guaranteed.
- Unlike keyword list it can have string keys.
pick
Keys to pick from a record and include in the result. Accepts a fixed explicit list of keys or a map with a configuration of default behaviour.
Values of nil / Ash.NotLoaded / Ash.ForbiddenField are omitted.
Map can have such options as:
private?- Whether to pick private fields.sensitive?- Whether to pick sensitive fields.relationships?- Whether to include loaded relationships (default:false). When enabled, loaded relationships will be encoded as nested TOON objects.calculations?- Whether to include calculations (default:true).aggregates?- Whether to include aggregates (default:true).include- Keys to pick. In addition to fields.exclude- Keys not to pick.format- The output format. Can be:toon(text) or:btoon(binary). Defaults to:toon.
Both ToonEx.Encoder (for TOON text format) and ToonEx.Btoon.Encoder (for BTOON binary format) protocols are implemented regardless of the format option. The format option is for documentation and future use.
toon do
# Pick only those listed keys
pick [:only_some_field]
# Pick non-sensitive fields
pick %{private?: true}
# Pick non-private fields
pick %{sensitive?: true}
# Pick all fields
pick %{private?: true, sensitive?: true}
# Pick usual but include and exclude some specific keys
pick %{include: [:ok_private_field], exclude: [:irrelevant_public_field]}
# Include loaded relationships in encoding
pick %{relationships?: true}
# Exclude calculations from encoding
pick %{calculations?: false}
# Exclude aggregates from encoding
pick %{aggregates?: false}
end
compact
A step to remove unneeded values from a result. Accepts a boolean, a tagged only/except tuple or a config map with values/fields keys.
toon do
# Remove all fields with nil value
compact true
# Remove fields with nil value except for specified exceptions
compact {:except, [:keep_nil]}
# Remove fields with specific unwanted values
compact %{values: [nil, false, ""]}
end
merge
A step to merge values into a result. Accepts a map or a tuples list.
Map has no guarantees about keys order so if you care about that prefer the list form.
toon do
# Merge with map
merge %{key: "value"}
# Merge with list
merge key: "value"
end
rename
A step to rename keys in a result. Accepts a map, a tuples list or a function for mapping.
toon do
# Rename with map
rename %{from_key: "to_key"}
# Rename with list
rename from_key: "to_key"
# Rename with a function
rename fn name -> String.capitalize(to_string(name)) end
end
order
A step to reorder keys in a result. Accepts a boolean, a sort function or a list of keys in a desired order.
If it is a list then it also acts as a filter and removes keys not present in that list.
toon do
# Order with standard `Enum.sort`
order true
# Order with a custom sort function
order fn keys -> Enum.sort(keys, :desc) end
# Order in accordance with a list
order [:only, :these, :keys, :in, :that, "order"]
end
customize
A step to arbitrary customize a result. Accepts a function that will get a result and a resource record as arguments and return a modified result.
As mentioned above a result has a form of a list with two elements, key and value, tuples. To work with it you might want to use List methods like List.keytake or List.keystore.
toon do
customize fn result, _record ->
result |> List.keystore(:custom_key, 0, {:custom_key, "custom_value"})
end
end
Typed structs
To use with Ash.TypedStruct add AshToonEx.TypedStruct extension.
All toon options and steps are the same except there are no private? or sensitive? filters in pick map form (since typed struct fields do not have those options).
defmodule Example.TypedStruct do
use Ash.TypedStruct,
extensions: [AshToonEx.TypedStruct]
end
Phoenix Integration
AshToonEx provides a Phoenix serializer module for working with TOON and BTOON formats in Phoenix controllers.
# In your controller
defmodule MyAppWeb.UserController do
use MyAppWeb, :controller
alias AshToonEx.Phoenix.ToonSerializer
def show(conn, %{"id" => id}) do
user = MyApp.User |> Ash.get!(id)
# For TOON text format
conn
|> put_resp_content_type("application/x-toon")
|> send_resp(200, ToonEx.encode!(user))
# Or use the helper
# conn |> ToonSerializer.send_toon(user)
end
def show_btoon(conn, %{"id" => id}) do
user = MyApp.User |> Ash.get!(id)
# For BTOON binary format
conn
|> put_resp_content_type("application/x-btoon")
|> send_resp(200, ToonEx.Btoon.encode!(user))
# Or use the helper
# conn |> ToonSerializer.send_btoon(user)
end
end
# Or use the provided helper
alias AshToonEx.Phoenix.ToonSerializer
conn |> ToonSerializer.send_toon(user)
conn |> ToonSerializer.send_btoon(user)
Protocol
Each resource or typed struct with AshToonEx extension also implements AshToonEx.Protocol.
It provides a single method get_fields that retrieves a list of key/value field tuples following the same AshToonEx's logic.
AshToonEx.Protocol.get_fields(%MyResource{id: 1, name: "Alice"})
# => [id: 1, name: "Alice"]
Encoding
With the extension applied, resources can be encoded directly to TOON text format:
user = %MyResource{id: 1, name: "Alice", email: "alice@example.com"}
ToonEx.encode!(user)
# => "email: alice@example.com\nid: 1\nname: Alice"
Resources can also be encoded to BTOON binary format:
user = %MyResource{id: 1, name: "Alice", email: "alice@example.com"}
ToonEx.Btoon.encode!(user)
# => <<...binary data...>>
# Decode back
ToonEx.Btoon.decode!(binary)
# => %{"id" => 1, "name" => "Alice", "email" => "alice@example.com"}
Note: BTOON encoding returns maps with string keys (not atom keys) as per the BTOON specification.
Links
- ToonEx — TOON and BTOON encoder/decoder for Elixir
- ash_jason — Ash extension for Jason protocol (inspiration for this project)
- Ash — Resource framework for Elixir
- Examples — Basic and advanced usage examples
License
MIT License