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 }