1use serde_json::Value;
8use yaml_rust2::{Yaml, YamlLoader};
9
10const KEY_ORDER: &[&str] = &[
17 "openapi",
19 "asyncapi",
20 "info",
21 "name",
24 "title",
25 "version",
26 "description",
27 "servers",
28 "url",
29 "host",
30 "protocol",
31 "paths",
33 "operationId",
34 "summary",
35 "tags",
36 "parameters",
37 "in",
38 "requestBody",
39 "required",
40 "content",
41 "responses",
42 "channels",
44 "address",
45 "action",
46 "channel",
47 "messages",
48 "operations",
49 "payload",
50 "tools",
52 "inputSchema",
53 "outputSchema",
54 "schema",
56 "$ref",
57 "type",
58 "format",
59 "properties",
60 "items",
61 "prefixItems",
62 "oneOf",
63 "allOf",
64 "const",
65 "enum",
66 "additionalProperties",
67 "components",
69 "schemas",
70 "$defs",
71];
72
73const NAME_KEYED: &[&str] = &[
82 "properties",
83 "$defs",
84 "schemas",
85 "definitions",
86 "paths",
87 "responses",
88 "content",
89 "channels",
90 "operations",
91 "messages",
92 "servers",
93 "parameters",
94 "types",
95 "roots",
96 "fields",
97 "args",
98];
99
100fn key_rank(key: &str, value: &Value) -> usize {
107 let position = |name: &str| {
108 KEY_ORDER
109 .iter()
110 .position(|k| *k == name)
111 .unwrap_or(KEY_ORDER.len())
112 };
113 if key == "required" && value.is_array() {
114 return position("properties") * 2 + 1;
115 }
116 position(key) * 2
117}
118
119fn ordered_keys(o: &serde_json::Map<String, Value>, data_keyed: bool) -> Vec<&String> {
124 let mut keys: Vec<&String> = o.keys().collect();
125 if data_keyed {
126 keys.sort();
127 } else {
128 keys.sort_by_key(|k| (key_rank(k, &o[*k]), (*k).clone()));
129 }
130 keys
131}
132
133pub fn to_yaml(value: &Value) -> String {
135 let mut out = String::new();
136 let yaml = to_yaml_value(value, false);
137 let mut emitter = yaml_rust2::YamlEmitter::new(&mut out);
138 if emitter.dump(&yaml).is_err() {
139 return to_json(value);
141 }
142 let body = out.strip_prefix("---\n").unwrap_or(&out).to_string();
144 format!("{body}\n")
145}
146
147pub fn to_json(value: &Value) -> String {
154 let mut out = String::new();
155 write_json(&mut out, value, 0, false);
156 out.push('\n');
157 out
158}
159
160fn write_json(out: &mut String, value: &Value, depth: usize, data_keyed: bool) {
161 match value {
162 Value::Object(o) if !o.is_empty() => {
163 out.push_str("{\n");
164 let keys = ordered_keys(o, data_keyed);
165 for (i, key) in keys.iter().enumerate() {
166 indent(out, depth + 1);
167 out.push_str(&Value::String((*key).clone()).to_string());
168 out.push_str(": ");
169 write_json(out, &o[*key], depth + 1, NAME_KEYED.contains(&key.as_str()));
170 if i + 1 < keys.len() {
171 out.push(',');
172 }
173 out.push('\n');
174 }
175 indent(out, depth);
176 out.push('}');
177 }
178 Value::Array(a) if !a.is_empty() => {
179 out.push_str("[\n");
180 for (i, item) in a.iter().enumerate() {
181 indent(out, depth + 1);
182 write_json(out, item, depth + 1, false);
183 if i + 1 < a.len() {
184 out.push(',');
185 }
186 out.push('\n');
187 }
188 indent(out, depth);
189 out.push(']');
190 }
191 other => out.push_str(&other.to_string()),
194 }
195}
196
197fn indent(out: &mut String, depth: usize) {
198 out.extend(std::iter::repeat_n(' ', depth * 2));
199}
200
201fn to_yaml_value(value: &Value, data_keyed: bool) -> yaml_rust2::Yaml {
202 use yaml_rust2::Yaml;
203 match value {
204 Value::Null => Yaml::Null,
205 Value::Bool(b) => Yaml::Boolean(*b),
206 Value::Number(n) => match n.as_i64() {
207 Some(i) => Yaml::Integer(i),
208 None => Yaml::Real(n.to_string()),
209 },
210 Value::String(s) => Yaml::String(s.clone()),
211 Value::Array(a) => Yaml::Array(a.iter().map(|v| to_yaml_value(v, false)).collect()),
212 Value::Object(o) => {
213 let mut hash = yaml_rust2::yaml::Hash::new();
214 for k in ordered_keys(o, data_keyed) {
215 hash.insert(
216 Yaml::String(k.clone()),
217 to_yaml_value(&o[k], NAME_KEYED.contains(&k.as_str())),
218 );
219 }
220 Yaml::Hash(hash)
221 }
222 }
223}
224
225pub fn from_text(text: &str) -> Result<Value, String> {
228 if let Ok(v) = serde_json::from_str::<Value>(text) {
229 return Ok(v);
230 }
231 let docs = YamlLoader::load_from_str(text).map_err(|e| format!("YAML parse error: {e}"))?;
232 docs.into_iter()
233 .next()
234 .map(|y| yaml_to_json(&y))
235 .ok_or_else(|| "empty spec document".to_string())
236}
237
238fn yaml_to_json(y: &Yaml) -> Value {
239 match y {
240 Yaml::Real(s) => s
241 .parse::<f64>()
242 .map(|f| serde_json::json!(f))
243 .unwrap_or(Value::Null),
244 Yaml::Integer(i) => serde_json::json!(i),
245 Yaml::String(s) => Value::String(s.clone()),
246 Yaml::Boolean(b) => Value::Bool(*b),
247 Yaml::Array(a) => Value::Array(a.iter().map(yaml_to_json).collect()),
248 Yaml::Hash(h) => {
249 let mut map = serde_json::Map::new();
250 for (k, v) in h {
251 if let Yaml::String(key) = k {
252 map.insert(key.clone(), yaml_to_json(v));
253 }
254 }
255 Value::Object(map)
256 }
257 _ => Value::Null,
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use serde_json::json;
265
266 #[test]
267 fn the_dialect_marker_leads_the_document() {
268 let doc = json!({ "components": {}, "info": { "version": "1", "title": "T" },
269 "asyncapi": "3.0.0" });
270 let yaml = to_yaml(&doc);
271 assert!(yaml.starts_with("asyncapi: 3.0.0"), "got:\n{yaml}");
272 let title = yaml.find("title:").expect("title");
273 let version = yaml.find("version:").expect("version");
274 assert!(title < version, "title reads before version:\n{yaml}");
275 }
276
277 #[test]
278 fn rendering_is_byte_identical_between_runs() {
279 let doc = json!({ "tools": [ { "inputSchema": {}, "name": "a" } ] });
280 assert_eq!(to_yaml(&doc), to_yaml(&doc));
281 assert_eq!(to_json(&doc), to_json(&doc));
282 }
283
284 #[test]
285 fn required_reads_before_a_parameter_but_after_schema_properties() {
286 let doc = json!({
287 "parameters": [ { "schema": { "type": "integer" }, "required": true, "name": "id" } ],
288 "components": { "schemas": { "Item": {
289 "required": ["id"],
290 "properties": { "id": { "type": "integer" } },
291 "type": "object" } } },
292 });
293 let yaml = to_yaml(&doc);
294 let param_required = yaml.find("required: true").expect("parameter required");
295 let param_schema = yaml.find("schema:").expect("parameter schema");
296 assert!(param_required < param_schema, "got:\n{yaml}");
297
298 let props = yaml.find("properties:").expect("schema properties");
299 let list = yaml.find("required:\n").expect("schema required list");
300 assert!(props < list, "got:\n{yaml}");
301 }
302
303 #[test]
304 fn json_output_ends_with_a_newline() {
305 let out = to_json(&json!({ "openapi": "3.1.0" }));
306 assert!(out.contains("\"openapi\": \"3.1.0\""), "got {out}");
307 assert!(out.ends_with('\n'));
308 }
309
310 #[test]
311 fn json_reads_in_the_same_order_as_yaml() {
312 let doc = json!({ "tools": [ { "description": "d", "inputSchema": {}, "name": "a" } ] });
313 let out = to_json(&doc);
314 let at = |needle: &str| {
315 out.find(needle)
316 .unwrap_or_else(|| panic!("{needle}\n{out}"))
317 };
318 assert!(at("\"name\"") < at("\"description\""), "got:\n{out}");
319 assert!(at("\"description\"") < at("\"inputSchema\""), "got:\n{out}");
320 }
321
322 #[test]
323 fn json_escapes_and_nests_exactly_as_serde_would() {
324 let doc = json!({ "a": { "quote\"key": "line\nbreak \u{1f600}" },
325 "b": [1, -2.5, true, null, [], {}] });
326 let out = to_json(&doc);
327 let round_tripped: Value = serde_json::from_str(&out).expect("valid JSON");
328 assert_eq!(round_tripped, doc);
329 }
330
331 #[test]
332 fn a_field_named_after_a_spec_keyword_is_not_hoisted() {
333 let doc = json!({ "components": { "schemas": { "Item": {
336 "type": "object",
337 "properties": { "tags": { "type": "array" },
338 "id": { "type": "string" },
339 "name": { "type": "string" } },
340 } } } });
341 for out in [to_yaml(&doc), to_json(&doc)] {
342 let at = |needle: &str| {
343 out.find(needle)
344 .unwrap_or_else(|| panic!("{needle}\n{out}"))
345 };
346 assert!(at("id") < at("name"), "alphabetical, got:\n{out}");
347 assert!(at("name") < at("tags"), "alphabetical, got:\n{out}");
348 }
349 }
350
351 #[test]
352 fn keyword_ordering_still_applies_inside_each_schema_body() {
353 let doc = json!({ "properties": { "id": { "properties": {}, "type": "object" } } });
356 let yaml = to_yaml(&doc);
357 assert!(
358 yaml.find("type:").expect("type") < yaml.find("properties: {}").expect("props"),
359 "got:\n{yaml}"
360 );
361 }
362
363 #[test]
364 fn empty_containers_stay_on_one_line() {
365 let out = to_json(&json!({ "properties": {}, "servers": [] }));
366 assert!(out.contains("\"properties\": {}"), "got:\n{out}");
367 assert!(out.contains("\"servers\": []"), "got:\n{out}");
368 }
369
370 #[test]
371 fn documents_carry_no_yaml_document_marker() {
372 assert!(!to_yaml(&json!({ "a": 1 })).contains("---"));
373 }
374}