isoloom_spec/
reference.rs1mod markdown;
7mod text;
8
9use serde_json::{Map, Value};
10
11pub use markdown::{markdown, slug};
12
13const SHALLOW: &[&str] = &["common", "groups"];
16
17const MAX_DEPTH: usize = 10;
19
20#[derive(Debug, Clone, Default, PartialEq, Eq)]
22pub struct Field {
23 pub path: String,
26 pub kind: String,
28 pub description: String,
30 pub default: Option<String>,
32 pub allowed: Vec<(String, String)>,
34 pub limits: Vec<String>,
36 pub examples: Vec<String>,
38 pub members: Vec<String>,
40}
41
42pub fn fields(schema: &Value) -> Vec<Field> {
45 let empty = Map::new();
46 let walker = Walker {
47 defs: schema.get("$defs").and_then(Value::as_object).unwrap_or(&empty),
48 };
49 let mut out = Vec::new();
50 walker.children(schema, "", 0, &mut out);
51 out
52}
53
54struct Walker<'a> {
56 defs: &'a Map<String, Value>,
57}
58
59impl<'a> Walker<'a> {
60 fn resolve(&self, node: &'a Value) -> &'a Value {
62 let mut node = node;
63 for _ in 0..MAX_DEPTH {
64 let Some(name) = node.get("$ref").and_then(Value::as_str).and_then(|r| r.strip_prefix("#/$defs/")) else {
65 break;
66 };
67 match self.defs.get(name) {
68 Some(next) => node = next,
69 None => break,
70 }
71 }
72 node
73 }
74
75 fn objects(&self, node: &'a Value, depth: usize) -> Vec<&'a Map<String, Value>> {
78 let node = self.resolve(node);
79 if let Some(p) = node.get("properties").and_then(Value::as_object) {
80 return vec![p];
81 }
82 if depth >= MAX_DEPTH {
83 return Vec::new();
84 }
85 text::variants(node).iter().flat_map(|v| self.objects(v, depth + 1)).collect()
86 }
87
88 fn children(&self, node: &'a Value, path: &str, depth: usize, out: &mut Vec<Field>) {
91 if depth >= MAX_DEPTH {
92 return;
93 }
94 let node = self.resolve(node);
95 let (inner, at) = match (text::map_value(node), node.get("items")) {
96 (Some(value), _) => (value, format!("{path}.*")),
97 (_, Some(items)) => (items, format!("{path}[]")),
98 _ => (node, path.to_owned()),
99 };
100 for props in self.objects(inner, depth) {
101 for (name, child) in props {
102 let child_path = if at.is_empty() { name.clone() } else { format!("{at}.{name}") };
103 if out.iter().any(|f| f.path == child_path) {
104 continue;
105 }
106 self.field(child, &child_path, depth + 1, out);
107 }
108 }
109 }
110
111 fn field(&self, node: &'a Value, path: &str, depth: usize, out: &mut Vec<Field>) {
113 let target = self.resolve(node);
114 let shallow = SHALLOW.contains(&path);
115 let pick = |key: &str| node.get(key).or_else(|| target.get(key));
116 let description = pick("description").and_then(Value::as_str).map(text::prose).unwrap_or_default();
117 let mut allowed = text::allowed(self, node);
118 if let [(_, meaning)] = allowed.as_mut_slice()
120 && *meaning == description
121 {
122 meaning.clear();
123 }
124 out.push(Field {
125 kind: text::kind(self, node, depth),
126 description,
127 default: pick("default").and_then(text::default),
128 allowed,
129 limits: text::limits(self, node),
130 examples: pick("examples")
131 .and_then(Value::as_array)
132 .map(|e| e.iter().map(text::json).collect())
133 .unwrap_or_default(),
134 members: if shallow { self.member_names(node, depth) } else { Vec::new() },
135 path: path.to_owned(),
136 });
137 if !shallow {
138 self.children(node, path, depth, out);
139 }
140 }
141
142 fn member_names(&self, node: &'a Value, depth: usize) -> Vec<String> {
144 let node = self.resolve(node);
145 let inner = text::map_value(node).unwrap_or(node);
146 self.objects(inner, depth).into_iter().flat_map(|p| p.keys().cloned()).collect()
147 }
148}
149
150#[cfg(test)]
151mod tests;