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("e_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("e_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 ®istry, 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 ®istry, 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(®istry, &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(®istry, &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(®istry, &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(®istry, &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(®istry, &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(®istry, &DtoRegistryRenderOptions::default()) 976 .expect("registry renders"); 977 978 assert_eq!( 979 rendered.body_ts(), 980 "export type ParseError = { InvalidKind: number, } | \"InvalidUnit\";" 981 ); 982 } 983 }