1use 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
21fn 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
33fn 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
42fn 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
50fn 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
59fn 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
114fn 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
123pub 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 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 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 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
253pub 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
274pub 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 names: HashMap<NodeId, String>,
304 taken: HashSet<String>,
305}
306
307impl TsGen<'_> {
308 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 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 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
414pub 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 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
558const 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
570pub 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 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 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}