An Elixir toolkit for the AT Protocol. hexdocs.pm/atex
elixir bluesky atproto decentralization

refactor: snake_case all lexicon defs

ovyerus.com 72f1ab6d 75981faa

verified
+94 -12
+10 -1
CHANGELOG.md
··· 6 6 and this project adheres to 7 7 [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 8 8 9 - <!-- ## [Unreleased] --> 9 + ## [Unreleased] 10 + 11 + ### Breking Changes 12 + 13 + - `deflexicon` now converts all def names to be in snake_case instead of the 14 + casing as written the lexicon. 15 + 16 + ### Added 17 + 18 + - `deflexicon` now emits structs for records, objects, queries, and procedures. 10 19 11 20 ## [0.5.0] - 2025-10-11 12 21
+83 -11
lib/atex/lexicon.ex
··· 1 1 defmodule Atex.Lexicon do 2 - @moduledoc """ 3 - Provide `deflexicon` macro for defining a module with types and schemas from an entire lexicon definition. 4 - 5 - Should it also define structs, with functions to convert from input case to snake case? 6 - """ 7 - 8 2 alias Atex.Lexicon.Validators 9 3 10 4 defmacro __using__(_opts) do ··· 15 9 end 16 10 end 17 11 12 + @doc """ 13 + Defines a lexicon module from a JSON lexicon definition. 14 + 15 + The `deflexicon` macro processes the provided lexicon map (typically loaded 16 + from a JSON file) and generates: 17 + 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. 28 + 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. 32 + 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. 37 + 38 + ## Example 39 + 40 + deflexicon(%{ 41 + "lexicon" => 1, 42 + "id" => "com.ovyerus.testing", 43 + "defs" => %{ 44 + "main" => %{ 45 + "type" => "record", 46 + "key" => "tid", 47 + "record" => %{ 48 + "type" => "object", 49 + "required" => ["foobar"], 50 + "properties" => %{ "foobar" => %{ "type" => "string" } } 51 + } 52 + } 53 + } 54 + }) 55 + 56 + The macro expands to following code (truncated for brevity): 57 + 58 + @type main() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()} 59 + @type t() :: %{required(:foobar) => String.t(), optional(:"$type") => String.t()} 60 + 61 + defschema(:main, %{ 62 + foobar: {:required, {:custom, {Atex.Lexicon.Validators.String, :validate, [[]]}}}, 63 + "$type": {{:literal, "com.ovyerus.testing"}, {:default, "com.ovyerus.testing"}} 64 + }) 65 + 66 + @enforce_keys [:foobar] 67 + defstruct foobar: nil, "$type": "com.ovyerus.testing" 68 + 69 + def from_json(json) do 70 + case apply(Com.Ovyerus.Testing, :main, [json]) do 71 + {:ok, map} -> {:ok, struct(__MODULE__, map)} 72 + err -> err 73 + end 74 + end 75 + 76 + The generated module can be used directly with `Atex.XRPC` functions, allowing 77 + type‑safe construction of requests and automatic decoding of responses. 78 + """ 18 79 defmacro deflexicon(lexicon) do 19 80 # Better way to get the real map, without having to eval? (custom function to compose one from quoted?) 20 81 lexicon = ··· 23 84 |> elem(0) 24 85 |> then(&Recase.Enumerable.atomize_keys/1) 25 86 |> then(&Atex.Lexicon.Schema.lexicon!/1) 26 - 27 - lexicon_id = Atex.NSID.to_atom(lexicon.id) 28 87 29 88 defs = 30 89 lexicon.defs ··· 58 117 end 59 118 60 119 quote do 61 - @type unquote(schema_key)() :: unquote(quoted_type) 120 + @type unquote(Recase.to_snake(schema_key))() :: unquote(quoted_type) 62 121 unquote(identity_type) 63 122 64 - defschema unquote(schema_key), unquote(quoted_schema) 123 + defschema unquote(Recase.to_snake(schema_key)), unquote(quoted_schema) 65 124 66 125 unquote(struct_def) 67 126 end 68 127 end) 69 128 70 129 quote do 71 - def id, do: unquote(lexicon_id) 130 + def id, do: unquote(lexicon.id) 72 131 73 132 unquote_splicing(defs) 74 133 end ··· 173 232 key not in required && key not in nullable && key != "$type" 174 233 end) 175 234 235 + schema_module = Atex.NSID.to_atom(nsid) 236 + 176 237 quoted_struct = 177 238 quote do 178 239 @enforce_keys unquote(enforced_keys) 179 240 defstruct unquote(struct_keys) 241 + 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)} 245 + err -> err 246 + end 247 + end 180 248 181 249 defimpl JSON.Encoder do 182 250 @optional_if_nil_keys unquote(optional_if_nil_keys) ··· 517 585 |> Atex.NSID.expand_possible_fragment_shorthand(ref) 518 586 |> Atex.NSID.to_atom_with_fragment() 519 587 588 + fragment = Recase.to_snake(fragment) 589 + 520 590 { 521 591 Macro.escape(Validators.lazy_ref(nsid, fragment)), 522 592 quote do ··· 537 607 nsid 538 608 |> Atex.NSID.expand_possible_fragment_shorthand(ref) 539 609 |> Atex.NSID.to_atom_with_fragment() 610 + 611 + fragment = Recase.to_snake(fragment) 540 612 541 613 { 542 614 Macro.escape(Validators.lazy_ref(nsid, fragment)),
+1
lib/atex/lexicon/validators/string.ex
··· 56 56 # TODO: is there a regex provided by the lexicon docs/somewhere? 57 57 try do 58 58 Multiformats.CID.decode(value) 59 + :ok 59 60 rescue 60 61 _ -> {:error, "should be a valid CID", []} 61 62 end