Current section
Files
Jump to
Current section
Files
src/atproto_codegen/emit/union.gleam
//// Open-union synthesis: a named type per union field with one variant per
//// ref plus `Other(Dynamic)`, `$type`-dispatching codecs included.
////
//// The three parts (type, encoder, decoder) are returned separately rather
//// than as one joined string: the `types` plugin renders the first two, the
//// `decode-json` plugin the third, and the host interleaves them with the
//// def's own type/encoder/decoder so the emitted file matches the order the
//// monolithic emitter used to produce.
import atproto_codegen/config.{type Config}
import atproto_codegen/emit/exprs.{escape}
import atproto_codegen/naming.{parse_ref, qualifier, ref_type_name}
import gleam/list
import gleam/string
import justin
pub type UnionParts {
UnionParts(type_def: String, encoder: String, decoder: String)
}
type Variant {
Variant(
ctor: String,
payload: String,
type_keys: List(String),
fields_fn: String,
decoder: String,
)
}
fn union_variant(
cfg: Config,
nsid: String,
name: String,
ref: String,
) -> Variant {
let r = parse_ref(nsid, ref)
let ref_name = ref_type_name(cfg.nsid_prefixes, r)
let qual = qualifier(cfg.nsid_prefixes, nsid, r.nsid)
// A `main` def's wire `$type` is conventionally the bare NSID; accept the fragment spelling too.
let type_keys = case r.def_name {
"main" -> [r.nsid, r.nsid <> "#main"]
def_name -> [r.nsid <> "#" <> def_name]
}
Variant(
ctor: name <> ref_name,
payload: qual <> ref_name,
type_keys:,
fields_fn: qual <> justin.snake_case(ref_name) <> "_fields",
decoder: qual <> justin.snake_case(ref_name) <> "_decoder()",
)
}
/// An open union: one variant per ref plus `Other(Dynamic)`, which round-trips
/// unrecognized `$type`s untouched instead of destroying them.
pub fn emit_union(
cfg: Config,
nsid: String,
name: String,
refs: List(String),
) -> UnionParts {
let variants = list.map(refs, union_variant(cfg, nsid, name, _))
let snake = justin.snake_case(name)
let type_lines =
list.map(variants, fn(v) { " " <> v.ctor <> "(" <> v.payload <> ")" })
|> list.append([" " <> name <> "Other(dynamic.Dynamic)"])
let type_def =
"pub type " <> name <> " {\n" <> string.join(type_lines, "\n") <> "\n}"
let encode_arms =
list.map(variants, fn(v) {
let assert [primary, ..] = v.type_keys
" "
<> v.ctor
<> "(inner) ->\n json.object([\n #(\"$type\", json.string(\""
<> escape(primary)
<> "\")),\n .."
<> v.fields_fn
<> "(inner)\n ])"
})
|> list.append([
" " <> name <> "Other(inner) -> internal.dynamic_to_json(inner)",
])
let encoder =
"pub fn encode_"
<> snake
<> "(value: "
<> name
<> ") -> json.Json {\n case value {\n"
<> string.join(encode_arms, "\n")
<> "\n }\n}"
let decode_arms =
list.map(variants, fn(v) {
let patterns =
v.type_keys
|> list.map(fn(k) { "\"" <> escape(k) <> "\"" })
|> string.join(" | ")
" "
<> patterns
<> " -> decode.map("
<> v.decoder
<> ", "
<> v.ctor
<> ")"
})
|> list.append([" _ -> decode.map(decode.dynamic, " <> name <> "Other)"])
let decoder =
"pub fn "
<> snake
<> "_decoder() -> decode.Decoder("
<> name
<> ") {\n use tag <- decode.optional_field(\"$type\", \"\", decode.string)\n case tag {\n"
<> string.join(decode_arms, "\n")
<> "\n }\n}"
UnionParts(type_def:, encoder:, decoder:)
}