semiont_codegen/
bundle.rs1use serde_json::{Map, Value, json};
7use std::collections::BTreeMap;
8use std::fs;
9use std::path::{Path, PathBuf};
10
11pub fn read_json(path: &Path) -> Value {
12 let text =
13 fs::read_to_string(path).unwrap_or_else(|e| panic!("cannot read {}: {e}", path.display()));
14 serde_json::from_str(&text).unwrap_or_else(|e| panic!("{} is not JSON: {e}", path.display()))
15}
16
17pub fn write_json(path: &Path, value: &Value) {
18 fs::write(
19 path,
20 serde_json::to_vec(value).expect("a JSON value serializes"),
21 )
22 .unwrap_or_else(|e| panic!("cannot write {}: {e}", path.display()));
23}
24
25pub struct Bundle {
27 named: BTreeMap<PathBuf, (String, String)>,
29 components: BTreeMap<String, Map<String, Value>>,
31}
32
33impl Bundle {
34 pub fn of(root_file: &Path) -> Value {
35 let root_file = root_file
36 .canonicalize()
37 .unwrap_or_else(|e| panic!("{}: {e}", root_file.display()));
38 let root = read_json(&root_file);
39 let base = root_file
40 .parent()
41 .expect("a file has a directory")
42 .to_path_buf();
43 let mut bundle = Bundle {
44 named: BTreeMap::new(),
45 components: BTreeMap::new(),
46 };
47
48 let declared = root
49 .get("components")
50 .and_then(Value::as_object)
51 .cloned()
52 .unwrap_or_default();
53 for (kind, entries) in &declared {
54 let Some(entries) = entries.as_object() else {
55 continue;
56 };
57 for (name, entry) in entries {
58 if let Some(file) = entry.get("$ref").and_then(Value::as_str) {
59 bundle.name(&base.join(file), kind, name);
60 }
61 }
62 }
63 let mut document = root.clone();
64 for (kind, entries) in &declared {
65 let Some(entries) = entries.as_object() else {
66 continue;
67 };
68 for (name, entry) in entries {
69 let resolved = match entry.get("$ref").and_then(Value::as_str) {
70 Some(file) => bundle.load(&base.join(file)),
71 None => {
72 let mut inline = entry.clone();
73 bundle.resolve(&mut inline, &base);
74 inline
75 }
76 };
77 bundle
78 .components
79 .entry(kind.clone())
80 .or_default()
81 .insert(name.clone(), resolved);
82 }
83 }
84 if let Some(object) = document.as_object_mut() {
85 object.remove("components");
86 for (key, value) in object.iter_mut() {
87 if key != "components" {
88 bundle.resolve(value, &base);
89 }
90 }
91 }
92 let components: Map<String, Value> = bundle
93 .components
94 .into_iter()
95 .map(|(kind, entries)| (kind, Value::Object(entries)))
96 .collect();
97 document["components"] = Value::Object(components);
98 document
99 }
100
101 fn name(&mut self, file: &Path, kind: &str, name: &str) {
102 let file = file
103 .canonicalize()
104 .unwrap_or_else(|e| panic!("{}: {e}", file.display()));
105 if let Some((k, n)) = self.named.get(&file)
106 && (k.as_str(), n.as_str()) != (kind, name)
107 {
108 panic!("{} is named both {k}/{n} and {kind}/{name}", file.display());
109 }
110 self.named.insert(file, (kind.to_owned(), name.to_owned()));
111 }
112
113 fn load(&mut self, file: &Path) -> Value {
115 let file = file
116 .canonicalize()
117 .unwrap_or_else(|e| panic!("{}: {e}", file.display()));
118 let mut value = read_json(&file);
119 let base = file.parent().expect("a file has a directory").to_path_buf();
120 self.resolve(&mut value, &base);
121 value
122 }
123
124 fn resolve(&mut self, node: &mut Value, base: &Path) {
125 match node {
126 Value::Array(items) => items.iter_mut().for_each(|item| self.resolve(item, base)),
127 Value::Object(object) => {
128 if let Some(reference) = object
129 .get("$ref")
130 .and_then(Value::as_str)
131 .map(str::to_owned)
132 {
133 let (file, pointer) = reference
134 .split_once('#')
135 .unwrap_or((reference.as_str(), ""));
136 if file.is_empty() {
137 panic!(
138 "an internal $ref {reference} in a file under {}: specs/src refers by file",
139 base.display()
140 );
141 }
142 if !pointer.is_empty() {
143 panic!(
144 "$ref {reference} under {} points into a file: specs/src refers to whole files",
145 base.display()
146 );
147 }
148 let target = base.join(file).canonicalize().unwrap_or_else(|e| {
149 panic!("$ref {reference} under {}: {e}", base.display())
150 });
151 let named = self.named.get(&target).cloned().or_else(|| {
152 let kind = target.parent()?.file_name()?.to_str()?.to_owned();
153 let name = target.file_stem()?.to_str()?.to_owned();
154 (kind == "schemas" || kind == "responses").then_some((kind, name))
155 });
156 match named {
157 Some((kind, name)) => {
158 if !self.named.contains_key(&target) {
159 self.name(&target, &kind, &name);
160 let resolved = self.load(&target);
161 self.components
162 .entry(kind.clone())
163 .or_default()
164 .insert(name.clone(), resolved);
165 }
166 *node = json!({ "$ref": format!("#/components/{kind}/{name}") });
167 }
168 None => *node = self.load(&target),
169 }
170 return;
171 }
172 object
173 .values_mut()
174 .for_each(|value| self.resolve(value, base));
175 }
176 _ => {}
177 }
178 }
179}
180
181pub fn draft7_definitions(document: &Value) -> Value {
185 let mut schemas = document["components"]["schemas"].clone();
186 fn convert(node: &mut Value) {
187 match node {
188 Value::Array(items) => items.iter_mut().for_each(convert),
189 Value::Object(object) => {
190 if let Some(Value::String(reference)) = object.get_mut("$ref")
191 && let Some(name) = reference.strip_prefix("#/components/schemas/")
192 {
193 *reference = format!("#/definitions/{name}");
194 }
195 if object.get("nullable") == Some(&Value::Bool(true)) {
196 object.remove("nullable");
197 match object.get("type").cloned() {
198 Some(Value::String(kind)) => {
199 object.insert("type".into(), json!([kind, "null"]));
200 }
201 _ => {
202 let inner = Value::Object(std::mem::take(object));
203 object.insert("anyOf".into(), json!([{ "type": "null" }, inner]));
204 }
205 }
206 }
207 object.values_mut().for_each(convert);
208 }
209 _ => {}
210 }
211 }
212 convert(&mut schemas);
213 json!({ "$schema": "http://json-schema.org/draft-07/schema#", "definitions": schemas })
214}