1use forge_core::schema::{EnumDef, FieldDef, FunctionDef, RustType, SchemaRegistry, TableDef};
8use serde_json::{Value, json};
9
10const WIRE_VERSION: &str = "2";
12
13const FORGE_VERSION: &str = env!("CARGO_PKG_VERSION");
15
16pub fn emit(registry: &SchemaRegistry) -> Value {
17 json!({
18 "$schema": "https://forge-rs.dev/schema/v2.json",
19 "version": FORGE_VERSION,
20 "wire_version": WIRE_VERSION,
21 "types": emit_types(registry),
22 "functions": emit_functions(registry),
23 })
24}
25
26pub fn emit_string(registry: &SchemaRegistry) -> Result<String, serde_json::Error> {
28 let value = emit(registry);
29 let mut output = serde_json::to_string_pretty(&value)?;
30 output.push('\n');
31 Ok(output)
32}
33
34fn emit_types(registry: &SchemaRegistry) -> Value {
35 let mut types = serde_json::Map::new();
36
37 let mut tables = registry.all_tables();
38 tables.sort_by(|a, b| a.struct_name.cmp(&b.struct_name));
39 for table in tables {
40 types.insert(table.struct_name.clone(), emit_table(&table));
41 }
42
43 let mut enums = registry.all_enums();
44 enums.sort_by(|a, b| a.name.cmp(&b.name));
45 for enum_def in enums {
46 types.insert(enum_def.name.clone(), emit_enum(&enum_def));
47 }
48
49 Value::Object(types)
50}
51
52fn emit_table(table: &TableDef) -> Value {
53 let fields: Vec<Value> = table.fields.iter().map(emit_field).collect();
54
55 let mut map = serde_json::Map::new();
56 map.insert(
57 "kind".into(),
58 json!(if table.is_dto { "dto" } else { "model" }),
59 );
60 map.insert("fields".into(), json!(fields));
61 if let Some(doc) = &table.doc {
62 map.insert("doc".into(), json!(doc));
63 }
64
65 Value::Object(map)
66}
67
68fn emit_field(field: &FieldDef) -> Value {
69 let mut map = serde_json::Map::new();
70 map.insert("name".into(), json!(field.name));
71 map.insert("type".into(), emit_rust_type(&field.rust_type));
72 map.insert("nullable".into(), json!(field.nullable));
73 if let Some(doc) = &field.doc {
74 map.insert("doc".into(), json!(doc));
75 }
76
77 Value::Object(map)
78}
79
80fn emit_enum(enum_def: &EnumDef) -> Value {
81 let variants: Vec<Value> = enum_def
82 .variants
83 .iter()
84 .map(|v| {
85 let mut map = serde_json::Map::new();
86 map.insert("name".into(), json!(v.name));
87 map.insert("value".into(), json!(v.sql_value));
88 if let Some(int_val) = v.int_value {
89 map.insert("int_value".into(), json!(int_val));
90 }
91 if let Some(doc) = &v.doc {
92 map.insert("doc".into(), json!(doc));
93 }
94 Value::Object(map)
95 })
96 .collect();
97
98 let mut map = serde_json::Map::new();
99 map.insert("kind".into(), json!("enum"));
100 map.insert("variants".into(), json!(variants));
101 if let Some(doc) = &enum_def.doc {
102 map.insert("doc".into(), json!(doc));
103 }
104
105 Value::Object(map)
106}
107
108fn emit_functions(registry: &SchemaRegistry) -> Value {
109 let mut functions = serde_json::Map::new();
110
111 let mut all_fns = registry.all_functions();
112 all_fns.sort_by(|a, b| a.name.cmp(&b.name));
113
114 for func in all_fns {
115 functions.insert(func.name.clone(), emit_function(&func));
116 }
117
118 Value::Object(functions)
119}
120
121fn emit_function(func: &FunctionDef) -> Value {
122 let args: Vec<Value> = func
123 .args
124 .iter()
125 .map(|arg| {
126 let mut map = serde_json::Map::new();
127 map.insert("name".into(), json!(arg.name));
128 map.insert("type".into(), emit_rust_type(&arg.rust_type));
129 if let Some(doc) = &arg.doc {
130 map.insert("doc".into(), json!(doc));
131 }
132 Value::Object(map)
133 })
134 .collect();
135
136 let mut map = serde_json::Map::new();
137 map.insert("kind".into(), json!(func.kind.as_str()));
138 map.insert("args".into(), json!(args));
139 map.insert("returns".into(), emit_rust_type(&func.return_type));
140 if let Some(doc) = &func.doc {
141 map.insert("doc".into(), json!(doc));
142 }
143
144 Value::Object(map)
145}
146
147fn emit_rust_type(rust_type: &RustType) -> Value {
148 match rust_type {
149 RustType::String => json!("string"),
150 RustType::I32 => json!({"base": "number", "format": "i32"}),
151 RustType::I64 => json!({"base": "number", "format": "i64"}),
152 RustType::F32 => json!({"base": "number", "format": "f32"}),
153 RustType::F64 => json!({"base": "number", "format": "f64"}),
154 RustType::Bool => json!("boolean"),
155 RustType::Uuid => json!({"base": "string", "format": "uuid"}),
156 RustType::Instant => json!({"base": "string", "format": "datetime"}),
157 RustType::LocalDate => json!({"base": "string", "format": "date"}),
158 RustType::LocalTime => json!({"base": "string", "format": "time"}),
159 RustType::Upload => json!("upload"),
160 RustType::Json => json!("any"),
161 RustType::Bytes => json!("bytes"),
162 RustType::Option(inner) => json!({"nullable": emit_rust_type(inner)}),
163 RustType::Vec(inner) => json!({"array": emit_rust_type(inner)}),
164 RustType::Custom(name) => json!({"$ref": name}),
165 }
166}
167
168#[cfg(test)]
169#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
170mod tests {
171 use super::*;
172 use forge_core::schema::{
173 EnumDef, EnumVariant, FieldDef, FunctionArg, FunctionDef, SchemaRegistry, TableDef,
174 };
175
176 #[test]
177 fn empty_registry_produces_valid_schema() {
178 let registry = SchemaRegistry::new();
179 let schema = emit(®istry);
180
181 assert_eq!(schema["wire_version"], "2");
182 assert!(schema["version"].is_string());
183 assert!(schema["types"].is_object());
184 assert!(schema["functions"].is_object());
185 }
186
187 #[test]
188 fn models_and_enums_appear_in_types() {
189 let registry = SchemaRegistry::new();
190
191 let mut table = TableDef::new("users", "User");
192 table.fields.push(FieldDef::new("id", RustType::Uuid));
193 table.fields.push(FieldDef::new("email", RustType::String));
194 registry.register_table(table);
195
196 let mut enum_def = EnumDef::new("Role");
197 enum_def.variants.push(EnumVariant::new("Admin"));
198 enum_def.variants.push(EnumVariant::new("Member"));
199 registry.register_enum(enum_def);
200
201 let schema = emit(®istry);
202
203 assert_eq!(schema["types"]["User"]["kind"], "model");
204 assert_eq!(schema["types"]["User"]["fields"][0]["name"], "id");
205 assert_eq!(schema["types"]["Role"]["kind"], "enum");
206 assert_eq!(schema["types"]["Role"]["variants"][0]["name"], "Admin");
207 }
208
209 #[test]
210 fn functions_appear_with_kind_and_args() {
211 let registry = SchemaRegistry::new();
212
213 let mut func = FunctionDef::query("get_user", RustType::Custom("User".into()));
214 func.args.push(FunctionArg::new("id", RustType::Uuid));
215 func.doc = Some("Fetch a user by ID".into());
216 registry.register_function(func);
217
218 let schema = emit(®istry);
219
220 let get_user = &schema["functions"]["get_user"];
221 assert_eq!(get_user["kind"], "query");
222 assert_eq!(get_user["args"][0]["name"], "id");
223 assert_eq!(get_user["doc"], "Fetch a user by ID");
224 }
225
226 #[test]
227 fn type_mapping_round_trips() {
228 assert_eq!(emit_rust_type(&RustType::String), json!("string"));
229 assert_eq!(emit_rust_type(&RustType::Bool), json!("boolean"));
230 assert_eq!(
231 emit_rust_type(&RustType::I32),
232 json!({"base": "number", "format": "i32"})
233 );
234 assert_eq!(
235 emit_rust_type(&RustType::Uuid),
236 json!({"base": "string", "format": "uuid"})
237 );
238 assert_eq!(
239 emit_rust_type(&RustType::Option(Box::new(RustType::String))),
240 json!({"nullable": "string"})
241 );
242 assert_eq!(
243 emit_rust_type(&RustType::Vec(Box::new(RustType::I32))),
244 json!({"array": {"base": "number", "format": "i32"}})
245 );
246 assert_eq!(
247 emit_rust_type(&RustType::Custom("User".into())),
248 json!({"$ref": "User"})
249 );
250 }
251
252 #[test]
253 fn dto_marked_correctly() {
254 let registry = SchemaRegistry::new();
255
256 let mut dto = TableDef::new("CreateUserArgs", "CreateUserArgs");
257 dto.is_dto = true;
258 dto.fields.push(FieldDef::new("name", RustType::String));
259 registry.register_table(dto);
260
261 let schema = emit(®istry);
262 assert_eq!(schema["types"]["CreateUserArgs"]["kind"], "dto");
263 }
264
265 #[test]
266 fn emit_string_has_trailing_newline() {
267 let registry = SchemaRegistry::new();
268 let output = emit_string(®istry).unwrap();
269 assert!(output.ends_with('\n'));
270 assert!(output.contains("wire_version"));
271 }
272
273 #[test]
274 fn deterministic_output() {
275 let registry = SchemaRegistry::new();
276
277 registry.register_function(FunctionDef::query("z_last", RustType::String));
278 registry.register_function(FunctionDef::query("a_first", RustType::String));
279 registry.register_function(FunctionDef::mutation("m_middle", RustType::I32));
280
281 let mut table_z = TableDef::new("zebras", "Zebra");
282 table_z.fields.push(FieldDef::new("id", RustType::Uuid));
283 registry.register_table(table_z);
284
285 let mut table_a = TableDef::new("apples", "Apple");
286 table_a.fields.push(FieldDef::new("id", RustType::Uuid));
287 registry.register_table(table_a);
288
289 let first = emit_string(®istry).unwrap();
290 let second = emit_string(®istry).unwrap();
291 assert_eq!(first, second, "Schema output must be deterministic");
292 }
293
294 #[test]
295 fn emit_rust_type_covers_every_numeric_and_temporal_variant() {
296 assert_eq!(
298 emit_rust_type(&RustType::I64),
299 json!({"base": "number", "format": "i64"})
300 );
301 assert_eq!(
302 emit_rust_type(&RustType::F32),
303 json!({"base": "number", "format": "f32"})
304 );
305 assert_eq!(
306 emit_rust_type(&RustType::F64),
307 json!({"base": "number", "format": "f64"})
308 );
309 assert_eq!(
310 emit_rust_type(&RustType::Instant),
311 json!({"base": "string", "format": "datetime"})
312 );
313 assert_eq!(
314 emit_rust_type(&RustType::LocalDate),
315 json!({"base": "string", "format": "date"})
316 );
317 assert_eq!(
318 emit_rust_type(&RustType::LocalTime),
319 json!({"base": "string", "format": "time"})
320 );
321 assert_eq!(emit_rust_type(&RustType::Upload), json!("upload"));
322 assert_eq!(emit_rust_type(&RustType::Json), json!("any"));
323 assert_eq!(emit_rust_type(&RustType::Bytes), json!("bytes"));
324 }
325
326 #[test]
327 fn emit_rust_type_nests_option_and_vec_recursively() {
328 let ty = RustType::Option(Box::new(RustType::Vec(Box::new(RustType::Custom(
329 "Role".into(),
330 )))));
331 assert_eq!(
332 emit_rust_type(&ty),
333 json!({"nullable": {"array": {"$ref": "Role"}}})
334 );
335 }
336
337 #[test]
338 fn field_emits_nullable_flag_for_option_types() {
339 let field = FieldDef::new("nickname", RustType::Option(Box::new(RustType::String)));
341 let json = emit_field(&field);
342 assert_eq!(json["name"], "nickname");
343 assert_eq!(json["nullable"], true);
344 assert_eq!(json["type"], json!({"nullable": "string"}));
345 }
346
347 #[test]
348 fn field_emits_doc_only_when_present() {
349 let mut documented = FieldDef::new("email", RustType::String);
350 documented.doc = Some("Primary contact address".into());
351 let with_doc = emit_field(&documented);
352 assert_eq!(with_doc["doc"], "Primary contact address");
353
354 let plain = emit_field(&FieldDef::new("id", RustType::Uuid));
355 assert!(plain.as_object().unwrap().get("doc").is_none());
356 }
357
358 #[test]
359 fn enum_emits_int_value_and_variant_doc_when_set() {
360 let mut enum_def = EnumDef::new("Status");
361 let mut active = EnumVariant::new("Active");
362 active.int_value = Some(1);
363 active.doc = Some("Currently in use".into());
364 enum_def.variants.push(active);
365 enum_def.variants.push(EnumVariant::new("Archived"));
366
367 let json = emit_enum(&enum_def);
368 assert_eq!(json["variants"][0]["int_value"], 1);
369 assert_eq!(json["variants"][0]["doc"], "Currently in use");
370
371 let second = json["variants"][1].as_object().unwrap();
372 assert!(second.get("int_value").is_none());
373 assert!(second.get("doc").is_none());
374 }
375
376 #[test]
377 fn enum_emits_top_level_doc_when_set() {
378 let mut enum_def = EnumDef::new("Role");
379 enum_def.doc = Some("User permission tier".into());
380 enum_def.variants.push(EnumVariant::new("Admin"));
381
382 let json = emit_enum(&enum_def);
383 assert_eq!(json["doc"], "User permission tier");
384 }
385
386 #[test]
387 fn table_emits_top_level_doc_when_set() {
388 let mut table = TableDef::new("users", "User");
389 table.doc = Some("Account records".into());
390 table.fields.push(FieldDef::new("id", RustType::Uuid));
391
392 let json = emit_table(&table);
393 assert_eq!(json["doc"], "Account records");
394 assert_eq!(json["kind"], "model");
395 }
396
397 #[test]
398 fn function_with_no_args_emits_empty_array() {
399 let func = FunctionDef::query("ping", RustType::Bool);
400 let json = emit_function(&func);
401 assert_eq!(json["kind"], "query");
402 assert_eq!(json["args"], json!([]));
403 assert_eq!(json["returns"], json!("boolean"));
404 }
405
406 #[test]
407 fn types_and_functions_sort_alphabetically_in_string_output() {
408 let registry = SchemaRegistry::new();
409 registry.register_function(FunctionDef::query("z_last", RustType::String));
410 registry.register_function(FunctionDef::query("a_first", RustType::String));
411
412 let mut zebra = TableDef::new("zebras", "Zebra");
413 zebra.fields.push(FieldDef::new("id", RustType::Uuid));
414 registry.register_table(zebra);
415
416 let mut apple = TableDef::new("apples", "Apple");
417 apple.fields.push(FieldDef::new("id", RustType::Uuid));
418 registry.register_table(apple);
419
420 let output = emit_string(®istry).unwrap();
421
422 let a_pos = output.find("\"a_first\"").expect("a_first present");
423 let z_pos = output.find("\"z_last\"").expect("z_last present");
424 assert!(
425 a_pos < z_pos,
426 "functions must be sorted: a_first at {a_pos}, z_last at {z_pos}"
427 );
428
429 let apple_pos = output.find("\"Apple\"").expect("Apple present");
430 let zebra_pos = output.find("\"Zebra\"").expect("Zebra present");
431 assert!(
432 apple_pos < zebra_pos,
433 "types must be sorted: Apple at {apple_pos}, Zebra at {zebra_pos}"
434 );
435 }
436}