src/openapi/examples.rs 5.4 K raw
1
//! Example payload generation from JSON schemas (OpenAPI 3.0/3.1 subset).
2
3
use serde_json::{Map, Value};
4
5
use super::resolve::deref;
6
7
/// Depth cap for generated structures (also breaks schema cycles).
8
pub const MAX_GEN_DEPTH: usize = 6;
9
10
/// Produce an example value for a schema, preferring explicitly authored
11
/// examples/defaults, then enums, then combinators, then type-based stubs.
12
pub fn example_for_schema(doc: &Value, schema: &Value) -> Value {
13
    gen_value(doc, schema, 0)
14
}
15
16
fn gen_value(doc: &Value, schema: &Value, depth: usize) -> Value {
17
    if depth > MAX_GEN_DEPTH {
18
        return Value::Null;
19
    }
20
    let schema = deref(doc, schema);
21
22
    if let Some(ex) = schema.get("example") {
23
        return ex.clone();
24
    }
25
    if let Some(def) = schema.get("default") {
26
        return def.clone();
27
    }
28
    // OpenAPI 3.1 / JSON Schema style `examples` array.
29
    if let Some(first) = schema
30
        .get("examples")
31
        .and_then(Value::as_array)
32
        .and_then(|a| a.first())
33
    {
34
        return first.clone();
35
    }
36
    if let Some(first) = schema
37
        .get("enum")
38
        .and_then(Value::as_array)
39
        .and_then(|a| a.first())
40
    {
41
        return first.clone();
42
    }
43
44
    if let Some(all) = schema.get("allOf").and_then(Value::as_array) {
45
        let mut merged = Map::new();
46
        for sub in all {
47
            if let Value::Object(props) = gen_value(doc, sub, depth + 1) {
48
                for (k, v) in props {
49
                    merged.insert(k, v);
50
                }
51
            }
52
        }
53
        return Value::Object(merged);
54
    }
55
    for key in ["oneOf", "anyOf"] {
56
        if let Some(first) = schema
57
            .get(key)
58
            .and_then(Value::as_array)
59
            .and_then(|a| a.first())
60
        {
61
            return gen_value(doc, first, depth + 1);
62
        }
63
    }
64
65
    let ty = schema.get("type").and_then(Value::as_str);
66
    // OpenAPI 3.1 allows `type` arrays like ["string", "null"].
67
    let ty = ty.or_else(|| {
68
        schema
69
            .get("type")
70
            .and_then(Value::as_array)
71
            .and_then(|a| a.first())
72
            .and_then(Value::as_str)
73
    });
74
    let ty = ty.or_else(|| {
75
        if schema.get("properties").is_some() {
76
            Some("object")
77
        } else {
78
            None
79
        }
80
    });
81
82
    match ty {
83
        Some("object") => {
84
            let mut map = Map::new();
85
            if let Some(props) = schema.get("properties").and_then(Value::as_object) {
86
                for (k, sub) in props {
87
                    map.insert(k.clone(), gen_value(doc, sub, depth + 1));
88
                }
89
            }
90
            Value::Object(map)
91
        }
92
        Some("array") => {
93
            let item = schema
94
                .get("items")
95
                .map(|s| gen_value(doc, s, depth + 1))
96
                .unwrap_or(Value::Null);
97
            Value::Array(vec![item])
98
        }
99
        Some("integer") => Value::from(1),
100
        Some("number") => Value::from(1.0),
101
        Some("boolean") => Value::from(true),
102
        _ => string_stub(schema),
103
    }
104
}
105
106
fn string_stub(schema: &Value) -> Value {
107
    match schema.get("format").and_then(Value::as_str) {
108
        // Substituted with a fresh UUID v4 at send time.
109
        Some("uuid") => Value::from("{{uuid}}"),
110
        Some("date-time") => Value::from("2024-01-01T00:00:00Z"),
111
        Some("date") => Value::from("2024-01-01"),
112
        Some("email") => Value::from("user@example.com"),
113
        _ => Value::from("string"),
114
    }
115
}
116
117
#[cfg(test)]
118
mod tests {
119
    use super::*;
120
    use serde_json::json;
121
122
    #[test]
123
    fn prefers_authored_example() {
124
        let doc = json!({});
125
        let schema = json!({"type": "integer", "example": 42});
126
        assert_eq!(example_for_schema(&doc, &schema), json!(42));
127
    }
128
129
    #[test]
130
    fn generates_object_with_refs() {
131
        let doc = json!({
132
            "components": { "schemas": {
133
                "Pet": {
134
                    "type": "object",
135
                    "properties": {
136
                        "id": {"type": "integer", "format": "int64"},
137
                        "name": {"type": "string"},
138
                        "tag": {"type": "string", "default": "friendly"}
139
                    }
140
                }
141
            }}
142
        });
143
        let schema = json!({"$ref": "#/components/schemas/Pet"});
144
        let v = example_for_schema(&doc, &schema);
145
        assert_eq!(v, json!({"id": 1, "name": "string", "tag": "friendly"}));
146
    }
147
148
    #[test]
149
    fn uuid_format_becomes_variable() {
150
        let doc = json!({});
151
        let schema = json!({"type": "string", "format": "uuid"});
152
        assert_eq!(example_for_schema(&doc, &schema), json!("{{uuid}}"));
153
    }
154
155
    #[test]
156
    fn all_of_merges() {
157
        let doc = json!({});
158
        let schema = json!({"allOf": [
159
            {"type": "object", "properties": {"a": {"type": "integer"}}},
160
            {"type": "object", "properties": {"b": {"type": "boolean"}}}
161
        ]});
162
        assert_eq!(
163
            example_for_schema(&doc, &schema),
164
            json!({"a": 1, "b": true})
165
        );
166
    }
167
168
    #[test]
169
    fn terminates_on_self_reference() {
170
        let doc = json!({
171
            "components": { "schemas": {
172
                "Node": {"type": "object", "properties": {
173
                    "child": {"$ref": "#/components/schemas/Node"}
174
                }}
175
            }}
176
        });
177
        let schema = json!({"$ref": "#/components/schemas/Node"});
178
        let _ = example_for_schema(&doc, &schema); // must terminate
179
    }
180
}