lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README

commit 81fce43cda89b3a2089d1bdd45edcb43fb1c9553
parent f55b6d65023e475bf71faee8b7261965d7c5dee9
Author: triesap <tyson@radroots.org>
Date:   Wed, 24 Jun 2026 09:04:55 +0000

replica: render schema bindings from dto registry

Replace the hand-authored replica binding model with a source-owned dto registry exported by radroots_replica_db_schema.

Teach xtask to render untagged DTO enums and route the replica package through TsSource::DtoRegistry.

Regenerate @radroots/replica-db-schema-bindings so JSON partial fields, nullable find-one results, NostrEventHead, and bigint policy match source.

Validate with cargo fmt/check/test, xtask generate/check, and build/typecheck for the affected TypeScript packages.

Diffstat:
Mtools/sdk_xtask_import/src/dto_render.rs | 116++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---
Mtools/sdk_xtask_import/src/dto_roots.rs | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mtools/sdk_xtask_import/src/output.rs | 29+++++++++++++++++++++++++++--
3 files changed, 186 insertions(+), 6 deletions(-)

diff --git a/tools/sdk_xtask_import/src/dto_render.rs b/tools/sdk_xtask_import/src/dto_render.rs @@ -161,13 +161,57 @@ fn render_enum( EnumRepr::Adjacent { tag, content } => { render_tagged_enum(def, tag, Some(content.as_str()), registry, options, imports) } - EnumRepr::Untagged => Err(format!( - "unsupported enum representation for {}", - enum_type_name(def) - )), + EnumRepr::Untagged => render_untagged_enum(def, registry, options, imports), } } +fn render_untagged_enum( + def: &EnumDef, + registry: &Registry, + options: &DtoRegistryRenderOptions, + imports: &mut BTreeMap<String, BTreeSet<String>>, +) -> Result<String, String> { + let variants = def + .variants + .iter() + .map(|variant| render_untagged_variant(def, variant, registry, options, imports)) + .collect::<Result<Vec<_>, _>>()?; + Ok(format!( + "export type {} = {};", + enum_type_name(def), + render_union(variants) + )) +} + +fn render_untagged_variant( + def: &EnumDef, + variant: &VariantDef, + registry: &Registry, + options: &DtoRegistryRenderOptions, + imports: &mut BTreeMap<String, BTreeSet<String>>, +) -> Result<String, String> { + let rendered: Result<String, String> = match &variant.shape { + VariantShape::Unit => Ok("undefined".to_owned()), + VariantShape::Newtype(ty) => render_type_ref(ty, None, registry, options, imports), + VariantShape::Tuple(items) => { + let rendered = items + .iter() + .map(|item| render_type_ref(item, None, registry, options, imports)) + .collect::<Result<Vec<_>, _>>()?; + Ok(format!("[{}]", rendered.join(", "))) + } + VariantShape::Struct(fields) => render_object_fields(fields, registry, options, imports), + }; + + rendered.map_err(|error| { + format!( + "{error} while rendering untagged enum {}.{}", + enum_type_name(def), + variant.rust_name + ) + }) +} + fn render_external_enum( def: &EnumDef, registry: &Registry, @@ -729,6 +773,70 @@ mod tests { } #[test] + fn renders_untagged_object_unions() { + let mut registry = Registry::new(); + registry.register_type( + RustTypeId::new("sdk", "Query"), + TypeDef::Enum( + EnumDef::new("Query", "Query", EnumRepr::Untagged, span()) + .with_variant(VariantDef::new( + "ById", + "byId", + VariantShape::Struct(vec![field("id", "id", TypeRef::String)]), + span(), + )) + .with_variant(VariantDef::new( + "BySlug", + "bySlug", + VariantShape::Struct(vec![field("slug", "slug", TypeRef::String)]), + span(), + )), + ), + ); + + let rendered = render_registry_types(&registry, &DtoRegistryRenderOptions::default()) + .expect("registry renders"); + + assert_eq!( + rendered.body_ts(), + "export type Query = { id: string, } | { slug: string, };" + ); + } + + #[test] + fn renders_untagged_newtype_aliases() { + let mut registry = Registry::new(); + registry.register_type( + RustTypeId::new("sdk", "FindOneResolve"), + TypeDef::Enum( + EnumDef::new( + "FindOneResolve", + "FindOneResolve", + EnumRepr::Untagged, + span(), + ) + .with_variant(VariantDef::new( + "Alias", + "alias", + VariantShape::Newtype(TypeRef::Override(TargetOverride::new( + BackendId::TypeScript, + "IResult<Farm | null>", + ))), + span(), + )), + ), + ); + + let rendered = render_registry_types(&registry, &DtoRegistryRenderOptions::default()) + .expect("registry renders"); + + assert_eq!( + rendered.body_ts(), + "export type FindOneResolve = IResult<Farm | null>;" + ); + } + + #[test] fn requires_explicit_large_integer_policy() { let mut registry = Registry::new(); registry.register_type( diff --git a/tools/sdk_xtask_import/src/dto_roots.rs b/tools/sdk_xtask_import/src/dto_roots.rs @@ -165,6 +165,13 @@ pub fn events_indexed_types_module() -> Result<DtoTypesModule, String> { )) } +pub fn replica_db_schema_types_module() -> Result<DtoTypesModule, String> { + render_registry_types( + &radroots_replica_db_schema_bindings::dto_registry(), + &DtoRegistryRenderOptions::default(), + ) +} + pub fn trade_types_module() -> Result<DtoTypesModule, String> { let root_set = package_root_set("trade").ok_or_else(|| "missing trade DTO roots".to_owned())?; let registry = root_set.registry(); @@ -330,6 +337,8 @@ mod tests { include_str!("../../../packages/events-bindings/src/generated/types.ts"); const EVENTS_INDEXED_BINDINGS_TYPES_TS: &str = include_str!("../../../packages/events-indexed-bindings/src/generated/types.ts"); + const REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/replica-db-schema-bindings/src/generated/types.ts"); const TRADE_BINDINGS_TYPES_TS: &str = include_str!("../../../packages/trade-bindings/src/generated/types.ts"); const EVENTS_TYPE_INVENTORY: &[&str] = &[ @@ -556,6 +565,37 @@ mod tests { } #[test] + fn replica_db_schema_generated_types_preserve_source_schema_contracts() { + let actual = type_inventory(REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS); + let trade_product_filter = type_declaration( + REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS, + "ITradeProductFieldsFilter", + ); + let trade_product_partial = type_declaration( + REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS, + "ITradeProductFieldsPartial", + ); + + assert!(actual.contains(&"Farm")); + assert!(actual.contains(&"GcsLocation")); + assert!(actual.contains(&"NostrEventHead")); + assert!(actual.contains(&"ReplicaDbJsonValue")); + assert!(actual.contains(&"ITradeProductFieldsPartial")); + assert!(!actual.contains(&"NostrEventState")); + assert!(REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS.contains( + "export type ReplicaDbJsonValue = null | boolean | number | string | Array<ReplicaDbJsonValue> | { [key: string]: ReplicaDbJsonValue };" + )); + assert!( + REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS + .contains("export type IFarmFindOneResolve = IResult<Farm | null>;") + ); + assert!(trade_product_filter.contains("year?: bigint")); + assert!(trade_product_filter.contains("qty_avail?: bigint")); + assert!(trade_product_partial.contains("year?: ReplicaDbJsonValue | null")); + assert!(trade_product_partial.contains("qty_avail?: ReplicaDbJsonValue | null")); + } + + #[test] fn trade_package_imports_source_owned_support_types() { assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/core-bindings\"")); assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/events-bindings\"")); @@ -577,4 +617,11 @@ mod tests { .map(|rest| rest.split([' ', '<']).next().expect("type name")) .collect() } + + fn type_declaration<'a>(types_ts: &'a str, name: &str) -> &'a str { + types_ts + .lines() + .find(|line| line.starts_with(&format!("export type {name} = "))) + .unwrap_or_else(|| panic!("missing type declaration for {name}")) + } } diff --git a/tools/sdk_xtask_import/src/output.rs b/tools/sdk_xtask_import/src/output.rs @@ -114,8 +114,8 @@ pub fn package_outputs() -> Result<Vec<PackageOutput>, String> { }, PackageOutput { spec: spec_by_key("replica_db_schema"), - types_ts: Some(TsSource::Module( - radroots_replica_db_schema_bindings::types_module(), + types_ts: Some(TsSource::DtoRegistry( + dto_roots::replica_db_schema_types_module()?, )), types_imports_ts: Some(REPLICA_DB_SCHEMA_TYPES_IMPORTS_TS), constants_ts: None, @@ -207,6 +207,8 @@ mod tests { const TRADE_BINDINGS_TYPES_TS: &str = include_str!("../../../packages/trade-bindings/src/generated/types.ts"); + const REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS: &str = + include_str!("../../../packages/replica-db-schema-bindings/src/generated/types.ts"); #[test] fn renders_sdk_header() { @@ -311,4 +313,27 @@ mod tests { assert_eq!(types.contents, TRADE_BINDINGS_TYPES_TS); } + + #[test] + fn replica_db_schema_output_uses_dto_registry_and_matches_checked_in_types() { + let output = package_outputs() + .expect("package outputs") + .into_iter() + .find(|output| output.spec.key == "replica_db_schema") + .expect("replica_db_schema output"); + + assert!(matches!(output.types_ts, Some(TsSource::DtoRegistry(_)))); + assert_eq!( + output.types_imports_ts, + Some(super::REPLICA_DB_SCHEMA_TYPES_IMPORTS_TS) + ); + + let types = output + .files() + .into_iter() + .find(|file| file.relative_path == "src/generated/types.ts") + .expect("types file"); + + assert_eq!(types.contents, REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS); + } }