sdk

Radroots SDK and bindings
git clone https://radroots.dev/git/sdk.git
Log | Files | Refs | README

dto_render.rs (33046B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 
      3 use dto_bindgen_core::{
      4     BackendId, Config, EnumDef, EnumRepr, FieldDef, IntRepr, LargeIntPolicy, Primitive, Registry,
      5     StructDef, TypeDef, TypeId, TypeRef, VariantDef, VariantShape,
      6 };
      7 
      8 #[derive(Clone, Debug, Eq, PartialEq)]
      9 pub struct DtoTypesModule {
     10     imports_ts: String,
     11     body_ts: String,
     12 }
     13 
     14 impl DtoTypesModule {
     15     pub fn new(imports_ts: impl Into<String>, body_ts: impl Into<String>) -> Self {
     16         Self {
     17             imports_ts: imports_ts.into(),
     18             body_ts: body_ts.into(),
     19         }
     20     }
     21 
     22     pub fn imports_ts(&self) -> Option<&str> {
     23         (!self.imports_ts.is_empty()).then_some(self.imports_ts.as_str())
     24     }
     25 
     26     pub fn body_ts(&self) -> &str {
     27         self.body_ts.as_str()
     28     }
     29 }
     30 
     31 #[derive(Clone, Debug, Eq, PartialEq)]
     32 pub struct DtoTypeImport {
     33     import_name: String,
     34     from: String,
     35 }
     36 
     37 impl DtoTypeImport {
     38     pub fn new(import_name: impl Into<String>, from: impl Into<String>) -> Self {
     39         Self {
     40             import_name: import_name.into(),
     41             from: from.into(),
     42         }
     43     }
     44 }
     45 
     46 #[derive(Clone, Debug, Eq, PartialEq)]
     47 pub struct DtoRegistryRenderOptions {
     48     config: Config,
     49     external_imports: BTreeMap<TypeId, DtoTypeImport>,
     50     external_overrides: BTreeMap<String, DtoTypeImport>,
     51 }
     52 
     53 impl DtoRegistryRenderOptions {
     54     pub fn new(config: Config) -> Self {
     55         Self {
     56             config,
     57             external_imports: BTreeMap::new(),
     58             external_overrides: BTreeMap::new(),
     59         }
     60     }
     61 
     62     pub fn with_external_type(
     63         mut self,
     64         type_id: TypeId,
     65         import_name: impl Into<String>,
     66         from: impl Into<String>,
     67     ) -> Self {
     68         self.external_imports
     69             .insert(type_id, DtoTypeImport::new(import_name, from));
     70         self
     71     }
     72 
     73     pub fn with_external_override(
     74         mut self,
     75         target_type: impl Into<String>,
     76         import_name: impl Into<String>,
     77         from: impl Into<String>,
     78     ) -> Self {
     79         self.external_overrides
     80             .insert(target_type.into(), DtoTypeImport::new(import_name, from));
     81         self
     82     }
     83 }
     84 
     85 impl Default for DtoRegistryRenderOptions {
     86     fn default() -> Self {
     87         Self::new(Config::default())
     88     }
     89 }
     90 
     91 pub fn render_registry_types(
     92     registry: &Registry,
     93     options: &DtoRegistryRenderOptions,
     94 ) -> Result<DtoTypesModule, String> {
     95     let mut imports = BTreeMap::<String, BTreeSet<String>>::new();
     96     let mut declarations = Vec::new();
     97     let mut type_defs = registry
     98         .types_by_id
     99         .iter()
    100         .filter(|(type_id, _)| !options.external_imports.contains_key(type_id))
    101         .collect::<Vec<_>>();
    102 
    103     type_defs.sort_by(|(_, left), (_, right)| type_name(left).cmp(type_name(right)));
    104 
    105     for (type_id, type_def) in type_defs {
    106         declarations.push(render_type_def(
    107             *type_id,
    108             type_def,
    109             registry,
    110             options,
    111             &mut imports,
    112         )?);
    113     }
    114 
    115     Ok(DtoTypesModule::new(
    116         render_imports(&imports),
    117         declarations.join("\n\n"),
    118     ))
    119 }
    120 
    121 fn render_type_def(
    122     type_id: TypeId,
    123     type_def: &TypeDef,
    124     registry: &Registry,
    125     options: &DtoRegistryRenderOptions,
    126     imports: &mut BTreeMap<String, BTreeSet<String>>,
    127 ) -> Result<String, String> {
    128     match type_def {
    129         TypeDef::Struct(def) => render_struct(type_id, def, registry, options, imports),
    130         TypeDef::Enum(def) => render_enum(type_id, def, registry, options, imports),
    131     }
    132 }
    133 
    134 fn render_struct(
    135     _type_id: TypeId,
    136     def: &StructDef,
    137     registry: &Registry,
    138     options: &DtoRegistryRenderOptions,
    139     imports: &mut BTreeMap<String, BTreeSet<String>>,
    140 ) -> Result<String, String> {
    141     Ok(format!(
    142         "export type {}{} = {};",
    143         struct_type_name(def),
    144         render_generic_params(def.generics.iter().map(|param| param.name.as_str())),
    145         render_object_fields(&def.fields, registry, options, imports)?
    146     ))
    147 }
    148 
    149 fn render_enum(
    150     _type_id: TypeId,
    151     def: &EnumDef,
    152     registry: &Registry,
    153     options: &DtoRegistryRenderOptions,
    154     imports: &mut BTreeMap<String, BTreeSet<String>>,
    155 ) -> Result<String, String> {
    156     match &def.repr {
    157         EnumRepr::External => render_external_enum(def, registry, options, imports),
    158         EnumRepr::Internal { tag } => {
    159             render_tagged_enum(def, tag, None, registry, options, imports)
    160         }
    161         EnumRepr::Adjacent { tag, content } => {
    162             render_tagged_enum(def, tag, Some(content.as_str()), registry, options, imports)
    163         }
    164         EnumRepr::Untagged => render_untagged_enum(def, registry, options, imports),
    165     }
    166 }
    167 
    168 fn render_untagged_enum(
    169     def: &EnumDef,
    170     registry: &Registry,
    171     options: &DtoRegistryRenderOptions,
    172     imports: &mut BTreeMap<String, BTreeSet<String>>,
    173 ) -> Result<String, String> {
    174     let variants = def
    175         .variants
    176         .iter()
    177         .map(|variant| render_untagged_variant(def, variant, registry, options, imports))
    178         .collect::<Result<Vec<_>, _>>()?;
    179     Ok(format!(
    180         "export type {} = {};",
    181         enum_type_name(def),
    182         render_union(variants)
    183     ))
    184 }
    185 
    186 fn render_untagged_variant(
    187     def: &EnumDef,
    188     variant: &VariantDef,
    189     registry: &Registry,
    190     options: &DtoRegistryRenderOptions,
    191     imports: &mut BTreeMap<String, BTreeSet<String>>,
    192 ) -> Result<String, String> {
    193     let rendered: Result<String, String> = match &variant.shape {
    194         VariantShape::Unit => {
    195             Err("untagged unit variants are unsupported for JSON DTO output".to_owned())
    196         }
    197         VariantShape::Newtype(ty) => render_type_ref(ty, None, registry, options, imports),
    198         VariantShape::Tuple(items) => {
    199             let rendered = items
    200                 .iter()
    201                 .map(|item| render_type_ref(item, None, registry, options, imports))
    202                 .collect::<Result<Vec<_>, _>>()?;
    203             Ok(format!("[{}]", rendered.join(", ")))
    204         }
    205         VariantShape::Struct(fields) => render_object_fields(fields, registry, options, imports),
    206     };
    207 
    208     rendered.map_err(|error| {
    209         format!(
    210             "{error} while rendering untagged enum {}.{}",
    211             enum_type_name(def),
    212             variant.rust_name
    213         )
    214     })
    215 }
    216 
    217 fn render_external_enum(
    218     def: &EnumDef,
    219     registry: &Registry,
    220     options: &DtoRegistryRenderOptions,
    221     imports: &mut BTreeMap<String, BTreeSet<String>>,
    222 ) -> Result<String, String> {
    223     let variants = def
    224         .variants
    225         .iter()
    226         .map(|variant| render_external_variant(def, variant, registry, options, imports))
    227         .collect::<Result<Vec<_>, _>>()?;
    228     Ok(format!(
    229         "export type {} = {};",
    230         enum_type_name(def),
    231         render_union(variants)
    232     ))
    233 }
    234 
    235 fn render_external_variant(
    236     def: &EnumDef,
    237     variant: &VariantDef,
    238     registry: &Registry,
    239     options: &DtoRegistryRenderOptions,
    240     imports: &mut BTreeMap<String, BTreeSet<String>>,
    241 ) -> Result<String, String> {
    242     let rendered: Result<String, String> = match &variant.shape {
    243         VariantShape::Unit => Ok(quote_string(&variant.wire_name)),
    244         VariantShape::Newtype(ty) => Ok(format!(
    245             "{{ {}: {}, }}",
    246             render_property_name(&variant.wire_name),
    247             render_type_ref(ty, None, registry, options, imports)?
    248         )),
    249         VariantShape::Tuple(items) => {
    250             let rendered = items
    251                 .iter()
    252                 .map(|item| render_type_ref(item, None, registry, options, imports))
    253                 .collect::<Result<Vec<_>, _>>()?;
    254             Ok(format!(
    255                 "{{ {}: [{}], }}",
    256                 render_property_name(&variant.wire_name),
    257                 rendered.join(", ")
    258             ))
    259         }
    260         VariantShape::Struct(fields) => Ok(format!(
    261             "{{ {}: {}, }}",
    262             render_property_name(&variant.wire_name),
    263             render_object_fields(fields, registry, options, imports)?
    264         )),
    265     };
    266 
    267     rendered.map_err(|error| {
    268         format!(
    269             "{error} while rendering external enum {}.{}",
    270             enum_type_name(def),
    271             variant.rust_name
    272         )
    273     })
    274 }
    275 
    276 fn render_tagged_enum(
    277     def: &EnumDef,
    278     tag: &str,
    279     content: Option<&str>,
    280     registry: &Registry,
    281     options: &DtoRegistryRenderOptions,
    282     imports: &mut BTreeMap<String, BTreeSet<String>>,
    283 ) -> Result<String, String> {
    284     let variants = def
    285         .variants
    286         .iter()
    287         .map(|variant| {
    288             render_tagged_variant(def, variant, tag, content, registry, options, imports)
    289         })
    290         .collect::<Result<Vec<_>, _>>()?;
    291     Ok(format!(
    292         "export type {} = {};",
    293         enum_type_name(def),
    294         render_union(variants)
    295     ))
    296 }
    297 
    298 fn render_tagged_variant(
    299     def: &EnumDef,
    300     variant: &VariantDef,
    301     tag: &str,
    302     content: Option<&str>,
    303     registry: &Registry,
    304     options: &DtoRegistryRenderOptions,
    305     imports: &mut BTreeMap<String, BTreeSet<String>>,
    306 ) -> Result<String, String> {
    307     let mut fields = vec![format!(
    308         "{}: {}",
    309         render_property_name(tag),
    310         quote_string(&variant.wire_name)
    311     )];
    312 
    313     match (&variant.shape, content) {
    314         (VariantShape::Unit, _) => {}
    315         (VariantShape::Struct(variant_fields), Some(content)) => {
    316             fields.push(format!(
    317                 "{}: {}",
    318                 render_property_name(content),
    319                 render_object_fields(variant_fields, registry, options, imports)?
    320             ));
    321         }
    322         (VariantShape::Struct(variant_fields), None) => {
    323             fields.extend(render_object_field_list(
    324                 variant_fields,
    325                 registry,
    326                 options,
    327                 imports,
    328             )?);
    329         }
    330         (VariantShape::Newtype(ty), Some(content)) => {
    331             fields.push(format!(
    332                 "{}: {}",
    333                 render_property_name(content),
    334                 render_type_ref(ty, None, registry, options, imports)?
    335             ));
    336         }
    337         (VariantShape::Tuple(items), Some(content)) => {
    338             let rendered = items
    339                 .iter()
    340                 .map(|item| render_type_ref(item, None, registry, options, imports))
    341                 .collect::<Result<Vec<_>, _>>()?;
    342             fields.push(format!(
    343                 "{}: [{}]",
    344                 render_property_name(content),
    345                 rendered.join(", ")
    346             ));
    347         }
    348         (VariantShape::Newtype(_) | VariantShape::Tuple(_), None) => {
    349             return Err(format!(
    350                 "unsupported internally tagged variant shape for {}.{}",
    351                 enum_type_name(def),
    352                 variant.rust_name
    353             ));
    354         }
    355     }
    356 
    357     Ok(format!("{{ {}, }}", fields.join(", ")))
    358 }
    359 
    360 fn render_object_fields(
    361     fields: &[FieldDef],
    362     registry: &Registry,
    363     options: &DtoRegistryRenderOptions,
    364     imports: &mut BTreeMap<String, BTreeSet<String>>,
    365 ) -> Result<String, String> {
    366     let rendered = render_object_field_list(fields, registry, options, imports)?;
    367     if rendered.is_empty() {
    368         return Ok("{}".to_owned());
    369     }
    370     Ok(format!("{{ {}, }}", rendered.join(", ")))
    371 }
    372 
    373 fn render_object_field_list(
    374     fields: &[FieldDef],
    375     registry: &Registry,
    376     options: &DtoRegistryRenderOptions,
    377     imports: &mut BTreeMap<String, BTreeSet<String>>,
    378 ) -> Result<Vec<String>, String> {
    379     fields
    380         .iter()
    381         .filter(|field| field.presence.is_serialized())
    382         .map(|field| render_object_field(field, registry, options, imports))
    383         .collect()
    384 }
    385 
    386 fn render_object_field(
    387     field: &FieldDef,
    388     registry: &Registry,
    389     options: &DtoRegistryRenderOptions,
    390     imports: &mut BTreeMap<String, BTreeSet<String>>,
    391 ) -> Result<String, String> {
    392     let optional = if field.presence.required_on_deserialize {
    393         ""
    394     } else {
    395         "?"
    396     };
    397     let mut value = render_type_ref(&field.ty, field.int_repr, registry, options, imports)
    398         .map_err(|error| {
    399             format!(
    400                 "{error} while rendering field {} at {}",
    401                 field.target.typescript, field.source
    402             )
    403         })?;
    404     if field.presence.nullable {
    405         value = render_nullable(value);
    406     }
    407     Ok(format!(
    408         "{}{}: {}",
    409         render_property_name(&field.target.typescript),
    410         optional,
    411         value
    412     ))
    413 }
    414 
    415 fn render_type_ref(
    416     ty: &TypeRef,
    417     int_repr: Option<IntRepr>,
    418     registry: &Registry,
    419     options: &DtoRegistryRenderOptions,
    420     imports: &mut BTreeMap<String, BTreeSet<String>>,
    421 ) -> Result<String, String> {
    422     match ty {
    423         TypeRef::Primitive(primitive) => render_primitive(*primitive, int_repr, options),
    424         TypeRef::String => Ok("string".to_owned()),
    425         TypeRef::Bytes(_) => Ok("Uint8Array".to_owned()),
    426         TypeRef::Option(inner) => Ok(render_nullable(render_type_ref(
    427             inner, int_repr, registry, options, imports,
    428         )?)),
    429         TypeRef::Vec(inner) => Ok(format!(
    430             "Array<{}>",
    431             render_type_ref(inner, int_repr, registry, options, imports)?
    432         )),
    433         TypeRef::Array { item, len } => {
    434             let item = render_type_ref(item, int_repr, registry, options, imports)?;
    435             Ok(format!("[{}]", vec![item; *len].join(", ")))
    436         }
    437         TypeRef::Map { key, value } => {
    438             if !matches!(key.as_ref(), TypeRef::String) {
    439                 return Err("non-string map keys are unsupported".to_owned());
    440             }
    441             Ok(format!(
    442                 "Record<string, {}>",
    443                 render_type_ref(value, int_repr, registry, options, imports)?
    444             ))
    445         }
    446         TypeRef::Named(type_id) => render_named_type(*type_id, registry, options, imports),
    447         TypeRef::GenericParam(name) => Ok(name.clone()),
    448         TypeRef::Override(target) if target.backend == BackendId::TypeScript => {
    449             if let Some(import) = options.external_overrides.get(&target.target_type) {
    450                 imports
    451                     .entry(import.from.clone())
    452                     .or_default()
    453                     .insert(import.import_name.clone());
    454                 return Ok(import.import_name.clone());
    455             }
    456             Ok(target.target_type.clone())
    457         }
    458         TypeRef::Override(_) => Err("target override is for a different backend".to_owned()),
    459     }
    460 }
    461 
    462 fn render_named_type(
    463     type_id: TypeId,
    464     registry: &Registry,
    465     options: &DtoRegistryRenderOptions,
    466     imports: &mut BTreeMap<String, BTreeSet<String>>,
    467 ) -> Result<String, String> {
    468     if let Some(import) = options.external_imports.get(&type_id) {
    469         imports
    470             .entry(import.from.clone())
    471             .or_default()
    472             .insert(import.import_name.clone());
    473         return Ok(import.import_name.clone());
    474     }
    475 
    476     let type_def = registry
    477         .type_def(type_id)
    478         .ok_or_else(|| format!("missing named type reference {type_id}"))?;
    479     Ok(type_name(type_def).to_owned())
    480 }
    481 
    482 fn render_primitive(
    483     primitive: Primitive,
    484     int_repr: Option<IntRepr>,
    485     options: &DtoRegistryRenderOptions,
    486 ) -> Result<String, String> {
    487     if primitive.requires_explicit_integer_policy() {
    488         return match int_repr {
    489             Some(IntRepr::JsonString) => Ok("string".to_owned()),
    490             Some(IntRepr::JsonNumberUnsafe) => Ok("number".to_owned()),
    491             Some(IntRepr::NonJsonBigint) => Ok("bigint".to_owned()),
    492             None => match options.config.numeric.large_int_policy {
    493                 LargeIntPolicy::RequireExplicit => {
    494                     Err("large integer field requires explicit numeric policy".to_owned())
    495                 }
    496                 LargeIntPolicy::JsonString => Ok("string".to_owned()),
    497                 LargeIntPolicy::JsonNumberUnsafe => Ok("number".to_owned()),
    498                 LargeIntPolicy::NonJsonBigint => Ok("bigint".to_owned()),
    499             },
    500         };
    501     }
    502 
    503     match primitive {
    504         Primitive::Bool => Ok("boolean".to_owned()),
    505         primitive if primitive.is_integer() || primitive.is_float() => Ok("number".to_owned()),
    506         _ => unreachable!("all primitive variants are covered by bool, integer, or float"),
    507     }
    508 }
    509 
    510 fn render_imports(imports: &BTreeMap<String, BTreeSet<String>>) -> String {
    511     let mut rendered = String::new();
    512     for (from, names) in imports {
    513         if names.len() == 1 {
    514             rendered.push_str("import type { ");
    515             rendered.push_str(names.iter().next().expect("single import name"));
    516             rendered.push_str(" } from ");
    517             rendered.push_str(&quote_string(from));
    518             rendered.push_str(";\n");
    519         } else {
    520             rendered.push_str("import type {\n");
    521             for name in names {
    522                 rendered.push_str("    ");
    523                 rendered.push_str(name);
    524                 rendered.push_str(",\n");
    525             }
    526             rendered.push_str("} from ");
    527             rendered.push_str(&quote_string(from));
    528             rendered.push_str(";\n");
    529         }
    530     }
    531     if !rendered.is_empty() {
    532         rendered.push('\n');
    533     }
    534     rendered
    535 }
    536 
    537 fn render_nullable(value: String) -> String {
    538     if value.split(" | ").any(|part| part == "null") {
    539         value
    540     } else {
    541         format!("{value} | null")
    542     }
    543 }
    544 
    545 fn render_union(items: Vec<String>) -> String {
    546     if items.is_empty() {
    547         return "never".to_owned();
    548     }
    549 
    550     let mut seen = BTreeSet::new();
    551     let mut rendered = Vec::new();
    552     for item in items {
    553         if seen.insert(item.clone()) {
    554             rendered.push(item);
    555         }
    556     }
    557     rendered.join(" | ")
    558 }
    559 
    560 fn render_generic_params<'a>(params: impl Iterator<Item = &'a str>) -> String {
    561     let params = params.collect::<Vec<_>>();
    562     if params.is_empty() {
    563         String::new()
    564     } else {
    565         format!("<{}>", params.join(", "))
    566     }
    567 }
    568 
    569 fn type_name(type_def: &TypeDef) -> &str {
    570     match type_def {
    571         TypeDef::Struct(def) => struct_type_name(def),
    572         TypeDef::Enum(def) => enum_type_name(def),
    573     }
    574 }
    575 
    576 fn struct_type_name(def: &StructDef) -> &str {
    577     def.attrs.ts_name.as_deref().unwrap_or(&def.export_name)
    578 }
    579 
    580 fn enum_type_name(def: &EnumDef) -> &str {
    581     def.attrs.ts_name.as_deref().unwrap_or(&def.export_name)
    582 }
    583 
    584 fn render_property_name(value: &str) -> String {
    585     if is_identifier(value) {
    586         value.to_owned()
    587     } else {
    588         quote_string(value)
    589     }
    590 }
    591 
    592 fn is_identifier(value: &str) -> bool {
    593     let mut chars = value.chars();
    594     match chars.next() {
    595         Some(first) if first == '_' || first == '$' || first.is_ascii_alphabetic() => {}
    596         _ => return false,
    597     }
    598     chars.all(|ch| ch == '_' || ch == '$' || ch.is_ascii_alphanumeric())
    599 }
    600 
    601 fn quote_string(value: &str) -> String {
    602     let mut escaped = String::with_capacity(value.len() + 2);
    603     escaped.push('"');
    604     for ch in value.chars() {
    605         match ch {
    606             '\\' => escaped.push_str("\\\\"),
    607             '"' => escaped.push_str("\\\""),
    608             '\n' => escaped.push_str("\\n"),
    609             '\r' => escaped.push_str("\\r"),
    610             '\t' => escaped.push_str("\\t"),
    611             ch => escaped.push(ch),
    612         }
    613     }
    614     escaped.push('"');
    615     escaped
    616 }
    617 
    618 #[cfg(test)]
    619 mod tests {
    620     use super::{DtoRegistryRenderOptions, render_registry_types};
    621     use dto_bindgen_core::{
    622         BackendId, EnumDef, EnumRepr, FieldDef, FieldPresence, GenericParam, IdentName, IntRepr,
    623         Primitive, Registry, RustTypeId, SourceSpan, StructDef, TargetFieldNames, TargetOverride,
    624         TypeDef, TypeRef, VariantDef, VariantShape, WireFieldNames,
    625     };
    626 
    627     fn span() -> SourceSpan {
    628         SourceSpan::new("src/dto.rs", 1, 1)
    629     }
    630 
    631     fn field(name: &str, target: &str, ty: TypeRef) -> FieldDef {
    632         FieldDef::new(
    633             IdentName::new(name),
    634             WireFieldNames::same(target),
    635             TargetFieldNames::new(target, name),
    636             ty,
    637             span(),
    638         )
    639     }
    640 
    641     #[test]
    642     fn renders_synthetic_registry_as_package_level_types() {
    643         let mut registry = Registry::new();
    644         let external_id = registry.register_type(
    645             RustTypeId::new("external", "ExternalThing"),
    646             TypeDef::Struct(StructDef::new("ExternalThing", "ExternalThing", span())),
    647         );
    648         let envelope = TypeDef::Struct(StructDef {
    649             generics: vec![GenericParam::new("T")],
    650             ..StructDef::new("Envelope", "Envelope", span()).with_field(field(
    651                 "value",
    652                 "value",
    653                 TypeRef::GenericParam("T".to_owned()),
    654             ))
    655         });
    656         registry.register_type(RustTypeId::new("sdk", "Envelope"), envelope);
    657         let thing = TypeDef::Struct(
    658             StructDef::new("SyntheticThing", "SyntheticThing", span())
    659                 .with_field(field("external", "external", TypeRef::Named(external_id)))
    660                 .with_field(
    661                     field(
    662                         "maybe_count",
    663                         "maybeCount",
    664                         TypeRef::Primitive(Primitive::I64),
    665                     )
    666                     .with_presence(FieldPresence::optional_nullable())
    667                     .with_int_repr(IntRepr::JsonString),
    668                 )
    669                 .with_field(field(
    670                     "point",
    671                     "point",
    672                     TypeRef::array(TypeRef::Primitive(Primitive::F64), 2),
    673                 ))
    674                 .with_field(field(
    675                     "envelope",
    676                     "envelope",
    677                     TypeRef::Override(TargetOverride::new(
    678                         BackendId::TypeScript,
    679                         "Envelope<ExternalThing>",
    680                     )),
    681                 )),
    682         );
    683         registry.register_type(RustTypeId::new("sdk", "SyntheticThing"), thing);
    684         let mode = TypeDef::Enum(
    685             EnumDef::new("SyntheticMode", "SyntheticMode", EnumRepr::External, span())
    686                 .with_variant(VariantDef::new(
    687                     "Ready",
    688                     "ready",
    689                     VariantShape::Unit,
    690                     span(),
    691                 ))
    692                 .with_variant(VariantDef::new("Done", "done", VariantShape::Unit, span())),
    693         );
    694         registry.register_type(RustTypeId::new("sdk", "SyntheticMode"), mode);
    695         let event = TypeDef::Enum(
    696             EnumDef::new(
    697                 "SyntheticEvent",
    698                 "SyntheticEvent",
    699                 EnumRepr::Adjacent {
    700                     tag: "type".to_owned(),
    701                     content: "payload".to_owned(),
    702                 },
    703                 span(),
    704             )
    705             .with_variant(VariantDef::new(
    706                 "Created",
    707                 "created",
    708                 VariantShape::Struct(vec![field("id", "id", TypeRef::String)]),
    709                 span(),
    710             ))
    711             .with_variant(VariantDef::new(
    712                 "Archived",
    713                 "archived",
    714                 VariantShape::Struct(vec![
    715                     field("reason", "reason", TypeRef::option(TypeRef::String))
    716                         .with_presence(FieldPresence::optional_nullable_skip_if_none()),
    717                 ]),
    718                 span(),
    719             )),
    720         );
    721         registry.register_type(RustTypeId::new("sdk", "SyntheticEvent"), event);
    722 
    723         let rendered = render_registry_types(
    724             &registry,
    725             &DtoRegistryRenderOptions::default().with_external_type(
    726                 external_id,
    727                 "ExternalThing",
    728                 "@radroots/external-bindings",
    729             ),
    730         )
    731         .expect("registry renders");
    732 
    733         assert_eq!(
    734             rendered.imports_ts(),
    735             Some("import type { ExternalThing } from \"@radroots/external-bindings\";\n\n")
    736         );
    737         assert_eq!(
    738             rendered.body_ts(),
    739             "export type Envelope<T> = { value: T, };\n\nexport type SyntheticEvent = { type: \"created\", payload: { id: string, }, } | { type: \"archived\", payload: { reason?: string | null, }, };\n\nexport type SyntheticMode = \"ready\" | \"done\";\n\nexport type SyntheticThing = { external: ExternalThing, maybeCount?: string | null, point: [number, number], envelope: Envelope<ExternalThing>, };"
    740         );
    741     }
    742 
    743     #[test]
    744     fn imports_typescript_overrides_when_configured() {
    745         let mut registry = Registry::new();
    746         registry.register_type(
    747             RustTypeId::new("sdk", "SyntheticThing"),
    748             TypeDef::Struct(
    749                 StructDef::new("SyntheticThing", "SyntheticThing", span()).with_field(field(
    750                     "external",
    751                     "external",
    752                     TypeRef::Override(TargetOverride::new(BackendId::TypeScript, "ExternalAlias")),
    753                 )),
    754             ),
    755         );
    756 
    757         let rendered = render_registry_types(
    758             &registry,
    759             &DtoRegistryRenderOptions::default().with_external_override(
    760                 "ExternalAlias",
    761                 "ExternalAlias",
    762                 "@radroots/external-bindings",
    763             ),
    764         )
    765         .expect("registry renders");
    766 
    767         assert_eq!(
    768             rendered.imports_ts(),
    769             Some("import type { ExternalAlias } from \"@radroots/external-bindings\";\n\n")
    770         );
    771         assert_eq!(
    772             rendered.body_ts(),
    773             "export type SyntheticThing = { external: ExternalAlias, };"
    774         );
    775     }
    776 
    777     #[test]
    778     fn renders_untagged_object_unions() {
    779         let mut registry = Registry::new();
    780         registry.register_type(
    781             RustTypeId::new("sdk", "Query"),
    782             TypeDef::Enum(
    783                 EnumDef::new("Query", "Query", EnumRepr::Untagged, span())
    784                     .with_variant(VariantDef::new(
    785                         "ById",
    786                         "byId",
    787                         VariantShape::Struct(vec![field("id", "id", TypeRef::String)]),
    788                         span(),
    789                     ))
    790                     .with_variant(VariantDef::new(
    791                         "BySlug",
    792                         "bySlug",
    793                         VariantShape::Struct(vec![field("slug", "slug", TypeRef::String)]),
    794                         span(),
    795                     )),
    796             ),
    797         );
    798 
    799         let rendered = render_registry_types(&registry, &DtoRegistryRenderOptions::default())
    800             .expect("registry renders");
    801 
    802         assert_eq!(
    803             rendered.body_ts(),
    804             "export type Query = { id: string, } | { slug: string, };"
    805         );
    806     }
    807 
    808     #[test]
    809     fn renders_untagged_newtype_aliases() {
    810         let mut registry = Registry::new();
    811         registry.register_type(
    812             RustTypeId::new("sdk", "FindOneResolve"),
    813             TypeDef::Enum(
    814                 EnumDef::new(
    815                     "FindOneResolve",
    816                     "FindOneResolve",
    817                     EnumRepr::Untagged,
    818                     span(),
    819                 )
    820                 .with_variant(VariantDef::new(
    821                     "Alias",
    822                     "alias",
    823                     VariantShape::Newtype(TypeRef::Override(TargetOverride::new(
    824                         BackendId::TypeScript,
    825                         "IResult<Farm | null>",
    826                     ))),
    827                     span(),
    828                 )),
    829             ),
    830         );
    831 
    832         let rendered = render_registry_types(&registry, &DtoRegistryRenderOptions::default())
    833             .expect("registry renders");
    834 
    835         assert_eq!(
    836             rendered.body_ts(),
    837             "export type FindOneResolve = IResult<Farm | null>;"
    838         );
    839     }
    840 
    841     #[test]
    842     fn rejects_untagged_unit_variants() {
    843         let mut registry = Registry::new();
    844         registry.register_type(
    845             RustTypeId::new("sdk", "MaybeReady"),
    846             TypeDef::Enum(
    847                 EnumDef::new("MaybeReady", "MaybeReady", EnumRepr::Untagged, span()).with_variant(
    848                     VariantDef::new("Ready", "ready", VariantShape::Unit, span()),
    849                 ),
    850             ),
    851         );
    852 
    853         let error = render_registry_types(&registry, &DtoRegistryRenderOptions::default())
    854             .expect_err("untagged unit variant blocks render");
    855 
    856         assert_eq!(
    857             error,
    858             "untagged unit variants are unsupported for JSON DTO output while rendering untagged enum MaybeReady.Ready"
    859         );
    860     }
    861 
    862     #[test]
    863     fn requires_explicit_large_integer_policy() {
    864         let mut registry = Registry::new();
    865         registry.register_type(
    866             RustTypeId::new("sdk", "Counter"),
    867             TypeDef::Struct(
    868                 StructDef::new("Counter", "Counter", span()).with_field(field(
    869                     "value",
    870                     "value",
    871                     TypeRef::Primitive(Primitive::U64),
    872                 )),
    873             ),
    874         );
    875 
    876         let error = render_registry_types(&registry, &DtoRegistryRenderOptions::default())
    877             .expect_err("missing policy blocks render");
    878 
    879         assert_eq!(
    880             error,
    881             "large integer field requires explicit numeric policy while rendering field value at src/dto.rs:1:1"
    882         );
    883     }
    884 
    885     #[test]
    886     fn propagates_integer_policy_through_transparent_containers_only() {
    887         let mut registry = Registry::new();
    888         let counter_id = registry.register_type(
    889             RustTypeId::new("sdk", "Counter"),
    890             TypeDef::Struct(
    891                 StructDef::new("Counter", "Counter", span()).with_field(
    892                     field("value", "value", TypeRef::Primitive(Primitive::U64))
    893                         .with_int_repr(IntRepr::JsonNumberUnsafe),
    894                 ),
    895             ),
    896         );
    897         registry.register_type(
    898             RustTypeId::new("sdk", "TransparentCounters"),
    899             TypeDef::Struct(
    900                 StructDef::new("TransparentCounters", "TransparentCounters", span())
    901                     .with_field(
    902                         field(
    903                             "maybe_count",
    904                             "maybeCount",
    905                             TypeRef::option(TypeRef::Primitive(Primitive::U64)),
    906                         )
    907                         .with_presence(FieldPresence::optional_nullable())
    908                         .with_int_repr(IntRepr::JsonString),
    909                     )
    910                     .with_field(
    911                         field(
    912                             "count_list",
    913                             "countList",
    914                             TypeRef::vec(TypeRef::Primitive(Primitive::U64)),
    915                         )
    916                         .with_int_repr(IntRepr::JsonString),
    917                     )
    918                     .with_field(
    919                         field(
    920                             "fixed_counts",
    921                             "fixedCounts",
    922                             TypeRef::array(TypeRef::Primitive(Primitive::U64), 2),
    923                         )
    924                         .with_int_repr(IntRepr::JsonString),
    925                     )
    926                     .with_field(
    927                         field(
    928                             "by_key",
    929                             "byKey",
    930                             TypeRef::Map {
    931                                 key: Box::new(TypeRef::String),
    932                                 value: Box::new(TypeRef::Primitive(Primitive::U64)),
    933                             },
    934                         )
    935                         .with_int_repr(IntRepr::JsonString),
    936                     )
    937                     .with_field(
    938                         field("named_counter", "namedCounter", TypeRef::Named(counter_id))
    939                             .with_int_repr(IntRepr::JsonString),
    940                     ),
    941             ),
    942         );
    943 
    944         let rendered = render_registry_types(&registry, &DtoRegistryRenderOptions::default())
    945             .expect("registry renders");
    946 
    947         assert_eq!(
    948             rendered.body_ts(),
    949             "export type Counter = { value: number, };\n\nexport type TransparentCounters = { maybeCount?: string | null, countList: Array<string>, fixedCounts: [string, string], byKey: Record<string, string>, namedCounter: Counter, };"
    950         );
    951     }
    952 
    953     #[test]
    954     fn renders_external_data_enums() {
    955         let mut registry = Registry::new();
    956         registry.register_type(
    957             RustTypeId::new("sdk", "ParseError"),
    958             TypeDef::Enum(
    959                 EnumDef::new("ParseError", "ParseError", EnumRepr::External, span())
    960                     .with_variant(VariantDef::new(
    961                         "InvalidKind",
    962                         "InvalidKind",
    963                         VariantShape::Newtype(TypeRef::Primitive(Primitive::U32)),
    964                         span(),
    965                     ))
    966                     .with_variant(VariantDef::new(
    967                         "InvalidUnit",
    968                         "InvalidUnit",
    969                         VariantShape::Unit,
    970                         span(),
    971                     )),
    972             ),
    973         );
    974 
    975         let rendered = render_registry_types(&registry, &DtoRegistryRenderOptions::default())
    976             .expect("registry renders");
    977 
    978         assert_eq!(
    979             rendered.body_ts(),
    980             "export type ParseError = { InvalidKind: number, } | \"InvalidUnit\";"
    981         );
    982     }
    983 }