# `AshTypst.Code`
[🔗](https://github.com/frankdugan3/ash_typst/blob/v0.4.0/lib/ash_typst_code.ex#L1)

Protocol to support Typst code syntax.

# `t`

```elixir
@type t() :: term()
```

All the types that implement this protocol.

# `encode`

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` -> [`dictionary`](https://typst.app/docs/reference/foundations/dictionary/)
- `List` -> [`array`](https://typst.app/docs/reference/foundations/array/)
- `Decimal` -> [`decimal`](https://typst.app/docs/reference/foundations/decimal/)
- `DateTime` -> [`datetime`](https://typst.app/docs/reference/foundations/datetime/)
- `NaiveDateTime` -> [`datetime`](https://typst.app/docs/reference/foundations/datetime/)
- `Date` -> [`datetime`](https://typst.app/docs/reference/foundations/datetime/)
- `Time` -> [`datetime`](https://typst.app/docs/reference/foundations/datetime/)
- `Integer` -> [`int`](https://typst.app/docs/reference/foundations/int/)
- `Float` -> [`float`](https://typst.app/docs/reference/foundations/float/)
- `String` -> [`str`](https://typst.app/docs/reference/foundations/str/)
- `Atom` converts one of several Typst types:
  - `nil` -> [`none`](https://typst.app/docs/reference/foundations/none/)
  - `true`/`false` -> [`bool`](https://typst.app/docs/reference/foundations/bool/)
  - All others -> [`str`](https://typst.app/docs/reference/foundations/str/)
- `Ash.Resource` (public fields) -> [`dictionary`](https://typst.app/docs/reference/foundations/dictionary/)
- `Ash.NotLoaded` -> [`none`](https://typst.app/docs/reference/foundations/none/)
- `Ash.CiString` -> [`str`](https://typst.app/docs/reference/foundations/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:

```elixir
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:

```elixir
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`:

```elixir
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
```

---

*Consult [api-reference.md](api-reference.md) for complete listing*
