| 51 |
51 |
|
/// Documentation for a request body schema, flattened to dotted paths: |
| 52 |
52 |
|
/// `owner.name`, `pets[].tag`. A body that isn't an object gets a single row. |
| 53 |
53 |
|
pub fn body_docs(doc: &Value, schema: &Value) -> Vec<FieldDoc> { |
|
54 |
+ |
schema_docs(doc, schema, "body") |
|
55 |
+ |
} |
|
56 |
+ |
|
|
57 |
+ |
/// Same flattening as [`body_docs`], for a success response schema. Rows land |
|
58 |
+ |
/// under `location: "response"` so the Docs tab lists them separately. |
|
59 |
+ |
pub fn response_docs(doc: &Value, schema: &Value) -> Vec<FieldDoc> { |
|
60 |
+ |
schema_docs(doc, schema, "response") |
|
61 |
+ |
} |
|
62 |
+ |
|
|
63 |
+ |
/// Flatten `schema` into dotted-path rows tagged with `location`. A schema that |
|
64 |
+ |
/// isn't an object gets a single `(location)` row. |
|
65 |
+ |
fn schema_docs(doc: &Value, schema: &Value, location: &str) -> Vec<FieldDoc> { |
| 54 |
66 |
|
let mut out = Vec::new(); |
| 55 |
|
- |
flatten(doc, schema, "", 0, &mut out); |
|
67 |
+ |
flatten(doc, schema, "", 0, location, &mut out); |
| 56 |
68 |
|
if out.is_empty() { |
| 57 |
69 |
|
let mut field = field_doc(doc, schema); |
| 58 |
70 |
|
if !field.ty.is_empty() && field.ty != "object" { |
| 59 |
|
- |
field.name = "(body)".into(); |
| 60 |
|
- |
field.location = "body".into(); |
|
71 |
+ |
field.name = format!("({location})"); |
|
72 |
+ |
field.location = location.into(); |
| 61 |
73 |
|
out.push(field); |
| 62 |
74 |
|
} |
| 63 |
75 |
|
} |
| 64 |
76 |
|
out |
| 65 |
77 |
|
} |
| 66 |
78 |
|
|
| 67 |
|
- |
fn flatten(doc: &Value, schema: &Value, prefix: &str, depth: usize, out: &mut Vec<FieldDoc>) { |
|
79 |
+ |
fn flatten( |
|
80 |
+ |
doc: &Value, |
|
81 |
+ |
schema: &Value, |
|
82 |
+ |
prefix: &str, |
|
83 |
+ |
depth: usize, |
|
84 |
+ |
location: &str, |
|
85 |
+ |
out: &mut Vec<FieldDoc>, |
|
86 |
+ |
) { |
| 68 |
87 |
|
if depth > MAX_BODY_DEPTH || out.len() >= MAX_BODY_FIELDS { |
| 69 |
88 |
|
return; |
| 70 |
89 |
|
} |
|
| 73 |
92 |
|
// An array contributes no fields of its own; describe its items under |
| 74 |
93 |
|
// `name[]` so the path reads like the JSON it documents. |
| 75 |
94 |
|
if let Some(items) = schema.get("items") { |
| 76 |
|
- |
flatten(doc, items, &format!("{prefix}[]"), depth + 1, out); |
|
95 |
+ |
flatten(doc, items, &format!("{prefix}[]"), depth + 1, location, out); |
| 77 |
96 |
|
return; |
| 78 |
97 |
|
} |
| 79 |
98 |
|
|
|
| 98 |
117 |
|
}; |
| 99 |
118 |
|
let mut field = field_doc(doc, sub); |
| 100 |
119 |
|
field.name = path.clone(); |
| 101 |
|
- |
field.location = "body".into(); |
|
120 |
+ |
field.location = location.into(); |
| 102 |
121 |
|
field.required = required.contains(name.as_str()); |
| 103 |
122 |
|
out.push(field); |
| 104 |
123 |
|
// Scalars fall straight back out of this call. |
| 105 |
|
- |
flatten(doc, sub, &path, depth + 1, out); |
|
124 |
+ |
flatten(doc, sub, &path, depth + 1, location, out); |
| 106 |
125 |
|
} |
| 107 |
126 |
|
} |
| 108 |
127 |
|
} |
| 109 |
128 |
|
|
| 110 |
|
- |
/// Schemas contributing properties to `schema`: itself, plus `allOf` members, |
| 111 |
|
- |
/// which OpenAPI uses for composition/inheritance. |
|
129 |
+ |
/// Schemas contributing properties to `schema`: itself, plus the members of any |
|
130 |
+ |
/// `allOf`/`oneOf`/`anyOf`. `allOf` is composition; `oneOf`/`anyOf` are |
|
131 |
+ |
/// alternatives, but the Docs tab merges every branch's fields flat so nested |
|
132 |
+ |
/// objects behind a union still show up. |
| 112 |
133 |
|
fn object_parts<'a>(doc: &'a Value, schema: &'a Value) -> Vec<&'a Value> { |
| 113 |
134 |
|
let mut parts = vec![schema]; |
| 114 |
|
- |
if let Some(all) = schema.get("allOf").and_then(Value::as_array) { |
| 115 |
|
- |
parts.extend(all.iter().map(|s| deref(doc, s))); |
|
135 |
+ |
for key in ["allOf", "oneOf", "anyOf"] { |
|
136 |
+ |
if let Some(members) = schema.get(key).and_then(Value::as_array) { |
|
137 |
+ |
parts.extend(members.iter().map(|s| deref(doc, s))); |
|
138 |
+ |
} |
| 116 |
139 |
|
} |
| 117 |
140 |
|
parts |
| 118 |
141 |
|
} |
|
| 332 |
355 |
|
let names: Vec<&str> = docs.iter().map(|d| d.name.as_str()).collect(); |
| 333 |
356 |
|
assert_eq!(names, ["id", "note"]); |
| 334 |
357 |
|
assert!(docs[0].required); |
|
358 |
+ |
} |
|
359 |
+ |
|
|
360 |
+ |
#[test] |
|
361 |
+ |
fn one_of_object_variants_contribute_nested_fields() { |
|
362 |
+ |
let doc = json!({}); |
|
363 |
+ |
// A property whose value is a union of two object shapes: both branches' |
|
364 |
+ |
// fields flatten under the property path. |
|
365 |
+ |
let schema = json!({"type": "object", "properties": { |
|
366 |
+ |
"payment": {"oneOf": [ |
|
367 |
+ |
{"type": "object", "properties": {"card": {"type": "string"}}}, |
|
368 |
+ |
{"type": "object", "properties": {"iban": {"type": "string"}}} |
|
369 |
+ |
]} |
|
370 |
+ |
}}); |
|
371 |
+ |
let names: Vec<String> = body_docs(&doc, &schema) |
|
372 |
+ |
.into_iter() |
|
373 |
+ |
.map(|d| d.name) |
|
374 |
+ |
.collect(); |
|
375 |
+ |
assert!(names.contains(&"payment".to_string())); |
|
376 |
+ |
assert!(names.contains(&"payment.card".to_string())); |
|
377 |
+ |
assert!(names.contains(&"payment.iban".to_string())); |
|
378 |
+ |
} |
|
379 |
+ |
|
|
380 |
+ |
#[test] |
|
381 |
+ |
fn response_docs_are_flattened_and_located() { |
|
382 |
+ |
let doc = json!({}); |
|
383 |
+ |
let schema = json!({"type": "object", "properties": { |
|
384 |
+ |
"id": {"type": "integer"}, |
|
385 |
+ |
"owner": {"type": "object", "properties": {"name": {"type": "string"}}} |
|
386 |
+ |
}}); |
|
387 |
+ |
let docs = response_docs(&doc, &schema); |
|
388 |
+ |
let names: Vec<&str> = docs.iter().map(|d| d.name.as_str()).collect(); |
|
389 |
+ |
assert_eq!(names, ["id", "owner", "owner.name"]); |
|
390 |
+ |
assert!(docs.iter().all(|d| d.location == "response")); |
|
391 |
+ |
} |
|
392 |
+ |
|
|
393 |
+ |
#[test] |
|
394 |
+ |
fn non_object_response_gets_one_row() { |
|
395 |
+ |
let doc = json!({}); |
|
396 |
+ |
let docs = response_docs(&doc, &json!({"type": "string"})); |
|
397 |
+ |
assert_eq!(docs.len(), 1); |
|
398 |
+ |
assert_eq!(docs[0].name, "(response)"); |
|
399 |
+ |
assert_eq!(docs[0].location, "response"); |
| 335 |
400 |
|
} |
| 336 |
401 |
|
|
| 337 |
402 |
|
#[test] |