Exosphere.Lexicon.Generator (Exosphere v0.4.0)

Copy Markdown View Source

Generates Elixir record modules from parsed lexicon IR.

For each app.bsky.* lexicon, generates one module for its main def plus nested modules for internal #defs reachable from it:

Each generated module has:

  • a struct with snake_cased fields plus an extra map for unknown keys
  • new/1 — build and validate from atom- or string-keyed attrs, returning {:ok, struct} | {:error, [{path, message}]}
  • to_map/1 — encode to wire format (record mains inject "$type"; union encoders inject "$type" into their variants)
  • from_map/1 — decode a wire map (delegates to new/1)
  • type_id/0 — the def's NSID ("app.bsky.feed.post", "…#replyRef", or nil)

Cross-lexicon refs resolve when the target def is itself being generated; unresolved refs degrade to pass-through term() values.

Summary

Functions

Generate all modules for lexicons (as from Parser.parse_dir/1).

Report refs the generator could not resolve: each is a {source_nsid, ref, location} triple where ref points at an unvendored lexicon (or a def the corpus does not contain), so the affected fields degrade to pass-through term() values.

Write generated specs under lib_dir (default lib/exosphere), creating directories as needed. Returns the paths written.

Types

spec()

@type spec() :: %{
  module: module(),
  path: String.t(),
  nsid: String.t(),
  def_name: String.t() | nil,
  node: Exosphere.Lexicon.Parser.schema_node(),
  lexicon: Exosphere.Lexicon.Parser.lexicon()
}

Functions

generate(lexicons, opts \\ [])

@spec generate(
  %{required(String.t()) => Exosphere.Lexicon.Parser.lexicon()},
  keyword()
) :: [%{module: module(), path: String.t(), code: String.t()}]

Generate all modules for lexicons (as from Parser.parse_dir/1).

Options

  • :base — root module namespace (default Exosphere). With base: MyApp, pub.oysters.post generates as MyApp.Pub.Oysters.Post.
  • :rules — extra namespace rules: {prefix, [segments]} pairs checked before the built-ins, mapping an authority to a fixed module path ({"pub.oysters.", [Oysters, Lexicons]}Oysters.Lexicons.Post instead of the stuttering Oysters.Pub.Oysters.Post).
  • :external%{{nsid, def_name} => module} map of defs that already exist as compiled modules elsewhere (e.g. the library's own generated corpus). Refs resolving into it point at the given module instead of generating a duplicate under :base — a host app referencing com.atproto.repo.strongRef gets Exosphere.ATProto.Repo.StrongRef, not a second struct its library-typed code would reject.
  • :seeds — NSIDs to generate from (default: all lexicons whose main def is a record or object). Refs still resolve against the full lexicons map, so a host app can seed with its own lexicons while passing the vendored corpus alongside for ref resolution.

Namespace rules: app.bsky.* maps to <base>.Bsky.*, com.atproto.* to <base>.ATProto.*, community.lexicon.* to <base>.Community.*; any other authority maps by its full NSID segments.

Generation starts from the seed lexicons whose main def is a record or object (XRPC query/procedure/subscription and permission-set defs are parsed but not yet generated), then walks refs transitively across lexicons — including into main-less defs.json lexicons — generating a nested module for each reachable object/record def. Refs to non-object defs (e.g. string enum defs), to unvendored lexicons, and into :external targets degrade to the referenced module or a pass-through term() value.

Returns specs (module, path relative to the output dir, formatted code), ordered parents-first.

unresolved_refs(lexicons)

@spec unresolved_refs(%{required(String.t()) => Exosphere.Lexicon.Parser.lexicon()}) ::
  [
    {source_nsid :: String.t(), ref :: String.t(), location :: String.t()}
  ]

Report refs the generator could not resolve: each is a {source_nsid, ref, location} triple where ref points at an unvendored lexicon (or a def the corpus does not contain), so the affected fields degrade to pass-through term() values.

write!(specs, lib_dir \\ "lib/exosphere")

@spec write!([%{module: module(), path: String.t(), code: String.t()}], Path.t()) :: [
  Path.t()
]

Write generated specs under lib_dir (default lib/exosphere), creating directories as needed. Returns the paths written.