···
defmodule Atex.Lexicon do
3
-
Provide `deflexicon` macro for defining a module with types and schemas from an entire lexicon definition.
5
-
Should it also define structs, with functions to convert from input case to snake case?
alias Atex.Lexicon.Validators
defmacro __using__(_opts) do
···
13
+
Defines a lexicon module from a JSON lexicon definition.
15
+
The `deflexicon` macro processes the provided lexicon map (typically loaded
16
+
from a JSON file) and generates:
18
+
- **Typespecs** for each definition, exposing a `t/0` type for the main
19
+
definition and named types for any additional definitions.
20
+
- **`Peri` schemas** via `defschema/2` for runtime validation of data.
21
+
- **Structs** for object and record definitions, with `@enforce_keys` ensuring
22
+
required fields are present.
23
+
- For **queries** and **procedures**, it creates structs for `params`,
24
+
`input`, and `output` when those sections exist in the lexicon. It also
25
+
generates a top‑level struct that aggregates `params` and `input` (when
26
+
applicable); this struct is used by the XRPC client to locate the
27
+
appropriate output struct.
29
+
If a procedure doesn't have a schema for a JSON body specified as it's input,
30
+
the top-level struct will instead have a `raw_input` field, allowing for
31
+
miscellaneous bodies such as a binary blob.
33
+
The generated structs also implement the `JSON.Encoder` and `Jason.Encoder`
34
+
protocols (the latter currently present for compatibility), as well as a
35
+
`from_json` function which is used to validate an input map - e.g. from a JSON
36
+
HTTP response - and turn it into a struct.
42
+
"id" => "com.ovyerus.testing",
49
+
"required" => ["foobar"],
50
+
"properties" => %{ "foobar" => %{ "type" => "string" } }
56
+
The macro expands to following code (truncated for brevity):
58
+
@type main() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()}
59
+
@type t() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()}
62
+
foobar: {:required, {:custom, {Atex.Lexicon.Validators.String, :validate, [[]]}}},
63
+
"$type": {{:literal, "com.ovyerus.testing"}, {:default, "com.ovyerus.testing"}}
66
+
@enforce_keys [:foobar]
67
+
defstruct foobar: nil, "$type": "com.ovyerus.testing"
69
+
def from_json(json) do
70
+
case apply(Com.Ovyerus.Testing, :main, [json]) do
71
+
{:ok, map} -> {:ok, struct(__MODULE__, map)}
76
+
The generated module can be used directly with `Atex.XRPC` functions, allowing
77
+
type‑safe construction of requests and automatic decoding of responses.
defmacro deflexicon(lexicon) do
# Better way to get the real map, without having to eval? (custom function to compose one from quoted?)
···
|> then(&Recase.Enumerable.atomize_keys/1)
|> then(&Atex.Lexicon.Schema.lexicon!/1)
27
-
lexicon_id = Atex.NSID.to_atom(lexicon.id)
···
61
-
@type unquote(schema_key)() :: unquote(quoted_type)
120
+
@type unquote(Recase.to_snake(schema_key))() :: unquote(quoted_type)
64
-
defschema unquote(schema_key), unquote(quoted_schema)
123
+
defschema unquote(Recase.to_snake(schema_key)), unquote(quoted_schema)
71
-
def id, do: unquote(lexicon_id)
130
+
def id, do: unquote(lexicon.id)
···
key not in required && key not in nullable && key != "$type"
235
+
schema_module = Atex.NSID.to_atom(nsid)
@enforce_keys unquote(enforced_keys)
defstruct unquote(struct_keys)
242
+
def from_json(json) do
243
+
case apply(unquote(schema_module), unquote(atomise(def_name)), [json]) do
244
+
{:ok, map} -> {:ok, struct(__MODULE__, map)}
@optional_if_nil_keys unquote(optional_if_nil_keys)
···
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
|> Atex.NSID.to_atom_with_fragment()
588
+
fragment = Recase.to_snake(fragment)
Macro.escape(Validators.lazy_ref(nsid, fragment)),
···
|> Atex.NSID.expand_possible_fragment_shorthand(ref)
|> Atex.NSID.to_atom_with_fragment()
611
+
fragment = Recase.to_snake(fragment)
Macro.escape(Validators.lazy_ref(nsid, fragment)),