Skip to main content

active_core/convert/
gen.rs

1//! Typed code generation from Active schemas (C-047,
2//! spec/converters.md §7). Reads the module's schema tree and emits
3//! JSON Schema, an OpenAPI fragment, TypeScript interfaces, Go
4//! structs, and Rust serde types. Only static literal constraints
5//! render — dynamic bounds still validate at runtime but have no
6//! static form (documented, never guessed).
7
8use crate::ir::{Module, Node, NodeId, SchemaField};
9use crate::types::ActiveType;
10use serde_json::{json, Value as JsonValue};
11use std::collections::{HashMap, HashSet};
12
13fn no_schema() -> String {
14    "document has no schema block".to_string()
15}
16
17fn schema_root(module: &Module) -> Result<NodeId, String> {
18    module.schema.ok_or_else(no_schema)
19}
20
21// ------------------------------------------------------------------
22// Shared shape helpers.
23// ------------------------------------------------------------------
24
25/// Static literal integer bound (dynamic expressions skipped).
26fn literal_int(module: &Module, id: Option<NodeId>) -> Option<i64> {
27    match id.and_then(|node| module.get(node)) {
28        Some(Node::Literal(crate::ir::Value::Int(i))) => Some(*i),
29        _ => None,
30    }
31}
32
33/// Static literal float bound (plain integers widen).
34fn literal_float(module: &Module, id: Option<NodeId>) -> Option<f64> {
35    match id.and_then(|node| module.get(node)) {
36        Some(Node::Literal(crate::ir::Value::Float(f))) => Some(*f),
37        Some(Node::Literal(crate::ir::Value::Int(i))) => Some(*i as f64),
38        _ => None,
39    }
40}
41
42/// Static literal string bound.
43fn literal_string(module: &Module, id: Option<NodeId>) -> Option<String> {
44    match id.and_then(|node| module.get(node)) {
45        Some(Node::Literal(crate::ir::Value::String(s))) => Some(s.clone()),
46        _ => None,
47    }
48}
49
50/// Static literal default as JSON (any literal kind, redacted like
51/// every export — secrets never leak through generated defaults).
52fn literal_default(module: &Module, field: &SchemaField) -> Option<JsonValue> {
53    match field.default.and_then(|node| module.get(node)) {
54        Some(Node::Literal(value)) => Some(super::value_to_json(value)),
55        _ => None,
56    }
57}
58
59/// Required unless a static default exists or the field is Null
60/// (absent Null keys validate clean — C-032). Tables and arrays are
61/// always required: they carry no default slot.
62fn is_required(module: &Module, id: NodeId) -> bool {
63    match module.get(id) {
64        Some(Node::SchemaField { field }) => {
65            !matches!(field.typ, ActiveType::Null) && literal_default(module, field).is_none()
66        }
67        _ => true,
68    }
69}
70
71fn pascal_case(s: &str) -> String {
72    let mut out = String::new();
73    for part in s.split(['_', '-']) {
74        let mut chars = part.chars();
75        match chars.next() {
76            None => out.push('_'),
77            Some(first) => {
78                out.extend(first.to_uppercase());
79                out.push_str(chars.as_str());
80            }
81        }
82    }
83    if out.is_empty() {
84        return "Type".to_string();
85    }
86    if out.starts_with(|c: char| c.is_ascii_digit()) {
87        out.insert(0, '_');
88    }
89    out
90}
91
92fn sanitize_ident(s: &str, fallback: &str) -> String {
93    let clean: String = s
94        .chars()
95        .map(|c| {
96            if c.is_ascii_alphanumeric() || c == '_' {
97                c
98            } else {
99                '_'
100            }
101        })
102        .collect();
103    if clean.is_empty()
104        || clean.starts_with(|c: char| c.is_ascii_digit())
105        || !clean
106            .chars()
107            .all(|c: char| c.is_ascii_alphanumeric() || c == '_')
108    {
109        return fallback.to_string();
110    }
111    clean
112}
113
114/// Innermost nominal node through array wrappers: arrays are
115/// transparent for naming (the element table/enum owns the name).
116fn nominal_id(module: &Module, mut id: NodeId) -> NodeId {
117    while let Some(Node::SchemaArray { element }) = module.get(id) {
118        id = *element;
119    }
120    id
121}
122
123// ------------------------------------------------------------------
124// JSON Schema (Draft 2020-12 compatible subset).
125// ------------------------------------------------------------------
126
127/// The schema tree as JSON Schema: objects for tables (closed →
128/// `additionalProperties: false`), enums, arrays, constraints from
129/// static literals, static defaults, and `deprecated` flags.
130pub fn schema_to_json_schema(module: &Module) -> Result<JsonValue, String> {
131    let root = schema_root(module)?;
132    Ok(json_schema_node(module, root))
133}
134
135fn json_schema_node(module: &Module, id: NodeId) -> JsonValue {
136    match module.get(id) {
137        Some(Node::SchemaTable { fields, closed }) => {
138            let mut properties = serde_json::Map::new();
139            let mut required = Vec::new();
140            for (name, field) in fields {
141                properties.insert(name.clone(), json_schema_node(module, *field));
142                if is_required(module, *field) {
143                    required.push(JsonValue::String(name.clone()));
144                }
145            }
146            let mut obj = serde_json::Map::new();
147            obj.insert("type".to_string(), JsonValue::String("object".to_string()));
148            obj.insert("properties".to_string(), JsonValue::Object(properties));
149            if !required.is_empty() {
150                obj.insert("required".to_string(), JsonValue::Array(required));
151            }
152            if *closed {
153                obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
154            }
155            JsonValue::Object(obj)
156        }
157        Some(Node::SchemaArray { element }) => {
158            json!({"type": "array", "items": json_schema_node(module, *element)})
159        }
160        Some(Node::SchemaField { field }) => json_schema_field(module, field),
161        _ => JsonValue::Bool(true),
162    }
163}
164
165fn json_schema_field(module: &Module, field: &SchemaField) -> JsonValue {
166    let mut obj = serde_json::Map::new();
167    match &field.typ {
168        ActiveType::Null => {
169            obj.insert("type".to_string(), JsonValue::String("null".to_string()));
170        }
171        ActiveType::Bool => {
172            obj.insert("type".to_string(), JsonValue::String("boolean".to_string()));
173        }
174        ActiveType::Int => {
175            obj.insert("type".to_string(), JsonValue::String("integer".to_string()));
176            if let Some(min) = literal_int(module, field.min) {
177                obj.insert("minimum".to_string(), JsonValue::from(min));
178            }
179            if let Some(max) = literal_int(module, field.max) {
180                obj.insert("maximum".to_string(), JsonValue::from(max));
181            }
182        }
183        ActiveType::Float => {
184            obj.insert("type".to_string(), JsonValue::String("number".to_string()));
185            if let Some(min) = literal_float(module, field.min) {
186                obj.insert("minimum".to_string(), JsonValue::from(min));
187            }
188            if let Some(max) = literal_float(module, field.max) {
189                obj.insert("maximum".to_string(), JsonValue::from(max));
190            }
191        }
192        ActiveType::String => {
193            obj.insert("type".to_string(), JsonValue::String("string".to_string()));
194            if let Some(min) = literal_int(module, field.min_len) {
195                obj.insert("minLength".to_string(), JsonValue::from(min));
196            }
197            if let Some(max) = literal_int(module, field.max_len) {
198                obj.insert("maxLength".to_string(), JsonValue::from(max));
199            }
200            if let Some(pattern) = literal_string(module, field.pattern) {
201                obj.insert("pattern".to_string(), JsonValue::String(pattern));
202            }
203        }
204        // Matches the §1 export shape: durations serialize as
205        // `{"ms": n}` objects.
206        ActiveType::Duration => {
207            obj.insert("type".to_string(), JsonValue::String("object".to_string()));
208            obj.insert("properties".to_string(), json!({"ms": {"type": "integer"}}));
209            obj.insert("required".to_string(), json!(["ms"]));
210            obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
211        }
212        // Bytes normalize to integers on export.
213        ActiveType::Bytes => {
214            obj.insert("type".to_string(), JsonValue::String("integer".to_string()));
215        }
216        ActiveType::Table { closed } => {
217            obj.insert("type".to_string(), JsonValue::String("object".to_string()));
218            if *closed {
219                obj.insert("additionalProperties".to_string(), JsonValue::Bool(false));
220            }
221        }
222        ActiveType::Enum(members) => {
223            obj.insert(
224                "enum".to_string(),
225                JsonValue::Array(members.iter().cloned().map(JsonValue::String).collect()),
226            );
227        }
228        // Secrets export as redacted strings.
229        ActiveType::Secret => {
230            obj.insert("type".to_string(), JsonValue::String("string".to_string()));
231            obj.insert(
232                "description".to_string(),
233                JsonValue::String("secret value, redacted as ***SECRET*** on export".to_string()),
234            );
235        }
236        ActiveType::Any => return JsonValue::Bool(true),
237    }
238    if let Some(default) = literal_default(module, field) {
239        obj.insert("default".to_string(), default);
240    }
241    if let Some(reason) = field.deprecated.as_deref() {
242        obj.insert("deprecated".to_string(), JsonValue::Bool(true));
243        if !reason.is_empty() {
244            obj.insert(
245                "description".to_string(),
246                JsonValue::String(reason.to_string()),
247            );
248        }
249    }
250    JsonValue::Object(obj)
251}
252
253// ------------------------------------------------------------------
254// OpenAPI fragment (3.1: plain JSON Schema under components).
255// ------------------------------------------------------------------
256
257/// The JSON Schema wrapped as an OpenAPI 3.1
258/// `components.schemas.<name>` fragment.
259pub fn schema_to_openapi(module: &Module, name: &str) -> Result<JsonValue, String> {
260    let schema = schema_to_json_schema(module)?;
261    let mut schemas = serde_json::Map::new();
262    schemas.insert(sanitize_ident(name, "Config"), schema);
263    let mut components = serde_json::Map::new();
264    components.insert("schemas".to_string(), JsonValue::Object(schemas));
265    let mut out = serde_json::Map::new();
266    out.insert(
267        "openapi".to_string(),
268        JsonValue::String("3.1.0".to_string()),
269    );
270    out.insert("components".to_string(), JsonValue::Object(components));
271    Ok(JsonValue::Object(out))
272}
273
274// ------------------------------------------------------------------
275// TypeScript interfaces.
276// ------------------------------------------------------------------
277
278/// The schema tree as TypeScript `export interface` declarations
279/// (nested tables become PascalCase interfaces; `?` marks
280/// defaulted/Null fields).
281pub fn schema_to_typescript(module: &Module, root_name: &str) -> Result<String, String> {
282    let root = schema_root(module)?;
283    if !matches!(module.get(root), Some(Node::SchemaTable { .. })) {
284        return Err("schema block must be a table for code generation".to_string());
285    }
286    let mut gen = TsGen {
287        module,
288        out: String::new(),
289        names: HashMap::new(),
290        taken: HashSet::new(),
291    };
292    let root_name = gen.type_name(root, pascal_case(&sanitize_ident(root_name, "Config")));
293    gen.emit_table(&root_name, root);
294    Ok(gen.out)
295}
296
297struct TsGen<'a> {
298    module: &'a Module,
299    out: String,
300    /// Type name per node plus every assigned name: same-derived
301    /// names for different nodes (`port` vs `Port` tables) gain
302    /// suffixes deterministically instead of colliding.
303    names: HashMap<NodeId, String>,
304    taken: HashSet<String>,
305}
306
307impl TsGen<'_> {
308    /// Canonical type name for a node under a derived base: stable
309    /// across the reference and declaration passes, which compute
310    /// the same base independently.
311    fn type_name(&mut self, id: NodeId, base: String) -> String {
312        if let Some(name) = self.names.get(&id) {
313            return name.clone();
314        }
315        let mut name = base;
316        while !self.taken.insert(name.clone()) {
317            name.push('_');
318        }
319        self.names.insert(id, name.clone());
320        name
321    }
322
323    fn emit_table(&mut self, name: &str, id: NodeId) {
324        let Some(Node::SchemaTable { fields, .. }) = self.module.get(id) else {
325            return;
326        };
327        let fields: Vec<(String, NodeId)> = fields.iter().map(|(k, v)| (k.clone(), *v)).collect();
328        self.out.push_str(&format!("export interface {name} {{\n"));
329        for (key, field) in &fields {
330            let optional = !is_required(self.module, *field);
331            let nominal = nominal_id(self.module, *field);
332            let tname = self.type_name(nominal, format!("{name}{}", pascal_case(key)));
333            let typ = self.ts_type(&tname, *field);
334            let mark = if optional { "?" } else { "" };
335            if let Some(reason) = self.deprecated(*field) {
336                if reason.is_empty() {
337                    self.out.push_str("  /** @deprecated */\n");
338                } else {
339                    self.out
340                        .push_str(&format!("  /** @deprecated {reason} */\n"));
341                }
342            }
343            self.out.push_str(&format!("  {key}{mark}: {typ};\n"));
344        }
345        self.out.push_str("}\n");
346        for (key, field) in &fields {
347            let nominal = nominal_id(self.module, *field);
348            let tname = self.type_name(nominal, format!("{name}{}", pascal_case(key)));
349            self.emit_nested(&tname, *field);
350        }
351    }
352
353    fn emit_nested(&mut self, name: &str, id: NodeId) {
354        match self.module.get(id) {
355            Some(Node::SchemaTable { .. }) => self.emit_table(name, id),
356            Some(Node::SchemaArray { element }) => self.emit_nested(name, *element),
357            _ => {}
358        }
359    }
360
361    fn ts_type(&mut self, hint: &str, id: NodeId) -> String {
362        match self.module.get(id) {
363            // Named by the caller: emit_table/emit_nested declares
364            // the same hint, so references always resolve.
365            Some(Node::SchemaTable { .. }) => hint.to_string(),
366            Some(Node::SchemaArray { element }) => {
367                format!("{}[]", self.ts_inline(hint, *element))
368            }
369            Some(Node::SchemaField { field }) => self.ts_field(field),
370            _ => "unknown".to_string(),
371        }
372    }
373
374    /// Inline type for array elements, threaded with the same naming
375    /// hint the declaration pass uses — element tables resolve to
376    /// the named interface, never to anonymous duplicates.
377    fn ts_inline(&mut self, hint: &str, id: NodeId) -> String {
378        match self.module.get(id) {
379            Some(Node::SchemaTable { .. }) => hint.to_string(),
380            Some(Node::SchemaArray { element }) => {
381                format!("{}[]", self.ts_inline(hint, *element))
382            }
383            Some(Node::SchemaField { field }) => self.ts_field(field),
384            _ => "unknown".to_string(),
385        }
386    }
387
388    fn ts_field(&self, field: &SchemaField) -> String {
389        match &field.typ {
390            ActiveType::Null => "null".to_string(),
391            ActiveType::Bool => "boolean".to_string(),
392            ActiveType::Int | ActiveType::Float | ActiveType::Duration | ActiveType::Bytes => {
393                "number".to_string()
394            }
395            ActiveType::String | ActiveType::Secret => "string".to_string(),
396            ActiveType::Table { .. } => "Record<string, unknown>".to_string(),
397            ActiveType::Enum(members) => members
398                .iter()
399                .map(|m| format!("{m:?}"))
400                .collect::<Vec<_>>()
401                .join(" | "),
402            ActiveType::Any => "unknown".to_string(),
403        }
404    }
405
406    fn deprecated(&self, id: NodeId) -> Option<String> {
407        match self.module.get(id) {
408            Some(Node::SchemaField { field }) => field.deprecated.clone(),
409            _ => None,
410        }
411    }
412}
413
414// ------------------------------------------------------------------
415// Go structs.
416// ------------------------------------------------------------------
417
418/// The schema tree as Go structs with `json` tags (nested tables
419/// become PascalCase structs; enums stay strings with their members
420/// documented).
421pub fn schema_to_go(module: &Module, root_name: &str) -> Result<String, String> {
422    let root = schema_root(module)?;
423    if !matches!(module.get(root), Some(Node::SchemaTable { .. })) {
424        return Err("schema block must be a table for code generation".to_string());
425    }
426    let mut gen = GoGen {
427        module,
428        out: String::new(),
429        names: HashMap::new(),
430        taken: HashSet::new(),
431    };
432    let root_name = gen.type_name(root, pascal_case(&sanitize_ident(root_name, "Config")));
433    gen.emit_table(&root_name, root);
434    Ok(gen.out)
435}
436
437struct GoGen<'a> {
438    module: &'a Module,
439    out: String,
440    names: HashMap<NodeId, String>,
441    taken: HashSet<String>,
442}
443
444impl GoGen<'_> {
445    fn type_name(&mut self, id: NodeId, base: String) -> String {
446        if let Some(name) = self.names.get(&id) {
447            return name.clone();
448        }
449        let mut name = base;
450        while !self.taken.insert(name.clone()) {
451            name.push('_');
452        }
453        self.names.insert(id, name.clone());
454        name
455    }
456
457    fn emit_table(&mut self, name: &str, id: NodeId) {
458        let Some(Node::SchemaTable { fields, .. }) = self.module.get(id) else {
459            return;
460        };
461        let fields: Vec<(String, NodeId)> = fields.iter().map(|(k, v)| (k.clone(), *v)).collect();
462        self.out.push_str(&format!("type {name} struct {{\n"));
463        let mut taken = HashSet::new();
464        for (key, field) in &fields {
465            let mut field_name = pascal_case(key);
466            while !taken.insert(field_name.clone()) {
467                field_name.push('_');
468            }
469            let nominal = nominal_id(self.module, *field);
470            let tname = self.type_name(nominal, format!("{name}{}", pascal_case(key)));
471            let typ = self.go_type(&tname, *field);
472            if let Some(reason) = self.deprecated(*field) {
473                if reason.is_empty() {
474                    self.out.push_str("\t// Deprecated: no reason given.\n");
475                } else {
476                    self.out.push_str(&format!("\t// Deprecated: {reason}\n"));
477                }
478            }
479            if let Some(doc) = self.doc(*field) {
480                self.out.push_str(&format!("\t// {doc}\n"));
481            }
482            self.out
483                .push_str(&format!("\t{field_name} {typ} `json:\"{key}\"`\n"));
484        }
485        self.out.push_str("}\n");
486        for (key, field) in &fields {
487            let nominal = nominal_id(self.module, *field);
488            let tname = self.type_name(nominal, format!("{name}{}", pascal_case(key)));
489            self.emit_nested(&tname, *field);
490        }
491    }
492
493    fn emit_nested(&mut self, name: &str, id: NodeId) {
494        match self.module.get(id) {
495            Some(Node::SchemaTable { .. }) => self.emit_table(name, id),
496            Some(Node::SchemaArray { element }) => self.emit_nested(name, *element),
497            _ => {}
498        }
499    }
500
501    fn go_type(&mut self, hint: &str, id: NodeId) -> String {
502        match self.module.get(id) {
503            Some(Node::SchemaTable { .. }) => hint.to_string(),
504            Some(Node::SchemaArray { element }) => {
505                format!("[]{}", self.go_inline(hint, *element))
506            }
507            Some(Node::SchemaField { field }) => self.go_field(field),
508            _ => "interface{}".to_string(),
509        }
510    }
511
512    fn go_inline(&mut self, hint: &str, id: NodeId) -> String {
513        match self.module.get(id) {
514            // Element tables resolve to the named struct the
515            // declaration pass emits under the same hint.
516            Some(Node::SchemaTable { .. }) => hint.to_string(),
517            Some(Node::SchemaArray { element }) => {
518                format!("[]{}", self.go_inline(hint, *element))
519            }
520            Some(Node::SchemaField { field }) => self.go_field(field),
521            _ => "interface{}".to_string(),
522        }
523    }
524
525    fn go_field(&self, field: &SchemaField) -> String {
526        match &field.typ {
527            ActiveType::Null | ActiveType::Any => "interface{}".to_string(),
528            ActiveType::Bool => "bool".to_string(),
529            ActiveType::Int | ActiveType::Duration | ActiveType::Bytes => "int64".to_string(),
530            ActiveType::Float => "float64".to_string(),
531            ActiveType::String | ActiveType::Secret | ActiveType::Enum(_) => "string".to_string(),
532            ActiveType::Table { .. } => "map[string]interface{}".to_string(),
533        }
534    }
535
536    fn doc(&self, id: NodeId) -> Option<String> {
537        match self.module.get(id) {
538            Some(Node::SchemaField { field }) => match &field.typ {
539                ActiveType::Duration => Some("milliseconds".to_string()),
540                ActiveType::Enum(members) => Some(format!("one of: {}", members.join(", "))),
541                ActiveType::Secret => {
542                    Some("secret value, redacted as ***SECRET*** on export".to_string())
543                }
544                _ => None,
545            },
546            _ => None,
547        }
548    }
549
550    fn deprecated(&self, id: NodeId) -> Option<String> {
551        match self.module.get(id) {
552            Some(Node::SchemaField { field }) => field.deprecated.clone(),
553            _ => None,
554        }
555    }
556}
557
558// ------------------------------------------------------------------
559// Rust serde types.
560// ------------------------------------------------------------------
561
562const RUST_KEYWORDS: &[&str] = &[
563    "as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for",
564    "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return",
565    "self", "Self", "static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where",
566    "while", "async", "await", "dyn", "abstract", "become", "box", "do", "final", "macro",
567    "override", "priv", "typeof", "unsized", "virtual", "yield", "try",
568];
569
570/// The schema tree as Rust serde types: structs with `rename` tags,
571/// real enums for `Enum`, `Option` for defaulted/Null fields.
572pub fn schema_to_rust(module: &Module, root_name: &str) -> Result<String, String> {
573    let root = schema_root(module)?;
574    if !matches!(module.get(root), Some(Node::SchemaTable { .. })) {
575        return Err("schema block must be a table for code generation".to_string());
576    }
577    let mut gen = RustGen {
578        module,
579        out: String::from(
580            "// Requires: serde = { version = \"1\", features = [\"derive\"] }, serde_json = \"1\"\nuse serde::{Deserialize, Serialize};\n\n",
581        ),
582        names: HashMap::new(),
583        taken: HashSet::new(),
584    };
585    let root_name = gen.type_name(root, pascal_case(&sanitize_ident(root_name, "Config")));
586    gen.emit_table(&root_name, root);
587    Ok(gen.out)
588}
589
590struct RustGen<'a> {
591    module: &'a Module,
592    out: String,
593    names: HashMap<NodeId, String>,
594    taken: HashSet<String>,
595}
596
597impl RustGen<'_> {
598    fn type_name(&mut self, id: NodeId, base: String) -> String {
599        if let Some(name) = self.names.get(&id) {
600            return name.clone();
601        }
602        let mut name = base;
603        while !self.taken.insert(name.clone()) {
604            name.push('_');
605        }
606        self.names.insert(id, name.clone());
607        name
608    }
609
610    fn emit_table(&mut self, name: &str, id: NodeId) {
611        let Some(Node::SchemaTable { fields, .. }) = self.module.get(id) else {
612            return;
613        };
614        let fields: Vec<(String, NodeId)> = fields.iter().map(|(k, v)| (k.clone(), *v)).collect();
615        self.out
616            .push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct ");
617        self.out.push_str(name);
618        self.out.push_str(" {\n");
619        let mut taken = HashSet::new();
620        for (key, field) in &fields {
621            let optional = !is_required(self.module, *field);
622            let raw = sanitize_ident(key, "field");
623            let (field_name, mut renamed) = if RUST_KEYWORDS.contains(&raw.as_str()) {
624                (format!("r#{raw}"), true)
625            } else {
626                (raw.clone(), raw != *key)
627            };
628            let mut field_name = field_name;
629            while !taken.insert(field_name.clone()) {
630                field_name.push('_');
631                renamed = true;
632            }
633            let inner = {
634                let nominal = nominal_id(self.module, *field);
635                let tname = self.type_name(nominal, format!("{name}{}", pascal_case(key)));
636                self.rust_type(&tname, *field)
637            };
638            let typ = if optional {
639                format!("Option<{inner}>")
640            } else {
641                inner
642            };
643            if let Some(reason) = self.deprecated(*field) {
644                if reason.is_empty() {
645                    self.out.push_str("\t#[deprecated]\n");
646                } else {
647                    self.out
648                        .push_str(&format!("\t#[deprecated(note = {reason:?})]\n"));
649                }
650            }
651            // Raw identifiers (`r#type`) never match their key
652            // textually; serde_derive strips the prefix, so an
653            // explicit rename keeps the mapping exact either way.
654            if renamed {
655                self.out
656                    .push_str(&format!("\t#[serde(rename = {key:?})]\n"));
657            }
658            self.out.push_str(&format!("\tpub {field_name}: {typ},\n"));
659        }
660        self.out.push_str("}\n");
661        for (key, field) in &fields {
662            let nominal = nominal_id(self.module, *field);
663            let tname = self.type_name(nominal, format!("{name}{}", pascal_case(key)));
664            self.emit_nested(&tname, *field);
665        }
666    }
667
668    fn emit_nested(&mut self, name: &str, id: NodeId) {
669        match self.module.get(id) {
670            Some(Node::SchemaTable { .. }) => self.emit_table(name, id),
671            Some(Node::SchemaArray { element }) => self.emit_nested(name, *element),
672            Some(Node::SchemaField { field }) => {
673                if let ActiveType::Enum(members) = &field.typ {
674                    self.emit_enum(name, members);
675                }
676            }
677            _ => {}
678        }
679    }
680
681    fn emit_enum(&mut self, name: &str, members: &[String]) {
682        self.out
683            .push_str("#[derive(Debug, Clone, Serialize, Deserialize)]\npub enum ");
684        self.out.push_str(name);
685        self.out.push_str(" {\n");
686        let mut taken = HashSet::new();
687        for member in members {
688            let mut variant = pascal_case(&sanitize_ident(member, "Value"));
689            while !taken.insert(variant.clone()) {
690                variant.push('_');
691            }
692            if &variant != member {
693                self.out
694                    .push_str(&format!("\t#[serde(rename = {member:?})]\n"));
695            }
696            self.out.push_str(&format!("\t{variant},\n"));
697        }
698        self.out.push_str("}\n");
699    }
700
701    fn rust_type(&mut self, hint: &str, id: NodeId) -> String {
702        match self.module.get(id) {
703            Some(Node::SchemaTable { .. }) => hint.to_string(),
704            Some(Node::SchemaArray { element }) => {
705                format!("Vec<{}>", self.rust_inline(hint, *element))
706            }
707            Some(Node::SchemaField { field }) => match &field.typ {
708                ActiveType::Null => "()".to_string(),
709                ActiveType::Bool => "bool".to_string(),
710                ActiveType::Int => "i64".to_string(),
711                ActiveType::Float => "f64".to_string(),
712                ActiveType::String | ActiveType::Secret => "String".to_string(),
713                ActiveType::Duration | ActiveType::Bytes => "u64".to_string(),
714                ActiveType::Table { .. } => {
715                    "std::collections::HashMap<String, serde_json::Value>".to_string()
716                }
717                ActiveType::Enum(_) => hint.to_string(),
718                ActiveType::Any => "serde_json::Value".to_string(),
719            },
720            _ => "serde_json::Value".to_string(),
721        }
722    }
723
724    /// Element types under the declaration hint: element tables and
725    /// enums resolve to the named items `emit_nested` declares.
726    fn rust_inline(&mut self, hint: &str, id: NodeId) -> String {
727        match self.module.get(id) {
728            Some(Node::SchemaTable { .. }) => hint.to_string(),
729            Some(Node::SchemaArray { element }) => {
730                format!("Vec<{}>", self.rust_inline(hint, *element))
731            }
732            Some(Node::SchemaField { field }) => match &field.typ {
733                ActiveType::Enum(_) => hint.to_string(),
734                _ => self.rust_type(hint, id),
735            },
736            _ => "serde_json::Value".to_string(),
737        }
738    }
739
740    fn deprecated(&self, id: NodeId) -> Option<String> {
741        match self.module.get(id) {
742            Some(Node::SchemaField { field }) => field.deprecated.clone(),
743            _ => None,
744        }
745    }
746}