@typokit/transform-native 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/lib.rs ADDED
@@ -0,0 +1,388 @@
1
+ mod parser;
2
+ mod type_extractor;
3
+ mod route_compiler;
4
+ mod openapi_generator;
5
+ mod schema_differ;
6
+ mod test_stub_generator;
7
+ mod typia_bridge;
8
+ mod output_pipeline;
9
+
10
+ use std::collections::HashMap;
11
+ use napi::bindgen_prelude::*;
12
+ use napi_derive::napi;
13
+ use serde::{Deserialize, Serialize};
14
+
15
+ /// Property metadata matching @typokit/types PropertyMetadata shape
16
+ #[napi(object)]
17
+ #[derive(Debug, Clone, Serialize, Deserialize)]
18
+ pub struct JsPropertyMetadata {
19
+ #[napi(js_name = "type")]
20
+ pub type_str: String,
21
+ pub optional: bool,
22
+ }
23
+
24
+ /// Type metadata matching @typokit/types TypeMetadata shape
25
+ #[napi(object)]
26
+ #[derive(Debug, Clone, Serialize, Deserialize)]
27
+ pub struct JsTypeMetadata {
28
+ pub name: String,
29
+ pub properties: HashMap<String, JsPropertyMetadata>,
30
+ }
31
+
32
+ /// Parse TypeScript source files and extract type metadata.
33
+ ///
34
+ /// Returns a SchemaTypeMap (Record<string, TypeMetadata>) mapping type names
35
+ /// to their extracted metadata including property types, optionality, and JSDoc tags.
36
+ #[napi]
37
+ pub fn parse_and_extract_types(file_paths: Vec<String>) -> Result<HashMap<String, JsTypeMetadata>> {
38
+ let internal_result = parser::parse_and_extract_types(&file_paths)
39
+ .map_err(|e| Error::from_reason(e))?;
40
+
41
+ let mut result: HashMap<String, JsTypeMetadata> = HashMap::new();
42
+
43
+ for (name, metadata) in internal_result {
44
+ let mut properties: HashMap<String, JsPropertyMetadata> = HashMap::new();
45
+ for (prop_name, prop) in metadata.properties {
46
+ properties.insert(
47
+ prop_name,
48
+ JsPropertyMetadata {
49
+ type_str: prop.type_str,
50
+ optional: prop.optional,
51
+ },
52
+ );
53
+ }
54
+ result.insert(
55
+ name.clone(),
56
+ JsTypeMetadata {
57
+ name: metadata.name,
58
+ properties,
59
+ },
60
+ );
61
+ }
62
+
63
+ Ok(result)
64
+ }
65
+
66
+ /// Compile route contracts from TypeScript files into a radix tree.
67
+ /// Returns TypeScript source code for the compiled route table.
68
+ #[napi]
69
+ pub fn compile_routes(file_paths: Vec<String>) -> Result<String> {
70
+ let mut all_entries = Vec::new();
71
+
72
+ for path in &file_paths {
73
+ let source = std::fs::read_to_string(path)
74
+ .map_err(|e| Error::from_reason(format!("Failed to read file {}: {}", path, e)))?;
75
+ let parsed = parser::parse_typescript(path, &source)
76
+ .map_err(|e| Error::from_reason(e))?;
77
+ let entries = route_compiler::extract_route_contracts(&parsed.module);
78
+ all_entries.extend(entries);
79
+ }
80
+
81
+ let tree = route_compiler::build_radix_tree(&all_entries)
82
+ .map_err(|e| Error::from_reason(e))?;
83
+
84
+ Ok(route_compiler::serialize_to_typescript(&tree))
85
+ }
86
+
87
+ /// Generate an OpenAPI 3.1.0 specification from route contracts and type definitions.
88
+ /// Returns the OpenAPI spec as a JSON string.
89
+ #[napi]
90
+ pub fn generate_open_api(
91
+ route_file_paths: Vec<String>,
92
+ type_file_paths: Vec<String>,
93
+ ) -> Result<String> {
94
+ let mut all_entries = Vec::new();
95
+
96
+ for path in &route_file_paths {
97
+ let source = std::fs::read_to_string(path)
98
+ .map_err(|e| Error::from_reason(format!("Failed to read file {}: {}", path, e)))?;
99
+ let parsed = parser::parse_typescript(path, &source)
100
+ .map_err(|e| Error::from_reason(e))?;
101
+ let entries = route_compiler::extract_route_contracts(&parsed.module);
102
+ all_entries.extend(entries);
103
+ }
104
+
105
+ let internal_type_map = parser::parse_and_extract_types(&type_file_paths)
106
+ .map_err(|e| Error::from_reason(e))?;
107
+
108
+ Ok(openapi_generator::generate_openapi(&all_entries, &internal_type_map))
109
+ }
110
+
111
+ // ─── Schema Change (napi object) ─────────────────────────────
112
+
113
+ /// A single schema change (matches @typokit/types SchemaChange)
114
+ #[napi(object)]
115
+ #[derive(Debug, Clone, Serialize, Deserialize)]
116
+ pub struct JsSchemaChange {
117
+ #[napi(js_name = "type")]
118
+ pub change_type: String,
119
+ pub entity: String,
120
+ pub field: Option<String>,
121
+ pub details: Option<HashMap<String, String>>,
122
+ }
123
+
124
+ /// A migration draft (matches @typokit/types MigrationDraft)
125
+ #[napi(object)]
126
+ #[derive(Debug, Clone, Serialize, Deserialize)]
127
+ pub struct JsMigrationDraft {
128
+ pub name: String,
129
+ pub sql: String,
130
+ pub destructive: bool,
131
+ pub changes: Vec<JsSchemaChange>,
132
+ }
133
+
134
+ /// Diff two schema versions and produce a migration draft.
135
+ ///
136
+ /// Compares old_types against new_types to detect added/removed/modified
137
+ /// entities and fields. Generates SQL DDL stubs for the changes.
138
+ #[napi]
139
+ pub fn diff_schemas(
140
+ old_types: HashMap<String, JsTypeMetadata>,
141
+ new_types: HashMap<String, JsTypeMetadata>,
142
+ migration_name: String,
143
+ ) -> JsMigrationDraft {
144
+ let old_internal = js_types_to_internal(&old_types);
145
+ let new_internal = js_types_to_internal(&new_types);
146
+
147
+ let draft = schema_differ::diff_schemas(&old_internal, &new_internal, &migration_name);
148
+
149
+ JsMigrationDraft {
150
+ name: draft.name,
151
+ sql: draft.sql,
152
+ destructive: draft.destructive,
153
+ changes: draft
154
+ .changes
155
+ .into_iter()
156
+ .map(|c| JsSchemaChange {
157
+ change_type: c.change_type,
158
+ entity: c.entity,
159
+ field: c.field,
160
+ details: c.details,
161
+ })
162
+ .collect(),
163
+ }
164
+ }
165
+
166
+ /// Generate contract test scaffolding from route contract files.
167
+ ///
168
+ /// Parses route contracts from the given files and generates TypeScript
169
+ /// test stubs with describe/it blocks for each route.
170
+ #[napi]
171
+ pub fn generate_test_stubs(file_paths: Vec<String>) -> Result<String> {
172
+ let mut all_entries = Vec::new();
173
+
174
+ for path in &file_paths {
175
+ let source = std::fs::read_to_string(path)
176
+ .map_err(|e| Error::from_reason(format!("Failed to read file {}: {}", path, e)))?;
177
+ let parsed = parser::parse_typescript(path, &source)
178
+ .map_err(|e| Error::from_reason(e))?;
179
+ let entries = route_compiler::extract_route_contracts(&parsed.module);
180
+ all_entries.extend(entries);
181
+ }
182
+
183
+ Ok(test_stub_generator::generate_test_stubs(&all_entries))
184
+ }
185
+
186
+ /// Validator input for a single type (passed to Typia bridge callback)
187
+ #[napi(object)]
188
+ #[derive(Debug, Clone)]
189
+ pub struct JsTypeValidatorInput {
190
+ pub name: String,
191
+ pub properties: HashMap<String, JsPropertyMetadata>,
192
+ }
193
+
194
+ /// Prepare type metadata for Typia validator generation.
195
+ ///
196
+ /// Converts parsed type metadata into a format suitable for passing
197
+ /// to the @typokit/transform-typia bridge callback.
198
+ #[napi]
199
+ pub fn prepare_validator_inputs(
200
+ type_file_paths: Vec<String>,
201
+ ) -> Result<Vec<JsTypeValidatorInput>> {
202
+ let internal_types = parser::parse_and_extract_types(&type_file_paths)
203
+ .map_err(|e| Error::from_reason(e))?;
204
+
205
+ let inputs = typia_bridge::prepare_validator_inputs(&internal_types);
206
+
207
+ Ok(inputs
208
+ .into_iter()
209
+ .map(|input| {
210
+ let mut properties = HashMap::new();
211
+ for (name, prop) in input.properties {
212
+ properties.insert(
213
+ name,
214
+ JsPropertyMetadata {
215
+ type_str: prop.type_str,
216
+ optional: prop.optional,
217
+ },
218
+ );
219
+ }
220
+ JsTypeValidatorInput {
221
+ name: input.name,
222
+ properties,
223
+ }
224
+ })
225
+ .collect())
226
+ }
227
+
228
+ /// Collect validator code results into a file path map.
229
+ ///
230
+ /// Maps type names to their output file paths under .typokit/validators/.
231
+ #[napi]
232
+ pub fn collect_validator_outputs(
233
+ results: Vec<Vec<String>>,
234
+ ) -> HashMap<String, String> {
235
+ let pairs: Vec<(String, String)> = results
236
+ .into_iter()
237
+ .filter_map(|pair| {
238
+ if pair.len() == 2 {
239
+ Some((pair[0].clone(), pair[1].clone()))
240
+ } else {
241
+ None
242
+ }
243
+ })
244
+ .collect();
245
+
246
+ typia_bridge::collect_validator_outputs(&pairs)
247
+ }
248
+
249
+ /// Result of running the full output pipeline
250
+ #[napi(object)]
251
+ #[derive(Debug, Clone)]
252
+ pub struct JsPipelineResult {
253
+ /// SHA-256 content hash of all input source files
254
+ pub content_hash: String,
255
+ /// Extracted type metadata (SchemaTypeMap-compatible)
256
+ pub types: HashMap<String, JsTypeMetadata>,
257
+ /// Compiled route table as TypeScript source
258
+ pub compiled_routes: String,
259
+ /// OpenAPI 3.1.0 spec as JSON string
260
+ pub openapi_spec: String,
261
+ /// Generated contract test stubs as TypeScript source
262
+ pub test_stubs: String,
263
+ /// Validator inputs ready for Typia bridge callback
264
+ pub validator_inputs: Vec<JsTypeValidatorInput>,
265
+ }
266
+
267
+ /// Compute a SHA-256 content hash of the given file paths and their contents.
268
+ ///
269
+ /// Used for cache invalidation: if the hash matches a previous build, outputs
270
+ /// can be reused without regeneration.
271
+ #[napi]
272
+ pub fn compute_content_hash(file_paths: Vec<String>) -> Result<String> {
273
+ output_pipeline::compute_content_hash(&file_paths)
274
+ .map_err(|e| Error::from_reason(e))
275
+ }
276
+
277
+ /// Run the full output pipeline: parse types, compile routes, generate OpenAPI,
278
+ /// generate test stubs, and prepare validator inputs.
279
+ ///
280
+ /// Returns all generated outputs plus a content hash for caching.
281
+ /// Validators are returned as inputs — the caller should pass them to
282
+ /// the Typia bridge callback and then call collectValidatorOutputs.
283
+ #[napi]
284
+ pub fn run_pipeline(
285
+ type_file_paths: Vec<String>,
286
+ route_file_paths: Vec<String>,
287
+ ) -> Result<JsPipelineResult> {
288
+ let result = output_pipeline::run_pipeline(&type_file_paths, &route_file_paths)
289
+ .map_err(|e| Error::from_reason(e))?;
290
+
291
+ // Convert internal types to JS types
292
+ let mut types: HashMap<String, JsTypeMetadata> = HashMap::new();
293
+ for (name, meta) in result.types {
294
+ let mut properties: HashMap<String, JsPropertyMetadata> = HashMap::new();
295
+ for (prop_name, prop) in meta.properties {
296
+ properties.insert(
297
+ prop_name,
298
+ JsPropertyMetadata {
299
+ type_str: prop.type_str,
300
+ optional: prop.optional,
301
+ },
302
+ );
303
+ }
304
+ types.insert(
305
+ name.clone(),
306
+ JsTypeMetadata {
307
+ name: meta.name,
308
+ properties,
309
+ },
310
+ );
311
+ }
312
+
313
+ let validator_inputs: Vec<JsTypeValidatorInput> = result
314
+ .validator_inputs
315
+ .into_iter()
316
+ .map(|input| {
317
+ let mut properties = HashMap::new();
318
+ for (name, prop) in input.properties {
319
+ properties.insert(
320
+ name,
321
+ JsPropertyMetadata {
322
+ type_str: prop.type_str,
323
+ optional: prop.optional,
324
+ },
325
+ );
326
+ }
327
+ JsTypeValidatorInput {
328
+ name: input.name,
329
+ properties,
330
+ }
331
+ })
332
+ .collect();
333
+
334
+ Ok(JsPipelineResult {
335
+ content_hash: result.content_hash,
336
+ types,
337
+ compiled_routes: result.compiled_routes,
338
+ openapi_spec: result.openapi_spec,
339
+ test_stubs: result.test_stubs,
340
+ validator_inputs,
341
+ })
342
+ }
343
+
344
+ /// Helper to convert JsTypeMetadata to internal TypeMetadata
345
+ fn js_types_to_internal(
346
+ js_types: &HashMap<String, JsTypeMetadata>,
347
+ ) -> HashMap<String, type_extractor::TypeMetadata> {
348
+ let mut result = HashMap::new();
349
+ for (name, meta) in js_types {
350
+ let mut properties = HashMap::new();
351
+ for (prop_name, prop) in &meta.properties {
352
+ properties.insert(
353
+ prop_name.clone(),
354
+ type_extractor::PropertyMetadata {
355
+ type_str: prop.type_str.clone(),
356
+ optional: prop.optional,
357
+ jsdoc: None,
358
+ },
359
+ );
360
+ }
361
+ result.insert(
362
+ name.clone(),
363
+ type_extractor::TypeMetadata {
364
+ name: meta.name.clone(),
365
+ properties,
366
+ jsdoc: None,
367
+ },
368
+ );
369
+ }
370
+ result
371
+ }
372
+
373
+ #[cfg(test)]
374
+ mod tests {
375
+ use super::*;
376
+
377
+ #[test]
378
+ fn test_parse_and_extract_types_nonexistent_file() {
379
+ let result = parse_and_extract_types(vec!["nonexistent.ts".to_string()]);
380
+ assert!(result.is_err());
381
+ }
382
+
383
+ #[test]
384
+ fn test_compile_routes_nonexistent_file() {
385
+ let result = compile_routes(vec!["nonexistent.ts".to_string()]);
386
+ assert!(result.is_err());
387
+ }
388
+ }