1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! The spec reference, `docs/spec.md`: every field of `isoloom.yml` read from its JSON schema
//! (type, default, allowed values, limits, description, examples). The schema is the code
//! itself, so the page can't drift: a test fails when the committed page differs from what
//! `isoloom docs --spec` prints.
mod markdown;
mod text;
use serde_json::{Map, Value};
pub use markdown::{markdown, slug};
/// Fields whose own fields repeat a machine's: listed by name only, since the machine's
/// section documents them.
const SHALLOW: &[&str] = &["common", "groups"];
/// How deep the walk goes, a guard against a schema that refers to itself.
const MAX_DEPTH: usize = 10;
/// One field of the spec as the reference shows it.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Field {
/// Its path: `*` for a name the author chooses, `[]` for the items of a list
/// (`machines.*.ports[].port`).
pub path: String,
/// Its type in words (`string`, `list of mappings`, `map of name to integer`).
pub kind: String,
/// What it is for, from the model's doc comment.
pub description: String,
/// Its default value as JSON, when the schema states one worth showing.
pub default: Option<String>,
/// The values it accepts, each with what it means, when it is a closed set.
pub allowed: Vec<(String, String)>,
/// Ranges and patterns the schema enforces beyond the type.
pub limits: Vec<String>,
/// Examples the schema carries.
pub examples: Vec<String>,
/// For a shallow field (`common`, `groups`), the names of the fields it holds.
pub members: Vec<String>,
}
/// Every field of `schema` (the output of [`crate::schema::schema`]), in the schema's
/// order: a field, then the fields inside it.
pub fn fields(schema: &Value) -> Vec<Field> {
let empty = Map::new();
let walker = Walker {
defs: schema.get("$defs").and_then(Value::as_object).unwrap_or(&empty),
};
let mut out = Vec::new();
walker.children(schema, "", 0, &mut out);
out
}
/// Walks the schema, following `$ref`s into its definitions.
struct Walker<'a> {
defs: &'a Map<String, Value>,
}
impl<'a> Walker<'a> {
/// The node a `$ref` points to (followed until there is none), or the node itself.
fn resolve(&self, node: &'a Value) -> &'a Value {
let mut node = node;
for _ in 0..MAX_DEPTH {
let Some(name) = node.get("$ref").and_then(Value::as_str).and_then(|r| r.strip_prefix("#/$defs/")) else {
break;
};
match self.defs.get(name) {
Some(next) => node = next,
None => break,
}
}
node
}
/// The property maps `node` can hold: its own, or each of its alternatives' (a field that
/// is a scalar or a mapping, like `gpus`).
fn objects(&self, node: &'a Value, depth: usize) -> Vec<&'a Map<String, Value>> {
let node = self.resolve(node);
if let Some(p) = node.get("properties").and_then(Value::as_object) {
return vec![p];
}
if depth >= MAX_DEPTH {
return Vec::new();
}
text::variants(node).iter().flat_map(|v| self.objects(v, depth + 1)).collect()
}
/// The fields inside `node`, at `path`: its properties, the properties of what its map
/// holds (at `path.*`), or of its list's items (at `path[]`).
fn children(&self, node: &'a Value, path: &str, depth: usize, out: &mut Vec<Field>) {
if depth >= MAX_DEPTH {
return;
}
let node = self.resolve(node);
let (inner, at) = match (text::map_value(node), node.get("items")) {
(Some(value), _) => (value, format!("{path}.*")),
(_, Some(items)) => (items, format!("{path}[]")),
_ => (node, path.to_owned()),
};
for props in self.objects(inner, depth) {
for (name, child) in props {
let child_path = if at.is_empty() { name.clone() } else { format!("{at}.{name}") };
if out.iter().any(|f| f.path == child_path) {
continue;
}
self.field(child, &child_path, depth + 1, out);
}
}
}
/// The field at `path`, then the fields inside it.
fn field(&self, node: &'a Value, path: &str, depth: usize, out: &mut Vec<Field>) {
let target = self.resolve(node);
let shallow = SHALLOW.contains(&path);
let pick = |key: &str| node.get(key).or_else(|| target.get(key));
let description = pick("description").and_then(Value::as_str).map(text::prose).unwrap_or_default();
let mut allowed = text::allowed(self, node);
// A single value whose meaning is the field's own description says it once.
if let [(_, meaning)] = allowed.as_mut_slice()
&& *meaning == description
{
meaning.clear();
}
out.push(Field {
kind: text::kind(self, node, depth),
description,
default: pick("default").and_then(text::default),
allowed,
limits: text::limits(self, node),
examples: pick("examples")
.and_then(Value::as_array)
.map(|e| e.iter().map(text::json).collect())
.unwrap_or_default(),
members: if shallow { self.member_names(node, depth) } else { Vec::new() },
path: path.to_owned(),
});
if !shallow {
self.children(node, path, depth, out);
}
}
/// The names of the fields a shallow field holds (in its map's values, for `groups`).
fn member_names(&self, node: &'a Value, depth: usize) -> Vec<String> {
let node = self.resolve(node);
let inner = text::map_value(node).unwrap_or(node);
self.objects(inner, depth).into_iter().flat_map(|p| p.keys().cloned()).collect()
}
}
#[cfg(test)]
mod tests;