Protocol to support Typst code syntax.
Summary
Functions
Encode Elixir data structures into Typst code syntax.
Types
@type t() :: term()
All the types that implement this protocol.
Functions
Encode Elixir data structures into Typst code syntax.
Examples
iex> AshTypst.Code.encode(~U[2015-01-13 13:00:07Z], %{timezone: "America/New_York"})
"datetime(year: 2015, month: 1, day: 13, hour: 8, minute: 0, second: 7)"
iex> AshTypst.Code.encode(nil, %{})
"none"
iex> AshTypst.Code.encode(%{true: true, false: false, other: :other}, %{})
"(\"false\": false, \"true\": true, \"other\": \"other\")"
iex> AshTypst.Code.encode(["one", 2, 3.0], %{})
"(\"one\", int(2), float(3.0))"The following types are supported by default:
Map->dictionaryList->arrayDecimal->decimalDateTime->datetimeNaiveDateTime->datetimeDate->datetimeTime->datetimeInteger->intFloat->floatString->strAtomconverts one of several Typst types:Ash.Resource(public fields) ->dictionaryAsh.NotLoaded->noneAsh.CiString->str
Structs (including Ash resources) are not encoded unless they opt in. Add
@derive AshTypst.Code to the module to use the built-in implementation
(which serializes an Ash resource's public fields), or implement the protocol
directly with defimpl AshTypst.Code, for: MyStruct for full control:
defmodule MyApp.Invoice do
use Ash.Resource, domain: MyApp.Domain
@derive AshTypst.Code
# ...
endQuery-level compaction
The built-in Ash resource implementation carries only the data your query actually produced. Every public field appears as a key in the emitted dictionary, but the values follow the query:
- Private fields are excluded entirely — only
public?: truefields are considered. - Anything the query did not produce is omitted. Attributes excluded
via
Ash.Query.select/2(or theselectoption of a render action'sreadblock) and relationships, calculations, or aggregates that were not loaded do not appear in the dictionary at all — not even asnone. - Loaded-but-empty is preserved. A field the query did produce with
an empty result (e.g. a
belongs_tothat resolved tonil) encodes asnone, keeping "no value" distinct from "not queried". - Forbidden fields are omitted silently. A field redacted by a field
policy (
Ash.ForbiddenField) is dropped, the same as not-loaded. - Anonymous calculations and aggregates live under
calculations/aggregates. Calculations and aggregates loaded ad hoc on the query (rather than declared on the resource) are encoded as dictionaries under those two keys; when empty, the keys are dropped.
The encoded output is Typst source code that the compiler must parse and evaluate on every compile, so payload size translates directly into compilation time. Because the payload follows the query, it is ideal to de-select attributes your template does not use — especially for large documents rendering thousands of records, where a few unused text columns can multiply the data the Typst compiler has to chew through:
read :many do
select [:name, :amount, :inserted_at]
load [:line_items]
endIdeally a template references only fields its query provides, so absent
keys are never an issue. If a single template is shared by actions with
different query loads, read the varying fields with a default:
record.at("field", default: none).
Key order
Dictionary keys are emitted in Erlang's native map iteration order — they are deliberately not sorted, to keep encoding fast for large datasets. That order is not defined across VM runs, so templates should access fields by name (or sort explicitly when iterating) rather than rely on key order, and byte-identical output across runs is not guaranteed.
To override which keys are encoded for a given struct, pass struct_keys
in the context — a map of struct module to the exact keys to take:
AshTypst.Code.encode(record, %{struct_keys: %{MyApp.Invoice => [:id, :total]}})Context must be passed through. This allows for things like dates to be formatted according to a given timezone, etc.
If timezone is specified in the context, supported types will be automatically shifted to that zone. Ensure you install and configure your choice of timezone database in config.exs:
config :elixir, :time_zone_database, Tzdata.TimeZoneDatabase
config :elixir, :time_zone_database, TimeZoneInfo.TimeZoneDatabase
config :elixir, :time_zone_database, Zoneinfo.TimeZoneDatabase
config :elixir, :time_zone_database, Tz.TimeZoneDatabase