sdk

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

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         &registry,
    149         &core_import_options(&registry, 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         &registry,
    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 }