1use crate::diag::{Diagnostic, Severity};
8use serde_json::Value as JsonValue;
9use std::collections::HashSet;
10
11pub fn json_text_to_active(text: &str) -> Result<(String, Vec<Diagnostic>), String> {
14 let value: JsonValue =
15 serde_json::from_str(text).map_err(|e| hard_error("invalid JSON", &e.to_string()))?;
16 Ok(json_value_to_active(&value))
17}
18
19pub fn yaml_text_to_active(text: &str) -> Result<(String, Vec<Diagnostic>), String> {
21 let yaml: serde_yaml::Value =
22 serde_yaml::from_str(text).map_err(|e| hard_error("invalid YAML", &e.to_string()))?;
23 let mut emitter = Emitter::new();
24 let value = emitter.yaml_to_json(&yaml, "");
25 let (source, mut diags) = emitter.finish(&value);
26 diags.sort_by(|a, b| a.code.cmp(&b.code));
27 Ok((source, diags))
28}
29
30pub fn toml_text_to_active(text: &str) -> Result<(String, Vec<Diagnostic>), String> {
32 let toml: toml::Value =
33 toml::from_str(text).map_err(|e| hard_error("invalid TOML", &e.to_string()))?;
34 let mut emitter = Emitter::new();
35 let value = emitter.toml_to_json(&toml, "");
36 let (source, mut diags) = emitter.finish(&value);
37 diags.sort_by(|a, b| a.code.cmp(&b.code));
38 Ok((source, diags))
39}
40
41fn hard_error(prefix: &str, detail: &str) -> String {
45 let first = detail.lines().next().unwrap_or("").trim();
46 format!("{prefix}: {first} (source excerpt withheld)")
47}
48
49pub fn json_value_to_active(v: &JsonValue) -> (String, Vec<Diagnostic>) {
54 Emitter::new().finish(v)
55}
56
57struct Emitter {
59 diags: Vec<Diagnostic>,
60}
61
62impl Emitter {
63 fn new() -> Self {
64 Self { diags: Vec::new() }
65 }
66
67 fn finish(mut self, v: &JsonValue) -> (String, Vec<Diagnostic>) {
68 let mut out = String::new();
69 match v {
70 JsonValue::Object(map) => {
71 self.emit_block(map, &mut out, 0, "");
72 }
73 other => {
74 self.note(
75 "W009",
76 format!("top-level {} wrapped as 'value'", Self::kind_name(other)),
77 );
78 let inline = self.inline(other);
79 out.push_str(&format!("value = {inline}\n"));
80 }
81 }
82 (out, self.diags)
83 }
84
85 fn kind_name(v: &JsonValue) -> &'static str {
86 match v {
87 JsonValue::Null => "null",
88 JsonValue::Bool(_) => "boolean",
89 JsonValue::Number(_) => "number",
90 JsonValue::String(_) => "string",
91 JsonValue::Array(_) => "array",
92 JsonValue::Object(_) => "object",
93 }
94 }
95
96 fn emit_block(
100 &mut self,
101 map: &serde_json::Map<String, JsonValue>,
102 out: &mut String,
103 indent: usize,
104 path: &str,
105 ) {
106 let pad = " ".repeat(indent);
107 let mut seen = HashSet::new();
108 for (key, val) in map {
109 let (clean, renamed) = self.block_key(key, val);
110 if renamed {
111 self.note("W007", format!("imported key '{key}' renamed to '{clean}'"));
112 }
113 if !seen.insert(clean.clone()) {
114 self.note(
115 "W007",
116 format!("duplicate key '{clean}' after sanitization; last wins"),
117 );
118 }
119 let child = if path.is_empty() {
120 clean.clone()
121 } else {
122 format!("{path}.{clean}")
123 };
124 match val {
125 JsonValue::Object(nested) => {
126 out.push_str(&format!("{pad}{clean} {{\n"));
127 self.emit_block(nested, out, indent + 1, &child);
128 out.push_str(&format!("{pad}}}\n"));
129 }
130 _ => {
131 let inline = self.inline(val);
132 out.push_str(&format!("{pad}{clean} = {inline}\n"));
133 }
134 }
135 }
136 }
137
138 fn block_key(&self, key: &str, val: &JsonValue) -> (String, bool) {
142 let clean = sanitize_key(key);
143 if let Some(renamed) = contextual_rename(&clean, val) {
144 return (renamed, true);
145 }
146 let changed = clean != key;
147 (clean, changed)
148 }
149
150 fn inline(&mut self, val: &JsonValue) -> String {
156 match val {
157 JsonValue::Null => "null".to_string(),
158 JsonValue::Bool(b) => b.to_string(),
159 JsonValue::Number(n) => {
160 if let Some(i) = n.as_i64() {
161 i.to_string()
162 } else if let Some(u) = n.as_u64() {
163 self.note(
164 "W010",
165 format!("integer {u} exceeds i64 range; emitted as float"),
166 );
167 format!("{}.0", u as f64)
173 } else {
174 n.as_f64()
182 .map(|f| {
183 if !f.is_finite() {
184 "null".to_string()
185 } else {
186 let s = format!("{f}");
187 if s.contains('.') {
188 s
189 } else {
190 format!("{s}.0")
191 }
192 }
193 })
194 .unwrap_or("null".to_string())
195 }
196 }
197 JsonValue::String(s) => Self::quote(s),
198 JsonValue::Array(items) => {
199 let parts: Vec<String> = items.iter().map(|item| self.inline(item)).collect();
200 format!("[{}]", parts.join(", "))
201 }
202 JsonValue::Object(map) => {
203 let mut parts = Vec::with_capacity(map.len());
204 let mut seen = HashSet::new();
205 for (key, item) in map {
206 let clean = sanitize_key(key);
207 let clean = contextual_rename(&clean, item).unwrap_or(clean);
208 if clean != *key {
209 self.note("W007", format!("imported key '{key}' renamed to '{clean}'"));
210 }
211 if !seen.insert(clean.clone()) {
212 self.note(
213 "W007",
214 format!("duplicate key '{clean}' after sanitization; last wins"),
215 );
216 }
217 let inline = self.inline(item);
218 parts.push(format!("{clean} = {inline}"));
219 }
220 format!("{{ {} }}", parts.join(", "))
221 }
222 }
223 }
224
225 fn quote(s: &str) -> String {
228 let mut out = String::with_capacity(s.len() + 2);
229 out.push('"');
230 for c in s.chars() {
231 match c {
232 '\n' => out.push_str("\\n"),
233 '\t' => out.push_str("\\t"),
234 '\r' => out.push_str("\\r"),
235 '\\' => out.push_str("\\\\"),
236 '"' => out.push_str("\\\""),
237 '\0' => out.push_str("\\0"),
238 '$' => out.push_str("\\$"),
239 _ => out.push(c),
240 }
241 }
242 out.push('"');
243 out
244 }
245
246 fn quote_schema(s: &str) -> String {
252 let mut out = String::with_capacity(s.len() + 2);
253 out.push('"');
254 let mut chars = s.chars().peekable();
255 while let Some(c) = chars.next() {
256 match c {
257 '\n' => out.push_str("\\n"),
258 '\t' => out.push_str("\\t"),
259 '\r' => out.push_str("\\r"),
260 '\\' => out.push_str("\\\\"),
261 '"' => out.push_str("\\\""),
262 '\0' => out.push_str("\\0"),
263 '$' if chars.peek() == Some(&'{') => out.push_str("\\$"),
264 _ => out.push(c),
265 }
266 }
267 out.push('"');
268 out
269 }
270
271 fn note(&mut self, code: &str, message: String) {
272 self.diags.push(Self::note_static(code, message));
273 }
274
275 fn note_static(code: &str, message: String) -> Diagnostic {
276 Diagnostic {
277 severity: Severity::Warning,
278 code: code.into(),
279 message,
280 path: None,
281 span: None,
282 repair: None,
283 confidence: 1.0,
284 file: None,
285 }
286 }
287
288 fn yaml_to_json(&mut self, v: &serde_yaml::Value, path: &str) -> JsonValue {
291 match v {
292 serde_yaml::Value::Null => JsonValue::Null,
293 serde_yaml::Value::Bool(b) => JsonValue::Bool(*b),
294 serde_yaml::Value::Number(n) => {
295 if let Some(i) = n.as_i64() {
296 JsonValue::from(i)
297 } else if let Some(u) = n.as_u64() {
298 JsonValue::from(u)
299 } else {
300 match n.as_f64() {
301 Some(f) if f.is_finite() => JsonValue::from(f),
302 _ => {
305 self.note(
306 "W010",
307 format!(
308 "non-finite YAML number at '{}' converted to null",
309 display_path(path)
310 ),
311 );
312 JsonValue::Null
313 }
314 }
315 }
316 }
317 serde_yaml::Value::String(s) => JsonValue::String(s.clone()),
318 serde_yaml::Value::Sequence(items) => JsonValue::Array(
319 items
320 .iter()
321 .enumerate()
322 .map(|(i, item)| self.yaml_to_json(item, &format!("{path}[{i}]")))
323 .collect(),
324 ),
325 serde_yaml::Value::Mapping(map) => {
326 let mut out = serde_json::Map::new();
327 for (key, val) in map {
328 let key_str = match key {
329 serde_yaml::Value::String(s) => s.clone(),
330 serde_yaml::Value::Null => {
331 self.note("W007", "YAML null key stringified to 'null'".to_string());
332 "null".to_string()
333 }
334 other => {
335 let rendered = match other {
336 serde_yaml::Value::Bool(b) => b.to_string(),
337 serde_yaml::Value::Number(n) => n.to_string(),
338 _ => "key".to_string(),
339 };
340 self.note(
341 "W007",
342 format!("YAML non-string key stringified to '{rendered}'"),
343 );
344 rendered
345 }
346 };
347 let child = if path.is_empty() {
348 key_str.clone()
349 } else {
350 format!("{path}.{key_str}")
351 };
352 out.insert(key_str, self.yaml_to_json(val, &child));
353 }
354 JsonValue::Object(out)
355 }
356 serde_yaml::Value::Tagged(tagged) => self.yaml_to_json(&tagged.value, path),
357 }
358 }
359
360 fn toml_to_json(&mut self, v: &toml::Value, path: &str) -> JsonValue {
363 match v {
364 toml::Value::String(s) => JsonValue::String(s.clone()),
365 toml::Value::Integer(i) => JsonValue::from(*i),
366 toml::Value::Float(f) => {
367 if f.is_finite() {
368 JsonValue::from(*f)
369 } else {
370 self.note(
371 "W010",
372 format!(
373 "non-finite TOML number at '{}' converted to null",
374 display_path(path)
375 ),
376 );
377 JsonValue::Null
378 }
379 }
380 toml::Value::Boolean(b) => JsonValue::Bool(*b),
381 toml::Value::Datetime(dt) => {
382 self.note(
383 "W008",
384 format!(
385 "TOML datetime at '{}' imported as string",
386 display_path(path)
387 ),
388 );
389 JsonValue::String(dt.to_string())
390 }
391 toml::Value::Array(items) => JsonValue::Array(
392 items
393 .iter()
394 .enumerate()
395 .map(|(i, item)| self.toml_to_json(item, &format!("{path}[{i}]")))
396 .collect(),
397 ),
398 toml::Value::Table(map) => {
399 let mut out = serde_json::Map::new();
400 for (key, val) in map {
401 let child = if path.is_empty() {
402 key.clone()
403 } else {
404 format!("{path}.{key}")
405 };
406 out.insert(key.clone(), self.toml_to_json(val, &child));
407 }
408 JsonValue::Object(out)
409 }
410 }
411 }
412}
413
414fn sanitize_key(key: &str) -> String {
422 const RESERVED: &[&str] = &["let", "env", "secret", "true", "false", "null"];
423 let mut clean = String::with_capacity(key.len());
424 for c in key.chars() {
425 if c.is_ascii_alphanumeric() || c == '_' {
426 clean.push(c);
427 } else {
428 clean.push('_');
429 }
430 }
431 if clean.is_empty() {
432 return "key".to_string();
433 }
434 if clean.starts_with(|c: char| c.is_ascii_digit()) {
435 clean.insert(0, '_');
436 }
437 if RESERVED.contains(&clean.as_str()) {
438 clean.push('_');
439 }
440 clean
441}
442
443fn contextual_rename(clean: &str, val: &JsonValue) -> Option<String> {
451 match (clean, val) {
452 ("schema", JsonValue::Object(_)) => Some("schema_".to_string()),
453 ("depends_on", JsonValue::Array(_)) => Some("depends_on_".to_string()),
454 _ => None,
455 }
456}
457
458fn display_path(path: &str) -> &str {
459 if path.is_empty() {
460 "(root)"
461 } else {
462 path
463 }
464}
465
466pub fn jsonschema_to_active(text: &str) -> Result<(String, Vec<Diagnostic>), String> {
479 let schema: JsonValue = serde_json::from_str(text)
480 .map_err(|e| hard_error("invalid JSON Schema", &e.to_string()))?;
481 let mut conv = SchemaConverter::new();
482 let out = conv.convert_root(&schema);
483 conv.diags.sort_by(|a, b| a.code.cmp(&b.code));
484 Ok((out, conv.diags))
485}
486
487struct SchemaConverter {
488 diags: Vec<Diagnostic>,
489}
490
491impl SchemaConverter {
492 fn new() -> Self {
493 Self { diags: Vec::new() }
494 }
495
496 fn note(&mut self, message: String) {
497 self.note_code("W012", message);
498 }
499
500 fn note_code(&mut self, code: &str, message: String) {
501 self.diags.push(Diagnostic {
502 severity: Severity::Warning,
503 code: code.into(),
504 message,
505 path: None,
506 span: None,
507 repair: None,
508 confidence: 1.0,
509 file: None,
510 });
511 }
512
513 fn convert_root(&mut self, schema: &JsonValue) -> String {
514 let mut out = String::from("# Imported from JSON Schema (L-021): constructs without an Active counterpart, if any, are reported as W012 notes.\n");
515 match schema {
516 JsonValue::Object(_) => {
517 out.push_str("schema = ");
518 out.push_str(&self.convert_root_object(schema));
519 out.push('\n');
520 }
521 JsonValue::Bool(true) => {
522 out.push_str("schema = {}\n");
523 }
524 JsonValue::Bool(false) => {
525 self.note("boolean schema `false` (accept-nothing) has no Active counterpart; emitted open schema".to_string());
526 out.push_str("schema = {}\n");
527 }
528 other => {
529 self.note(format!(
530 "top-level schema must be an object or boolean, got {}; emitted open schema",
531 kind_word(other)
532 ));
533 out.push_str("schema = {}\n");
534 }
535 }
536 out
537 }
538
539 fn convert_root_object(&mut self, schema: &JsonValue) -> String {
544 let Some(obj) = schema.as_object() else {
545 return "Table".to_string();
546 };
547 let props = obj.get("properties").and_then(|v| v.as_object());
548 let required: Vec<&str> = obj
549 .get("required")
550 .and_then(|v| v.as_array())
551 .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
552 .unwrap_or_default();
553 for key in [
555 "allOf",
556 "anyOf",
557 "oneOf",
558 "not",
559 "$ref",
560 "$defs",
561 "definitions",
562 "$anchor",
563 "$dynamicRef",
564 "$dynamicAnchor",
565 "if",
566 "then",
567 "else",
568 "dependentRequired",
569 "dependentSchemas",
570 "contains",
571 "prefixItems",
572 "unevaluatedProperties",
573 "unevaluatedItems",
574 "contentMediaType",
575 "contentEncoding",
576 "multipleOf",
577 "const",
578 ] {
579 if obj.contains_key(key) {
580 self.note(format!(
581 "JSON Schema '{key}' at '(root)' has no Active counterpart; skipped"
582 ));
583 }
584 }
585 let empty = empty_map();
586 let mut fields = self.object_fields(obj, props.unwrap_or(&empty), &required, "");
587 if obj.get("additionalProperties").and_then(|v| v.as_bool()) == Some(false) {
588 if !fields.is_empty() {
589 fields.push_str(", ");
590 }
591 fields.push_str("closed = true");
592 }
593 format!("{{ {fields} }}")
594 }
595
596 fn field_name(&mut self, key: &str) -> String {
597 let clean = sanitize_key(key);
598 if clean != key {
599 self.note_code("W007", format!("imported key '{key}' renamed to '{clean}'"));
602 }
603 clean
604 }
605
606 fn convert_node(&mut self, schema: &JsonValue, path: &str) -> String {
607 let obj = match schema.as_object() {
608 Some(o) => o,
609 None => return "Any".to_string(),
610 };
611 for key in [
613 "allOf",
614 "anyOf",
615 "oneOf",
616 "not",
617 "$ref",
618 "$defs",
619 "definitions",
620 "$anchor",
621 "$dynamicRef",
622 "$dynamicAnchor",
623 "if",
624 "then",
625 "else",
626 "dependentRequired",
627 "dependentSchemas",
628 "contains",
629 "prefixItems",
630 "unevaluatedProperties",
631 "unevaluatedItems",
632 "contentMediaType",
633 "contentEncoding",
634 ] {
635 if obj.contains_key(key) {
636 self.note(format!(
637 "JSON Schema '{key}' at '{}' has no Active counterpart; skipped",
638 display_path(path)
639 ));
640 }
641 }
642 let props = obj.get("properties").and_then(|v| v.as_object());
645 let required: Vec<&str> = obj
646 .get("required")
647 .and_then(|v| v.as_array())
648 .map(|a| a.iter().filter_map(|v| v.as_str()).collect())
649 .unwrap_or_default();
650 let typ = obj.get("type").and_then(|v| v.as_str()).unwrap_or("");
651 if props.is_some() && !matches!(typ, "" | "object") {
654 self.note(format!(
655 "properties at '{}' contradict type '{typ}'; properties win",
656 display_path(path)
657 ));
658 }
659 match typ {
660 "object" => self.convert_object(obj, props.unwrap_or(&empty_map()), &required, path),
661 "" if props.is_some() => {
662 self.convert_object(obj, props.unwrap_or(&empty_map()), &required, path)
663 }
664 "" => "Any".to_string(),
667 "array" => self.convert_array(obj, path),
668 "string" => self.convert_string(obj, path),
669 "integer" => self.convert_int(obj, path),
670 "number" => self.convert_number(obj, path),
671 "boolean" => "Bool".to_string(),
672 "null" => "Null".to_string(),
673 other => {
674 self.note(format!(
675 "unknown type '{other}' at '{}'; emitted Any",
676 display_path(path)
677 ));
678 "Any".to_string()
679 }
680 }
681 }
682
683 fn convert_object(
684 &mut self,
685 obj: &serde_json::Map<String, JsonValue>,
686 props: &serde_json::Map<String, JsonValue>,
687 required: &[&str],
688 path: &str,
689 ) -> String {
690 let fields = self.object_fields(obj, props, required, path);
691 let closed = obj.get("additionalProperties").and_then(|v| v.as_bool()) == Some(false);
692 let mut out = String::from("Table { ");
693 out.push_str(&fields);
694 if closed {
695 if !fields.is_empty() {
696 out.push_str(", ");
697 }
698 out.push_str("closed = true");
699 }
700 out.push_str(" }");
701 out
702 }
703
704 fn object_fields(
705 &mut self,
706 _obj: &serde_json::Map<String, JsonValue>,
707 props: &serde_json::Map<String, JsonValue>,
708 required: &[&str],
709 path: &str,
710 ) -> String {
711 let mut parts = Vec::new();
712 for (key, subschema) in props {
713 let name = self.field_name(key);
714 let child = if path.is_empty() {
715 name.clone()
716 } else {
717 format!("{path}.{name}")
718 };
719 let rendered = self.convert_node(subschema, &child);
720 let is_required = required.contains(&key.as_str());
721 let has_default = subschema
722 .as_object()
723 .is_some_and(|o| o.contains_key("default"));
724 if !is_required && !has_default {
725 self.note(format!(
726 "optional property '{child}' without default tightens to required (Active has no optional-without-default)"
727 ));
728 }
729 let name = if name == "schema" && rendered.starts_with("Table") {
732 self.note("property 'schema' holding a table renamed to 'schema_' (would consume as declaration)".to_string());
733 "schema_".to_string()
734 } else {
735 name
736 };
737 parts.push(format!("{name} = {rendered}"));
738 }
739 parts.join(", ")
742 }
743
744 fn default_arg(&self, obj: &serde_json::Map<String, JsonValue>) -> Option<String> {
745 match obj.get("default") {
746 Some(JsonValue::String(s)) => Some(format!("default = {}", Emitter::quote_schema(s))),
747 Some(JsonValue::Bool(b)) => Some(format!("default = {b}")),
748 Some(JsonValue::Number(n)) => {
749 if let Some(i) = n.as_i64() {
750 Some(format!("default = {i}"))
751 } else {
752 n.as_f64().map(|f| format!("default = {f}"))
753 }
754 }
755 _ => None,
756 }
757 }
758
759 fn convert_string(&mut self, obj: &serde_json::Map<String, JsonValue>, path: &str) -> String {
760 let mut args = Vec::new();
761 if let Some(n) = obj.get("minLength").and_then(|v| v.as_u64()) {
762 args.push(format!("min_len = {n}"));
763 }
764 if let Some(n) = obj.get("maxLength").and_then(|v| v.as_u64()) {
765 args.push(format!("max_len = {n}"));
766 }
767 if let Some(p) = obj.get("pattern").and_then(|v| v.as_str()) {
768 args.push(format!("pattern = {}", Emitter::quote_schema(p)));
769 }
770 if let Some(members) = obj.get("enum").and_then(|v| v.as_array()) {
771 let mut strs = Vec::new();
772 let mut ok = true;
773 for m in members {
774 match m.as_str() {
775 Some(s) => strs.push(Emitter::quote_schema(s)),
776 None => {
777 ok = false;
778 }
779 }
780 }
781 if ok && !strs.is_empty() {
782 if obj.contains_key("default") {
783 self.note(format!(
787 "default at '{}' dropped: Enum members must be bare string literals",
788 display_path(path)
789 ));
790 }
791 return format!("Enum({})", strs.join(", "));
792 }
793 self.note(format!(
794 "non-string enum at '{}' has no Active counterpart; emitted String",
795 display_path(path)
796 ));
797 }
798 if obj.contains_key("const") {
799 self.note(format!(
800 "const at '{}' has no Active counterpart; emitted String",
801 display_path(path)
802 ));
803 }
804 let mut all = args;
805 all.extend(self.default_arg(obj));
806 if all.is_empty() {
807 "String".to_string()
808 } else {
809 format!("String({})", all.join(", "))
810 }
811 }
812
813 fn int_bound(
814 &mut self,
815 obj: &serde_json::Map<String, JsonValue>,
816 path: &str,
817 key: &str,
818 out_key: &str,
819 ) -> Option<String> {
820 if let Some(n) = obj.get(key).and_then(|v| v.as_i64()) {
821 return Some(format!("{out_key} = {n}"));
822 }
823 if obj.contains_key(key) {
824 self.note(format!(
825 "non-integer '{key}' at '{}' skipped (only integer bounds map)",
826 display_path(path)
827 ));
828 }
829 None
830 }
831
832 fn convert_int(&mut self, obj: &serde_json::Map<String, JsonValue>, path: &str) -> String {
833 let mut args = Vec::new();
834 if let Some(b) = self.int_bound(obj, path, "minimum", "min") {
835 args.push(b);
836 }
837 if let Some(b) = self.int_bound(obj, path, "maximum", "max") {
838 args.push(b);
839 }
840 for key in ["exclusiveMinimum", "exclusiveMaximum"] {
841 if obj.contains_key(key) {
842 self.note(format!(
843 "'{key}' at '{}' imported as inclusive bound (direction loosened, announced)",
844 display_path(path)
845 ));
846 if let Some(n) = obj.get(key).and_then(|v| v.as_i64()) {
847 args.push(format!(
848 "{} = {n}",
849 if key == "exclusiveMinimum" {
850 "min"
851 } else {
852 "max"
853 }
854 ));
855 }
856 }
857 }
858 if obj.contains_key("multipleOf") {
859 self.note(format!(
860 "'multipleOf' at '{}' has no Active counterpart; skipped",
861 display_path(path)
862 ));
863 }
864 if obj.contains_key("const") {
865 self.note(format!(
866 "const at '{}' has no Active counterpart; emitted Int",
867 display_path(path)
868 ));
869 }
870 let mut all = args;
871 all.extend(self.default_arg(obj));
872 if all.is_empty() {
873 "Int".to_string()
874 } else {
875 format!("Int({})", all.join(", "))
876 }
877 }
878
879 fn convert_number(&mut self, obj: &serde_json::Map<String, JsonValue>, path: &str) -> String {
880 let mut args = Vec::new();
881 for (key, out_key) in [("minimum", "min"), ("maximum", "max")] {
882 if let Some(n) = obj.get(key).and_then(|v| v.as_f64()) {
883 args.push(format!("{out_key} = {n}"));
884 }
885 }
886 for key in ["exclusiveMinimum", "exclusiveMaximum"] {
887 if obj.contains_key(key) {
888 self.note(format!(
889 "'{key}' at '{}' imported as inclusive bound (direction loosened, announced)",
890 display_path(path)
891 ));
892 if let Some(n) = obj.get(key).and_then(|v| v.as_f64()) {
893 args.push(format!(
894 "{} = {n}",
895 if key == "exclusiveMinimum" {
896 "min"
897 } else {
898 "max"
899 }
900 ));
901 }
902 }
903 }
904 if obj.contains_key("multipleOf") {
905 self.note(format!(
906 "'multipleOf' at '{}' has no Active counterpart; skipped",
907 display_path(path)
908 ));
909 }
910 let mut all = args;
911 all.extend(self.default_arg(obj));
912 if all.is_empty() {
913 "Float".to_string()
914 } else {
915 format!("Float({})", all.join(", "))
916 }
917 }
918
919 fn convert_array(&mut self, obj: &serde_json::Map<String, JsonValue>, path: &str) -> String {
920 for key in ["minItems", "maxItems"] {
921 if obj.contains_key(key) {
922 self.note(format!(
923 "'{key}' at '{}' has no Active counterpart; skipped",
924 display_path(path)
925 ));
926 }
927 }
928 match obj.get("items") {
929 Some(items) => format!("Array<{}>", self.convert_node(items, &format!("{path}[]"))),
930 None => "Array<Any>".to_string(),
931 }
932 }
933}
934
935fn kind_word(v: &JsonValue) -> &'static str {
936 match v {
937 JsonValue::Null => "null",
938 JsonValue::Bool(_) => "boolean",
939 JsonValue::Number(_) => "number",
940 JsonValue::String(_) => "string",
941 JsonValue::Array(_) => "array",
942 JsonValue::Object(_) => "object",
943 }
944}
945
946fn empty_map() -> serde_json::Map<String, JsonValue> {
947 serde_json::Map::new()
948}