AshTypst.Code protocol (ash_typst v0.4.0)

Copy Markdown View Source

Protocol to support Typst code syntax.

Summary

Types

t()

All the types that implement this protocol.

Functions

Encode Elixir data structures into Typst code syntax.

Types

t()

@type t() :: term()

All the types that implement this protocol.

Functions

encode(value, context)

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:

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

  # ...
end

Query-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?: true fields are considered.
  • Anything the query did not produce is omitted. Attributes excluded via Ash.Query.select/2 (or the select option of a render action's read block) and relationships, calculations, or aggregates that were not loaded do not appear in the dictionary at all — not even as none.
  • Loaded-but-empty is preserved. A field the query did produce with an empty result (e.g. a belongs_to that resolved to nil) encodes as none, 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]
end

Ideally 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