dto_roots.rs (25825B)
1 use dto_bindgen_core::{Registry, RootDescriptor, RustTypeId, TypeId, build_registry}; 2 3 use crate::dto_render::{DtoRegistryRenderOptions, DtoTypesModule, render_registry_types}; 4 5 #[derive(Clone, Copy, Debug)] 6 pub struct DtoPackageRootSet { 7 pub package_key: &'static str, 8 roots: fn() -> Vec<RootDescriptor>, 9 } 10 11 impl DtoPackageRootSet { 12 pub fn roots(&self) -> Vec<RootDescriptor> { 13 (self.roots)() 14 } 15 16 pub fn registry(&self) -> Registry { 17 build_registry(self.roots()) 18 } 19 } 20 21 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 22 pub struct ManualDescriptorFamily { 23 pub package_key: &'static str, 24 pub source_family: &'static str, 25 pub reason: &'static str, 26 } 27 28 #[derive(Clone, Copy, Debug, Eq, PartialEq)] 29 pub struct SdkLocalWrapperAllowance { 30 pub package_key: &'static str, 31 pub shape_family: &'static str, 32 pub reason: &'static str, 33 } 34 35 pub const DTO_PACKAGE_ROOTS: &[DtoPackageRootSet] = &[ 36 DtoPackageRootSet { 37 package_key: "core", 38 roots: core_roots, 39 }, 40 DtoPackageRootSet { 41 package_key: "events", 42 roots: events_roots, 43 }, 44 DtoPackageRootSet { 45 package_key: "events_indexed", 46 roots: events_indexed_roots, 47 }, 48 DtoPackageRootSet { 49 package_key: "trade", 50 roots: trade_roots, 51 }, 52 DtoPackageRootSet { 53 package_key: "types", 54 roots: types_roots, 55 }, 56 ]; 57 58 pub const MANUAL_DESCRIPTOR_FAMILIES: &[ManualDescriptorFamily] = &[ 59 ManualDescriptorFamily { 60 package_key: "core", 61 source_family: "decimal, currency, money, quantity, percent, quantity price, unit, and discount value families", 62 reason: "custom serde, string-backed newtypes, aliases, and tagged enum wire forms require source-owned manual descriptors", 63 }, 64 ManualDescriptorFamily { 65 package_key: "events", 66 source_family: "event timestamps, counters, and optional metadata fields", 67 reason: "large integers and source-specific optional/null policy must be explicit", 68 }, 69 ManualDescriptorFamily { 70 package_key: "events", 71 source_family: "GeoJSON coordinate arrays", 72 reason: "fixed-size Rust arrays must preserve tuple semantics in TypeScript", 73 }, 74 ManualDescriptorFamily { 75 package_key: "events_indexed", 76 source_family: "checkpoint and index cursor fields", 77 reason: "custom deserialization and large integers require manual descriptor policy", 78 }, 79 ManualDescriptorFamily { 80 package_key: "trade", 81 source_family: "trade listing roots and package projection count fields", 82 reason: "core aliases, source-owned event imports, and count-family numeric policy require explicit descriptors", 83 }, 84 ManualDescriptorFamily { 85 package_key: "replica_db_schema", 86 source_family: "untagged query wrappers and serde_json value fields", 87 reason: "schema query shapes are generated and not all source fields map to derive-supported DTOs", 88 }, 89 ManualDescriptorFamily { 90 package_key: "types", 91 source_family: "generic result wrapper types", 92 reason: "generic export instantiations must be explicit and package-scoped", 93 }, 94 ]; 95 96 pub const SDK_LOCAL_WRAPPER_ALLOWANCES: &[SdkLocalWrapperAllowance] = &[ 97 SdkLocalWrapperAllowance { 98 package_key: "core", 99 shape_family: "RadrootsCoreCurrency and RadrootsCoreDecimal package aliases", 100 reason: "source descriptors correctly describe fields as strings, while package roots still need stable named TypeScript aliases", 101 }, 102 SdkLocalWrapperAllowance { 103 package_key: "replica_db_schema", 104 shape_family: "generated query argument wrappers", 105 reason: "schema operation inputs are generated package shapes rather than source-owned public DTO structs", 106 }, 107 SdkLocalWrapperAllowance { 108 package_key: "types", 109 shape_family: "IResult, IResultList, and IResultPass generic envelopes", 110 reason: "generic helper envelopes are SDK package contracts used across generated schema packages", 111 }, 112 SdkLocalWrapperAllowance { 113 package_key: "events_indexed", 114 shape_family: "RadrootsEventsIndexedShardId package alias", 115 reason: "source descriptors correctly describe the shard id newtype as a string, while package roots still need a stable named TypeScript alias", 116 }, 117 SdkLocalWrapperAllowance { 118 package_key: "trade", 119 shape_family: "marketplace, query, projection, sort, review, and backoffice DTO shapes", 120 reason: "these are SDK package contract shapes layered over source-owned trade, events, and core DTOs", 121 }, 122 ]; 123 124 pub fn package_root_set(package_key: &str) -> Option<&'static DtoPackageRootSet> { 125 DTO_PACKAGE_ROOTS 126 .iter() 127 .find(|root_set| root_set.package_key == package_key) 128 } 129 130 pub fn core_types_module() -> Result<DtoTypesModule, String> { 131 let root_set = package_root_set("core").ok_or_else(|| "missing core DTO roots".to_owned())?; 132 let rendered = 133 render_registry_types(&root_set.registry(), &DtoRegistryRenderOptions::default())?; 134 Ok(DtoTypesModule::new( 135 rendered.imports_ts().unwrap_or_default(), 136 format!( 137 "export type RadrootsCoreCurrency = string;\n\nexport type RadrootsCoreDecimal = string;\n\n{}", 138 rendered.body_ts() 139 ), 140 )) 141 } 142 143 pub fn events_types_module() -> Result<DtoTypesModule, String> { 144 let root_set = 145 package_root_set("events").ok_or_else(|| "missing events DTO roots".to_owned())?; 146 let registry = root_set.registry(); 147 let rendered = render_registry_types( 148 ®istry, 149 &core_import_options(®istry, DtoRegistryRenderOptions::default()), 150 )?; 151 Ok(DtoTypesModule::new( 152 rendered.imports_ts().unwrap_or_default(), 153 with_events_sdk_wrappers(rendered.body_ts()), 154 )) 155 } 156 157 pub fn events_indexed_types_module() -> Result<DtoTypesModule, String> { 158 let root_set = package_root_set("events_indexed") 159 .ok_or_else(|| "missing events-indexed DTO roots".to_owned())?; 160 let rendered = 161 render_registry_types(&root_set.registry(), &DtoRegistryRenderOptions::default())?; 162 Ok(DtoTypesModule::new( 163 rendered.imports_ts().unwrap_or_default(), 164 with_events_indexed_sdk_wrappers(rendered.body_ts()), 165 )) 166 } 167 168 pub fn replica_db_schema_types_module() -> Result<DtoTypesModule, String> { 169 render_registry_types( 170 &radroots_replica_db_schema_bindings::dto_registry(), 171 &DtoRegistryRenderOptions::default(), 172 ) 173 } 174 175 pub fn trade_types_module() -> Result<DtoTypesModule, String> { 176 let root_set = package_root_set("trade").ok_or_else(|| "missing trade DTO roots".to_owned())?; 177 let registry = root_set.registry(); 178 render_registry_types( 179 ®istry, 180 &trade_import_options(DtoRegistryRenderOptions::default()), 181 ) 182 } 183 184 pub fn types_types_module() -> Result<DtoTypesModule, String> { 185 let root_set = package_root_set("types").ok_or_else(|| "missing types DTO roots".to_owned())?; 186 render_registry_types(&root_set.registry(), &DtoRegistryRenderOptions::default()) 187 } 188 189 fn core_roots() -> Vec<RootDescriptor> { 190 radroots_core::dto::dto_roots().into_iter().collect() 191 } 192 193 fn events_roots() -> Vec<RootDescriptor> { 194 radroots_events::dto::dto_roots().into_iter().collect() 195 } 196 197 fn events_indexed_roots() -> Vec<RootDescriptor> { 198 radroots_events_indexed::dto::dto_roots() 199 .into_iter() 200 .collect() 201 } 202 203 fn trade_roots() -> Vec<RootDescriptor> { 204 radroots_trade_bindings::dto_roots() 205 } 206 207 fn types_roots() -> Vec<RootDescriptor> { 208 radroots_types_bindings::dto_roots() 209 } 210 211 fn core_import_options( 212 registry: &Registry, 213 mut options: DtoRegistryRenderOptions, 214 ) -> DtoRegistryRenderOptions { 215 for export_name in [ 216 "RadrootsCoreCurrency", 217 "RadrootsCoreDecimal", 218 "RadrootsCoreDiscount", 219 "RadrootsCoreDiscountScope", 220 "RadrootsCoreDiscountThreshold", 221 "RadrootsCoreDiscountValue", 222 "RadrootsCoreMoney", 223 "RadrootsCorePercent", 224 "RadrootsCoreQuantity", 225 "RadrootsCoreQuantityPrice", 226 "RadrootsCoreUnit", 227 "RadrootsCoreUnitDimension", 228 ] { 229 if let Some(type_id) = core_type_id(registry, export_name) { 230 options = options.with_external_type(type_id, export_name, "@radroots/core-bindings"); 231 } 232 } 233 options 234 } 235 236 fn core_type_id(registry: &Registry, rust_ident: &str) -> Option<TypeId> { 237 registry 238 .rust_id_to_type_id 239 .get(&RustTypeId::new("radroots_core", rust_ident)) 240 .copied() 241 } 242 243 fn trade_import_options(mut options: DtoRegistryRenderOptions) -> DtoRegistryRenderOptions { 244 for export_name in [ 245 "RadrootsCoreCurrency", 246 "RadrootsCoreDecimal", 247 "RadrootsCoreDiscount", 248 "RadrootsCoreDiscountValue", 249 "RadrootsCoreMoney", 250 "RadrootsCoreQuantity", 251 "RadrootsCoreQuantityPrice", 252 "RadrootsCoreUnit", 253 ] { 254 options = 255 options.with_external_override(export_name, export_name, "@radroots/core-bindings"); 256 } 257 258 for export_name in [ 259 "RadrootsFarmRef", 260 "RadrootsListing", 261 "RadrootsListingAvailability", 262 "RadrootsListingBin", 263 "RadrootsListingDeliveryMethod", 264 "RadrootsListingImage", 265 "RadrootsListingLocation", 266 "RadrootsListingProduct", 267 "RadrootsListingStatus", 268 "RadrootsNostrEventPtr", 269 "RadrootsPlotRef", 270 "RadrootsResourceAreaRef", 271 "RadrootsTradeMessagePayload", 272 "RadrootsTradeMessageType", 273 "RadrootsTradeOrderEconomicLine", 274 "RadrootsTradeOrderItem", 275 "RadrootsTradeOrderStatus", 276 ] { 277 options = 278 options.with_external_override(export_name, export_name, "@radroots/events-bindings"); 279 } 280 281 options 282 } 283 284 fn with_events_sdk_wrappers(body: &str) -> String { 285 let mut declarations = body 286 .split("\n\n") 287 .filter(|declaration| !declaration.trim().is_empty()) 288 .map(str::to_owned) 289 .collect::<Vec<_>>(); 290 declarations.push( 291 "export type RadrootsListingProductTagKeys = readonly [\"key\", \"title\", \"category\", \"summary\", \"process\", \"lot\", \"location\", \"profile\", \"year\"];" 292 .to_owned(), 293 ); 294 declarations.sort_by(|left, right| declaration_name(left).cmp(declaration_name(right))); 295 declarations.join("\n\n") 296 } 297 298 fn declaration_name(declaration: &str) -> &str { 299 declaration 300 .strip_prefix("export type ") 301 .and_then(|rest| rest.split([' ', '<']).next()) 302 .unwrap_or(declaration) 303 } 304 305 fn with_events_indexed_sdk_wrappers(body: &str) -> String { 306 let mut declarations = body 307 .split("\n\n") 308 .filter(|declaration| !declaration.trim().is_empty()) 309 .map(str::to_owned) 310 .collect::<Vec<_>>(); 311 declarations.push("export type RadrootsEventsIndexedShardId = string;".to_owned()); 312 let order = [ 313 "RadrootsEventsIndexedShardId", 314 "RadrootsEventsIndexedIdRange", 315 "RadrootsEventsIndexedShardMetadata", 316 "RadrootsEventsIndexedManifest", 317 "RadrootsEventsIndexedShardCheckpoint", 318 "RadrootsEventsIndexedIndexCheckpoint", 319 ]; 320 declarations.sort_by_key(|declaration| { 321 order 322 .iter() 323 .position(|name| *name == declaration_name(declaration)) 324 .unwrap_or(order.len()) 325 }); 326 declarations.join("\n\n") 327 } 328 329 #[cfg(test)] 330 mod tests { 331 use std::collections::BTreeSet; 332 333 use super::{ 334 DTO_PACKAGE_ROOTS, MANUAL_DESCRIPTOR_FAMILIES, SDK_LOCAL_WRAPPER_ALLOWANCES, 335 package_root_set, 336 }; 337 338 const EVENTS_BINDINGS_TYPES_TS: &str = 339 include_str!("../../../packages/events-bindings/src/generated/types.ts"); 340 const EVENTS_INDEXED_BINDINGS_TYPES_TS: &str = 341 include_str!("../../../packages/events-indexed-bindings/src/generated/types.ts"); 342 const REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS: &str = 343 include_str!("../../../packages/replica-db-schema-bindings/src/generated/types.ts"); 344 const TRADE_BINDINGS_TYPES_TS: &str = 345 include_str!("../../../packages/trade-bindings/src/generated/types.ts"); 346 const REPLICA_SCHEMA_MODEL_SOURCES: &[&str] = &[ 347 include_str!("../../../../lib/crates/replica_db_schema/src/models/farm.rs"), 348 include_str!("../../../../lib/crates/replica_db_schema/src/models/farm_gcs_location.rs"), 349 include_str!("../../../../lib/crates/replica_db_schema/src/models/farm_member.rs"), 350 include_str!("../../../../lib/crates/replica_db_schema/src/models/farm_member_claim.rs"), 351 include_str!("../../../../lib/crates/replica_db_schema/src/models/farm_tag.rs"), 352 include_str!("../../../../lib/crates/replica_db_schema/src/models/gcs_location.rs"), 353 include_str!("../../../../lib/crates/replica_db_schema/src/models/log_error.rs"), 354 include_str!("../../../../lib/crates/replica_db_schema/src/models/media_image.rs"), 355 include_str!("../../../../lib/crates/replica_db_schema/src/models/nostr_event_head.rs"), 356 include_str!("../../../../lib/crates/replica_db_schema/src/models/nostr_profile.rs"), 357 include_str!("../../../../lib/crates/replica_db_schema/src/models/nostr_profile_relay.rs"), 358 include_str!("../../../../lib/crates/replica_db_schema/src/models/nostr_relay.rs"), 359 include_str!("../../../../lib/crates/replica_db_schema/src/models/plot.rs"), 360 include_str!("../../../../lib/crates/replica_db_schema/src/models/plot_gcs_location.rs"), 361 include_str!("../../../../lib/crates/replica_db_schema/src/models/plot_tag.rs"), 362 include_str!("../../../../lib/crates/replica_db_schema/src/models/trade_product.rs"), 363 include_str!( 364 "../../../../lib/crates/replica_db_schema/src/models/trade_product_location.rs" 365 ), 366 include_str!("../../../../lib/crates/replica_db_schema/src/models/trade_product_media.rs"), 367 ]; 368 const EVENTS_TYPE_INVENTORY: &[&str] = &[ 369 "JobFeedbackStatus", 370 "JobInputType", 371 "JobPaymentRequest", 372 "RadrootsAccountClaim", 373 "RadrootsActiveTradeEnvelope", 374 "RadrootsActiveTradeMessageType", 375 "RadrootsAppData", 376 "RadrootsComment", 377 "RadrootsCoop", 378 "RadrootsCoopLocation", 379 "RadrootsCoopRef", 380 "RadrootsDocument", 381 "RadrootsDocumentSubject", 382 "RadrootsFarm", 383 "RadrootsFarmLocation", 384 "RadrootsFarmRef", 385 "RadrootsFollow", 386 "RadrootsFollowProfile", 387 "RadrootsGcsLocation", 388 "RadrootsGeoChat", 389 "RadrootsGeoJsonPoint", 390 "RadrootsGeoJsonPolygon", 391 "RadrootsGiftWrap", 392 "RadrootsGiftWrapRecipient", 393 "RadrootsJobFeedback", 394 "RadrootsJobInput", 395 "RadrootsJobParam", 396 "RadrootsJobRequest", 397 "RadrootsJobResult", 398 "RadrootsList", 399 "RadrootsListEntry", 400 "RadrootsListSet", 401 "RadrootsListing", 402 "RadrootsListingAvailability", 403 "RadrootsListingBin", 404 "RadrootsListingDeliveryMethod", 405 "RadrootsListingImage", 406 "RadrootsListingImageSize", 407 "RadrootsListingLocation", 408 "RadrootsListingProduct", 409 "RadrootsListingProductTagKeys", 410 "RadrootsListingStatus", 411 "RadrootsMessage", 412 "RadrootsMessageFile", 413 "RadrootsMessageFileDimensions", 414 "RadrootsMessageRecipient", 415 "RadrootsNostrEvent", 416 "RadrootsNostrEventPtr", 417 "RadrootsNostrEventRef", 418 "RadrootsPlot", 419 "RadrootsPlotLocation", 420 "RadrootsPlotRef", 421 "RadrootsPost", 422 "RadrootsProfile", 423 "RadrootsProfileType", 424 "RadrootsReaction", 425 "RadrootsRelayDocument", 426 "RadrootsResourceArea", 427 "RadrootsResourceAreaLocation", 428 "RadrootsResourceAreaRef", 429 "RadrootsResourceHarvestCap", 430 "RadrootsResourceHarvestProduct", 431 "RadrootsSeal", 432 "RadrootsTradeAnswer", 433 "RadrootsTradeDiscountDecision", 434 "RadrootsTradeDiscountOffer", 435 "RadrootsTradeDiscountRequest", 436 "RadrootsTradeDomain", 437 "RadrootsTradeEconomicActor", 438 "RadrootsTradeEconomicEffect", 439 "RadrootsTradeEconomicLineKind", 440 "RadrootsTradeEnvelope", 441 "RadrootsTradeInventoryCommitment", 442 "RadrootsTradeListingCancel", 443 "RadrootsTradeListingParseError", 444 "RadrootsTradeListingValidateRequest", 445 "RadrootsTradeListingValidateResult", 446 "RadrootsTradeListingValidationError", 447 "RadrootsTradeMessagePayload", 448 "RadrootsTradeMessageType", 449 "RadrootsTradeOrderCancelled", 450 "RadrootsTradeOrderChange", 451 "RadrootsTradeOrderDecision", 452 "RadrootsTradeOrderDecisionEvent", 453 "RadrootsTradeOrderEconomicItem", 454 "RadrootsTradeOrderEconomicLine", 455 "RadrootsTradeOrderEconomicTotals", 456 "RadrootsTradeOrderEconomics", 457 "RadrootsTradeOrderItem", 458 "RadrootsTradeOrderRequested", 459 "RadrootsTradeOrderResponse", 460 "RadrootsTradeOrderRevision", 461 "RadrootsTradeOrderRevisionDecision", 462 "RadrootsTradeOrderRevisionDecisionEvent", 463 "RadrootsTradeOrderRevisionProposed", 464 "RadrootsTradeOrderRevisionResponse", 465 "RadrootsTradeOrderStatus", 466 "RadrootsTradePricingBasis", 467 "RadrootsTradeQuestion", 468 "RadrootsTradeTransportLane", 469 ]; 470 const EVENTS_INDEXED_TYPE_INVENTORY: &[&str] = &[ 471 "RadrootsEventsIndexedShardId", 472 "RadrootsEventsIndexedIdRange", 473 "RadrootsEventsIndexedShardMetadata", 474 "RadrootsEventsIndexedManifest", 475 "RadrootsEventsIndexedShardCheckpoint", 476 "RadrootsEventsIndexedIndexCheckpoint", 477 ]; 478 const TRADE_TYPE_INVENTORY: &[&str] = &[ 479 "RadrootsTradeFacetCount", 480 "RadrootsTradeListing", 481 "RadrootsTradeListingBackofficeOverlay", 482 "RadrootsTradeListingBackofficeQuery", 483 "RadrootsTradeListingBackofficeView", 484 "RadrootsTradeListingBinProjection", 485 "RadrootsTradeListingFacets", 486 "RadrootsTradeListingMarketStatus", 487 "RadrootsTradeListingProjection", 488 "RadrootsTradeListingQuery", 489 "RadrootsTradeListingSort", 490 "RadrootsTradeListingSortField", 491 "RadrootsTradeListingSubtotal", 492 "RadrootsTradeListingTotal", 493 "RadrootsTradeMarketplaceListingSummary", 494 "RadrootsTradeMarketplaceOrderSummary", 495 "RadrootsTradeModerationFlag", 496 "RadrootsTradeModerationSeverity", 497 "RadrootsTradeModerationStatus", 498 "RadrootsTradeOrderBackofficeOverlay", 499 "RadrootsTradeOrderBackofficeQuery", 500 "RadrootsTradeOrderBackofficeView", 501 "RadrootsTradeOrderFacets", 502 "RadrootsTradeOrderQuery", 503 "RadrootsTradeOrderSort", 504 "RadrootsTradeOrderSortField", 505 "RadrootsTradeOrderWorkflowMessage", 506 "RadrootsTradeOrderWorkflowProjection", 507 "RadrootsTradeReviewPriority", 508 "RadrootsTradeReviewQueueEntry", 509 "RadrootsTradeReviewStatus", 510 "RadrootsTradeSortDirection", 511 ]; 512 513 #[test] 514 fn approved_source_roots_build_registries() { 515 for root_set in DTO_PACKAGE_ROOTS { 516 let registry = root_set.registry(); 517 assert!( 518 !registry.has_errors(), 519 "registry for {} has diagnostics: {:?}", 520 root_set.package_key, 521 registry.diagnostics 522 ); 523 assert!(!registry.roots.is_empty()); 524 } 525 } 526 527 #[test] 528 fn package_roots_are_explicit_not_discovered() { 529 assert!(package_root_set("core").is_some()); 530 assert!(package_root_set("events").is_some()); 531 assert!(package_root_set("events_indexed").is_some()); 532 assert!(package_root_set("trade").is_some()); 533 assert!(package_root_set("types").is_some()); 534 } 535 536 #[test] 537 fn manual_descriptor_catalog_covers_known_review_families() { 538 assert!( 539 MANUAL_DESCRIPTOR_FAMILIES 540 .iter() 541 .any(|family| family.source_family.contains("GeoJSON")) 542 ); 543 assert!( 544 MANUAL_DESCRIPTOR_FAMILIES 545 .iter() 546 .any(|family| family.source_family.contains("generic result")) 547 ); 548 assert!( 549 SDK_LOCAL_WRAPPER_ALLOWANCES 550 .iter() 551 .any(|allowance| allowance.shape_family.contains("RadrootsCoreDecimal")) 552 ); 553 assert!( 554 SDK_LOCAL_WRAPPER_ALLOWANCES 555 .iter() 556 .any(|allowance| allowance.shape_family.contains("IResult")) 557 ); 558 assert!(SDK_LOCAL_WRAPPER_ALLOWANCES.iter().any(|allowance| { 559 allowance 560 .shape_family 561 .contains("RadrootsEventsIndexedShardId") 562 })); 563 assert!(SDK_LOCAL_WRAPPER_ALLOWANCES.iter().any(|allowance| { 564 allowance 565 .shape_family 566 .contains("marketplace, query, projection") 567 })); 568 } 569 570 #[test] 571 fn events_type_inventory_matches_current_package_surface() { 572 let actual = type_inventory(EVENTS_BINDINGS_TYPES_TS); 573 574 assert_eq!(actual, EVENTS_TYPE_INVENTORY); 575 } 576 577 #[test] 578 fn events_indexed_type_inventory_matches_current_package_surface() { 579 let actual = type_inventory(EVENTS_INDEXED_BINDINGS_TYPES_TS); 580 581 assert_eq!(actual, EVENTS_INDEXED_TYPE_INVENTORY); 582 } 583 584 #[test] 585 fn trade_type_inventory_matches_current_package_surface() { 586 let actual = type_inventory(TRADE_BINDINGS_TYPES_TS); 587 588 assert_eq!(actual, TRADE_TYPE_INVENTORY); 589 } 590 591 #[test] 592 fn replica_db_schema_generated_types_preserve_source_schema_contracts() { 593 let actual = type_inventory(REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS); 594 let trade_product_filter = type_declaration( 595 REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS, 596 "ITradeProductFieldsFilter", 597 ); 598 let trade_product_partial = type_declaration( 599 REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS, 600 "ITradeProductFieldsPartial", 601 ); 602 603 assert!(actual.contains(&"Farm")); 604 assert!(actual.contains(&"GcsLocation")); 605 assert!(actual.contains(&"NostrEventHead")); 606 assert!(actual.contains(&"ReplicaDbJsonValue")); 607 assert!(actual.contains(&"ITradeProductFieldsPartial")); 608 assert!(!actual.contains(&"NostrEventState")); 609 assert!(REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS.contains( 610 "export type ReplicaDbJsonValue = null | boolean | number | string | Array<ReplicaDbJsonValue> | { [key: string]: ReplicaDbJsonValue };" 611 )); 612 assert!( 613 REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS 614 .contains("export type IFarmFindOneResolve = IResult<Farm | null>;") 615 ); 616 assert!(trade_product_filter.contains("year?: bigint")); 617 assert!(trade_product_filter.contains("qty_avail?: bigint")); 618 assert!(trade_product_partial.contains("year?: ReplicaDbJsonValue | null")); 619 assert!(trade_product_partial.contains("qty_avail?: ReplicaDbJsonValue | null")); 620 } 621 622 #[test] 623 fn replica_db_schema_generated_types_match_source_public_inventory() { 624 let actual = type_inventory(REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS) 625 .into_iter() 626 .collect::<BTreeSet<_>>(); 627 let missing = source_public_schema_type_inventory() 628 .into_iter() 629 .filter(|name| !actual.contains(name)) 630 .collect::<Vec<_>>(); 631 632 assert!( 633 missing.is_empty(), 634 "missing generated replica schema exports: {missing:?}" 635 ); 636 } 637 638 #[test] 639 fn trade_package_imports_source_owned_support_types() { 640 assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/core-bindings\"")); 641 assert!(TRADE_BINDINGS_TYPES_TS.contains("from \"@radroots/events-bindings\"")); 642 643 for duplicate in [ 644 "export type RadrootsListing = ", 645 "export type RadrootsFarmRef = ", 646 "export type RadrootsTradeMessageType = ", 647 "export type RadrootsTradeOrderStatus = ", 648 ] { 649 assert!(!TRADE_BINDINGS_TYPES_TS.contains(duplicate)); 650 } 651 } 652 653 fn type_inventory(types_ts: &str) -> Vec<&str> { 654 types_ts 655 .lines() 656 .filter_map(|line| line.strip_prefix("export type ")) 657 .map(|rest| rest.split([' ', '<']).next().expect("type name")) 658 .collect() 659 } 660 661 fn source_public_schema_type_inventory() -> Vec<&'static str> { 662 let mut names = BTreeSet::new(); 663 664 for source in REPLICA_SCHEMA_MODEL_SOURCES { 665 for line in source.lines() { 666 if let Some(name) = public_rust_type_name(line) 667 && !name.ends_with("Ts") 668 { 669 names.insert(name); 670 } 671 } 672 } 673 674 names.into_iter().collect() 675 } 676 677 fn public_rust_type_name(line: &'static str) -> Option<&'static str> { 678 let line = line.trim_start(); 679 680 ["pub struct ", "pub enum ", "pub type "] 681 .into_iter() 682 .find_map(|prefix| { 683 line.strip_prefix(prefix).map(|rest| { 684 rest.split(|char: char| !(char == '_' || char.is_ascii_alphanumeric())) 685 .next() 686 .expect("type name") 687 }) 688 }) 689 } 690 691 fn type_declaration<'a>(types_ts: &'a str, name: &str) -> &'a str { 692 types_ts 693 .lines() 694 .find(|line| line.starts_with(&format!("export type {name} = "))) 695 .unwrap_or_else(|| panic!("missing type declaration for {name}")) 696 } 697 }