1use std::path::Path;
17
18use schemars::generate::SchemaSettings;
19use serde_json::{Map, Value, json};
20
21pub const SCHEMA_PATH: &str = "site/public/schemas/bynk.toml.json";
23
24pub const SCHEMA_URL: &str = "https://bynk-lang.org/schemas/bynk.toml.json";
28
29const DOCS_URL: &str = "https://bynk-lang.org/docs/manifest/";
31
32pub fn generate() -> Value {
34 let mut generator = SchemaSettings::draft07()
38 .with(|s| s.inline_subschemas = true)
39 .into_generator();
40 let mut properties = Map::new();
41 let tables = bynk_project::MANIFEST_TABLES
42 .iter()
43 .chain(bynk_project::MANIFEST_MAP_TABLES);
44 for (table, _) in tables {
45 let schema = match *table {
46 "fmt" => bynk_fmt::fmt_table_schema(&mut generator),
47 other => bynk_project::manifest_schema::table_schema(other, &mut generator)
48 .unwrap_or_else(|| panic!("`[{other}]` is in MANIFEST_TABLES but has no schema")),
49 };
50 let mut schema = without_null(schema.to_value());
51 if let Value::Object(fields) = &mut schema {
52 fields.remove("title");
54 fields.insert(
55 "x-taplo".into(),
56 json!({ "links": { "key": format!("{DOCS_URL}#{table}") } }),
57 );
58 }
59 properties.insert((*table).to_string(), schema);
60 }
61 let root = json!({
62 "$schema": "http://json-schema.org/draft-07/schema#",
63 "$id": SCHEMA_URL,
64 "title": "bynk.toml",
65 "description": "The Bynk project manifest. Every table and key is optional. \
66 Generated from the toolchain's manifest types by `cargo xtask manifest-schema`.",
67 "type": "object",
68 "additionalProperties": false,
69 "properties": properties,
70 "x-taplo": { "links": { "key": DOCS_URL } },
71 });
72 sorted(root)
73}
74
75pub fn render() -> String {
77 let mut text = serde_json::to_string_pretty(&generate()).expect("a JSON value serialises");
78 text.push('\n');
79 text
80}
81
82pub fn write(root: &Path) -> std::io::Result<()> {
84 let path = root.join(SCHEMA_PATH);
85 if let Some(dir) = path.parent() {
86 std::fs::create_dir_all(dir)?;
87 }
88 std::fs::write(path, render())
89}
90
91fn without_null(schema: Value) -> Value {
97 let Value::Object(mut fields) = schema else {
98 return match schema {
99 Value::Array(items) => Value::Array(items.into_iter().map(without_null).collect()),
100 other => other,
101 };
102 };
103 if let Some(Value::Array(types)) = fields.get_mut("type") {
104 types.retain(|t| t != "null");
105 if types.len() == 1 {
106 let only = types.remove(0);
107 fields.insert("type".into(), only);
108 }
109 }
110 if let Some(Value::Array(branches)) = fields.get_mut("anyOf") {
111 branches.retain(|b| b != &json!({ "type": "null" }));
112 if let [Value::Object(branch)] = branches.as_slice() {
115 let branch = branch.clone();
116 fields.remove("anyOf");
117 for (key, value) in branch {
118 fields.entry(key).or_insert(value);
119 }
120 }
121 }
122 Value::Object(
123 fields
124 .into_iter()
125 .map(|(k, v)| (k, without_null(v)))
126 .collect(),
127 )
128}
129
130fn sorted(value: Value) -> Value {
133 match value {
134 Value::Object(map) => {
135 let mut entries: Vec<(String, Value)> = map.into_iter().collect();
136 entries.sort_by(|a, b| a.0.cmp(&b.0));
137 Value::Object(entries.into_iter().map(|(k, v)| (k, sorted(v))).collect())
138 }
139 Value::Array(items) => Value::Array(items.into_iter().map(sorted).collect()),
140 other => other,
141 }
142}
143
144#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn without_null_drops_null_from_a_type_list() {
150 assert_eq!(
151 without_null(json!({ "type": ["string", "null"] })),
152 json!({ "type": "string" })
153 );
154 assert_eq!(
155 without_null(json!({ "type": ["string", "integer", "null"] })),
156 json!({ "type": ["string", "integer"] })
157 );
158 }
159
160 #[test]
161 fn without_null_folds_a_lone_object_branch_keeping_the_parents_fields() {
162 let folded = without_null(json!({
163 "description": "outer",
164 "anyOf": [{ "type": "string", "description": "inner" }, { "type": "null" }],
165 }));
166 assert_eq!(folded, json!({ "description": "outer", "type": "string" }));
167 }
168
169 #[test]
170 fn without_null_keeps_an_any_of_it_cannot_fold() {
171 let two = json!({ "anyOf": [{ "type": "string" }, { "type": "integer" }] });
173 assert_eq!(without_null(two.clone()), two);
174 assert_eq!(
176 without_null(json!({ "anyOf": [true, { "type": "null" }] })),
177 json!({ "anyOf": [true] })
178 );
179 }
180}