sdk

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

output.rs (11510B)


      1 use crate::{
      2     dto_render::DtoTypesModule,
      3     dto_roots,
      4     manifest::manifest_file_name,
      5     manifest::package_manifest,
      6     package_matrix::{PackageSpec, package_specs},
      7     ts::{generated_constants_file, generated_header, generated_kinds_file, generated_types_file},
      8 };
      9 
     10 pub struct PackageOutput {
     11     pub spec: PackageSpec,
     12     pub types_ts: Option<TsSource>,
     13     pub types_imports_ts: Option<&'static str>,
     14     pub constants_ts: Option<TsSource>,
     15     pub kinds_ts: Option<TsSource>,
     16 }
     17 
     18 pub struct GeneratedFile {
     19     pub relative_path: String,
     20     pub contents: String,
     21 }
     22 
     23 pub enum TsSource {
     24     DtoRegistry(DtoTypesModule),
     25     Raw(String),
     26 }
     27 
     28 impl TsSource {
     29     fn render(&self) -> String {
     30         match self {
     31             Self::DtoRegistry(module) => module.body_ts().to_owned(),
     32             Self::Raw(body) => body.clone(),
     33         }
     34     }
     35 
     36     fn imports(&self) -> Option<&str> {
     37         match self {
     38             Self::DtoRegistry(module) => module.imports_ts(),
     39             Self::Raw(_) => None,
     40         }
     41     }
     42 }
     43 
     44 impl PackageOutput {
     45     pub fn files(&self) -> Vec<GeneratedFile> {
     46         let mut files = Vec::new();
     47         if let Some(types_ts) = &self.types_ts {
     48             let imports = combined_imports(self.types_imports_ts, types_ts.imports());
     49             files.push(GeneratedFile {
     50                 relative_path: format!("src/generated/{}", generated_types_file()),
     51                 contents: render_ts(types_ts, imports.as_deref()),
     52             });
     53         }
     54         if let Some(constants_ts) = &self.constants_ts {
     55             files.push(GeneratedFile {
     56                 relative_path: format!("src/generated/{}", generated_constants_file()),
     57                 contents: render_ts(constants_ts, None),
     58             });
     59         }
     60         if let Some(kinds_ts) = &self.kinds_ts {
     61             files.push(GeneratedFile {
     62                 relative_path: format!("src/generated/{}", generated_kinds_file()),
     63                 contents: render_ts(kinds_ts, None),
     64             });
     65         }
     66         files.push(GeneratedFile {
     67             relative_path: format!("src/generated/{}", manifest_file_name()),
     68             contents: render_manifest(self.spec),
     69         });
     70         files.push(GeneratedFile {
     71             relative_path: "src/index.ts".to_owned(),
     72             contents: render_index(self),
     73         });
     74         files
     75     }
     76 }
     77 
     78 pub fn package_outputs() -> Result<Vec<PackageOutput>, String> {
     79     Ok(vec![
     80         PackageOutput {
     81             spec: spec_by_key("core"),
     82             types_ts: Some(TsSource::DtoRegistry(dto_roots::core_types_module()?)),
     83             types_imports_ts: None,
     84             constants_ts: None,
     85             kinds_ts: None,
     86         },
     87         PackageOutput {
     88             spec: spec_by_key("events"),
     89             types_ts: Some(TsSource::DtoRegistry(dto_roots::events_types_module()?)),
     90             types_imports_ts: None,
     91             constants_ts: Some(TsSource::Raw(radroots_events_bindings::constants_module())),
     92             kinds_ts: Some(TsSource::Raw(radroots_events_bindings::kinds_module())),
     93         },
     94         PackageOutput {
     95             spec: spec_by_key("events_indexed"),
     96             types_ts: Some(TsSource::DtoRegistry(
     97                 dto_roots::events_indexed_types_module()?,
     98             )),
     99             types_imports_ts: None,
    100             constants_ts: None,
    101             kinds_ts: None,
    102         },
    103         PackageOutput {
    104             spec: spec_by_key("identity"),
    105             types_ts: None,
    106             types_imports_ts: None,
    107             constants_ts: Some(TsSource::Raw(radroots_identity_bindings::constants_module())),
    108             kinds_ts: None,
    109         },
    110         PackageOutput {
    111             spec: spec_by_key("replica_db_schema"),
    112             types_ts: Some(TsSource::DtoRegistry(
    113                 dto_roots::replica_db_schema_types_module()?,
    114             )),
    115             types_imports_ts: Some(REPLICA_DB_SCHEMA_TYPES_IMPORTS_TS),
    116             constants_ts: None,
    117             kinds_ts: None,
    118         },
    119         PackageOutput {
    120             spec: spec_by_key("trade"),
    121             types_ts: Some(TsSource::DtoRegistry(dto_roots::trade_types_module()?)),
    122             types_imports_ts: None,
    123             constants_ts: None,
    124             kinds_ts: None,
    125         },
    126         PackageOutput {
    127             spec: spec_by_key("types"),
    128             types_ts: Some(TsSource::DtoRegistry(dto_roots::types_types_module()?)),
    129             types_imports_ts: None,
    130             constants_ts: None,
    131             kinds_ts: None,
    132         },
    133     ])
    134 }
    135 
    136 fn spec_by_key(key: &str) -> PackageSpec {
    137     package_specs()
    138         .iter()
    139         .copied()
    140         .find(|spec| spec.key == key)
    141         .unwrap_or_else(|| panic!("missing package spec for {key}"))
    142 }
    143 
    144 fn render_ts(source: &TsSource, imports: Option<&str>) -> String {
    145     let body = source.render();
    146     let imports = imports.unwrap_or("");
    147     let mut rendered = format!("{}{}{}", generated_header(), imports, body.trim_start());
    148     if !rendered.ends_with('\n') {
    149         rendered.push('\n');
    150     }
    151     rendered
    152 }
    153 
    154 fn combined_imports(first: Option<&str>, second: Option<&str>) -> Option<String> {
    155     match (first, second) {
    156         (Some(first), Some(second)) => Some(format!("{first}{second}")),
    157         (Some(first), None) => Some(first.to_owned()),
    158         (None, Some(second)) => Some(second.to_owned()),
    159         (None, None) => None,
    160     }
    161 }
    162 
    163 const REPLICA_DB_SCHEMA_TYPES_IMPORTS_TS: &str = r#"import type {
    164     IResult,
    165     IResultList,
    166     IResultPass,
    167 } from "@radroots/types-bindings";
    168 
    169 "#;
    170 
    171 fn render_manifest(spec: PackageSpec) -> String {
    172     let mut value = package_manifest(spec);
    173     value["generated"] = serde_json::Value::Bool(true);
    174     format!(
    175         "{}\n",
    176         serde_json::to_string_pretty(&value).expect("manifest json serializes")
    177     )
    178 }
    179 
    180 fn render_index(output: &PackageOutput) -> String {
    181     let mut lines = Vec::new();
    182     if output.types_ts.is_some() {
    183         lines.push("export * from \"./generated/types.js\";");
    184     }
    185     if output.constants_ts.is_some() {
    186         lines.push("export * from \"./generated/constants.js\";");
    187     }
    188     if output.kinds_ts.is_some() {
    189         lines.push("export * from \"./generated/kinds.js\";");
    190     }
    191     if lines.is_empty() {
    192         lines.push("export {};");
    193     }
    194     format!("{}\n", lines.join("\n"))
    195 }
    196 
    197 #[cfg(test)]
    198 mod tests {
    199     use super::{PackageOutput, TsSource, package_outputs, render_ts};
    200     use crate::{dto_render::DtoTypesModule, package_matrix::package_specs};
    201 
    202     const TRADE_BINDINGS_TYPES_TS: &str =
    203         include_str!("../../../packages/trade-bindings/src/generated/types.ts");
    204     const REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS: &str =
    205         include_str!("../../../packages/replica-db-schema-bindings/src/generated/types.ts");
    206 
    207     #[test]
    208     fn renders_sdk_header() {
    209         let output = render_ts(&TsSource::Raw("export type A = string;".to_owned()), None);
    210         assert!(output.starts_with("// @generated by cargo xtask generate ts"));
    211         assert!(output.contains("export type A = string;"));
    212     }
    213 
    214     #[test]
    215     fn renders_import_prelude_after_header() {
    216         let output = render_ts(
    217             &TsSource::Raw("export type A = string;".to_owned()),
    218             Some("import type { B } from \"b\";\n\n"),
    219         );
    220         assert!(output.starts_with(
    221             "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nimport type"
    222         ));
    223         assert!(output.contains("export type A = string;"));
    224     }
    225 
    226     #[test]
    227     fn renders_raw_sources() {
    228         let output = render_ts(&TsSource::Raw("export type A = string;".to_owned()), None);
    229         assert_eq!(
    230             output,
    231             "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nexport type A = string;\n"
    232         );
    233     }
    234 
    235     #[test]
    236     fn includes_core_and_types_outputs() {
    237         let package_names = package_outputs()
    238             .expect("package outputs")
    239             .into_iter()
    240             .map(|output| output.spec.package_name)
    241             .collect::<Vec<_>>();
    242         assert!(package_names.contains(&"@radroots/core-bindings"));
    243         assert!(package_names.contains(&"@radroots/events-bindings"));
    244         assert!(package_names.contains(&"@radroots/events-indexed-bindings"));
    245         assert!(package_names.contains(&"@radroots/identity-bindings"));
    246         assert!(package_names.contains(&"@radroots/replica-db-schema-bindings"));
    247         assert!(package_names.contains(&"@radroots/trade-bindings"));
    248         assert!(package_names.contains(&"@radroots/types-bindings"));
    249     }
    250 
    251     #[test]
    252     fn dto_registry_source_uses_package_shell() {
    253         let output = PackageOutput {
    254             spec: package_specs()[0],
    255             types_ts: Some(TsSource::DtoRegistry(DtoTypesModule::new(
    256                 "import type { ExternalThing } from \"@radroots/external-bindings\";\n\n",
    257                 "export type SyntheticThing = { external: ExternalThing, };",
    258             ))),
    259             types_imports_ts: Some("import type { LocalPrelude } from \"@radroots/local\";\n\n"),
    260             constants_ts: None,
    261             kinds_ts: None,
    262         };
    263         let files = output.files();
    264         let types = files
    265             .iter()
    266             .find(|file| file.relative_path == "src/generated/types.ts")
    267             .expect("types file");
    268         let manifest = files
    269             .iter()
    270             .find(|file| file.relative_path == "src/generated/sdk-manifest.json")
    271             .expect("manifest file");
    272         let index = files
    273             .iter()
    274             .find(|file| file.relative_path == "src/index.ts")
    275             .expect("index file");
    276 
    277         assert_eq!(
    278             types.contents,
    279             "// @generated by cargo xtask generate ts\n// Do not edit by hand.\nimport type { LocalPrelude } from \"@radroots/local\";\n\nimport type { ExternalThing } from \"@radroots/external-bindings\";\n\nexport type SyntheticThing = { external: ExternalThing, };\n"
    280         );
    281         assert!(manifest.contents.contains("\"generated\": true"));
    282         assert_eq!(index.contents, "export * from \"./generated/types.js\";\n");
    283     }
    284 
    285     #[test]
    286     fn trade_output_uses_dto_registry_and_matches_checked_in_types() {
    287         let output = package_outputs()
    288             .expect("package outputs")
    289             .into_iter()
    290             .find(|output| output.spec.key == "trade")
    291             .expect("trade output");
    292 
    293         assert!(matches!(output.types_ts, Some(TsSource::DtoRegistry(_))));
    294         assert!(output.types_imports_ts.is_none());
    295 
    296         let types = output
    297             .files()
    298             .into_iter()
    299             .find(|file| file.relative_path == "src/generated/types.ts")
    300             .expect("types file");
    301 
    302         assert_eq!(types.contents, TRADE_BINDINGS_TYPES_TS);
    303     }
    304 
    305     #[test]
    306     fn replica_db_schema_output_uses_dto_registry_and_matches_checked_in_types() {
    307         let output = package_outputs()
    308             .expect("package outputs")
    309             .into_iter()
    310             .find(|output| output.spec.key == "replica_db_schema")
    311             .expect("replica_db_schema output");
    312 
    313         assert!(matches!(output.types_ts, Some(TsSource::DtoRegistry(_))));
    314         assert_eq!(
    315             output.types_imports_ts,
    316             Some(super::REPLICA_DB_SCHEMA_TYPES_IMPORTS_TS)
    317         );
    318 
    319         let types = output
    320             .files()
    321             .into_iter()
    322             .find(|file| file.relative_path == "src/generated/types.ts")
    323             .expect("types file");
    324 
    325         assert_eq!(types.contents, REPLICA_DB_SCHEMA_BINDINGS_TYPES_TS);
    326     }
    327 }