mockforge_openapi/
schema_ref_resolver.rs1use jsonschema::{Draft, Validator};
24use openapiv3::{OpenAPI, Schema};
25use serde_json::Value;
26
27pub fn build_validator(schema: &Schema, spec: &OpenAPI) -> Result<Validator, String> {
32 let schema_json = serde_json::to_value(schema)
33 .map_err(|e| format!("Failed to convert OpenAPI schema to JSON: {e}"))?;
34 let merged = merge_components_into(schema_json, spec);
35 jsonschema::options()
36 .with_draft(Draft::Draft7)
37 .build(&merged)
38 .map_err(|e| format!("Failed to create schema validator: {e}"))
39}
40
41pub fn merge_components_into(mut schema_json: Value, spec: &OpenAPI) -> Value {
46 let Some(components) = &spec.components else {
47 return schema_json;
48 };
49 let Value::Object(ref mut map) = schema_json else {
50 let inner = schema_json;
53 let wrapper = serde_json::json!({
54 "allOf": [inner],
55 });
56 return merge_components_into(wrapper, spec);
57 };
58 if map.contains_key("components") {
59 return schema_json;
60 }
61 if let Ok(comp_json) = serde_json::to_value(components) {
62 map.insert("components".to_string(), comp_json);
63 }
64 schema_json
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70 use openapiv3::{Components, ReferenceOr, SchemaData, SchemaKind, StringType, Type};
71
72 fn make_spec_with_named_schema(name: &str, schema: Schema) -> OpenAPI {
73 let mut components = Components::default();
74 components.schemas.insert(name.to_string(), ReferenceOr::Item(schema));
75 OpenAPI {
76 openapi: "3.0.0".into(),
77 info: Default::default(),
78 components: Some(components),
79 ..Default::default()
80 }
81 }
82
83 #[test]
89 fn dotted_schema_ref_resolves_against_spec_context() {
90 let nested = Schema {
92 schema_data: SchemaData::default(),
93 schema_kind: SchemaKind::Type(Type::String(StringType::default())),
94 };
95 let spec = make_spec_with_named_schema("Foo.Bar.Baz", nested);
96
97 let body_schema_json = serde_json::json!({
100 "type": "object",
101 "properties": {
102 "name": {"$ref": "#/components/schemas/Foo.Bar.Baz"}
103 },
104 "required": ["name"]
105 });
106 let body_schema: Schema = serde_json::from_value(body_schema_json).unwrap();
107
108 let validator =
109 build_validator(&body_schema, &spec).expect("validator builds against spec context");
110
111 let good = serde_json::json!({"name": "hello"});
113 assert!(validator.iter_errors(&good).next().is_none());
114
115 let bad = serde_json::json!({"name": 42});
117 assert!(validator.iter_errors(&bad).next().is_some());
118 }
119
120 #[test]
128 fn naked_validator_fails_to_build_on_dotted_ref() {
129 let body_schema_json = serde_json::json!({
130 "type": "object",
131 "properties": {
132 "name": {"$ref": "#/components/schemas/Foo.Bar.Baz"}
133 }
134 });
135 let result = jsonschema::options().with_draft(Draft::Draft7).build(&body_schema_json);
136 let err = result.expect_err("naked validator should fail to build");
137 let msg = err.to_string();
138 assert!(
139 msg.contains("Foo.Bar.Baz") || msg.contains("/components/schemas/"),
140 "naked-validator error should reference the unresolvable pointer; got: {msg}"
141 );
142 }
143
144 #[test]
147 fn explicit_components_takes_precedence() {
148 let spec = make_spec_with_named_schema(
149 "X",
150 Schema {
151 schema_data: SchemaData::default(),
152 schema_kind: SchemaKind::Type(Type::String(StringType::default())),
153 },
154 );
155 let schema = serde_json::json!({
156 "type": "object",
157 "components": {"schemas": {"X": {"type": "integer"}}}
158 });
159 let merged = merge_components_into(schema.clone(), &spec);
160 let x = merged.get("components").and_then(|c| c.get("schemas")).and_then(|s| s.get("X"));
163 assert_eq!(
164 x.and_then(|v| v.get("type")).and_then(|v| v.as_str()),
165 Some("integer"),
166 "explicit schema components should not be clobbered"
167 );
168 }
169
170 #[test]
174 fn spec_without_components_is_a_noop() {
175 let spec = OpenAPI {
176 openapi: "3.0.0".into(),
177 info: Default::default(),
178 ..Default::default()
179 };
180 let schema = serde_json::json!({"type": "string"});
181 let merged = merge_components_into(schema.clone(), &spec);
182 assert_eq!(merged, schema);
183 }
184}