ferrin_schema/
transform.rs1use serde_json::Map;
4use serde_json::Value;
5use serde_json::json;
6
7use crate::transform_refs::child_path;
8use crate::transform_refs::rewrite_local_references;
9use crate::transform_refs::visit_children;
10
11use crate::SchemaError;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum SchemaTransform {
17 AdditionalPropertiesFalse,
22 RemovePropertyNames,
24 OpenAiStrict,
27}
28
29impl SchemaTransform {
30 #[must_use]
32 pub fn openai_strict() -> Self {
33 Self::OpenAiStrict
34 }
35
36 #[must_use]
38 pub fn additional_properties_false() -> Self {
39 Self::AdditionalPropertiesFalse
40 }
41
42 pub fn apply(self, schema: &mut Value) -> Result<(), SchemaError> {
49 match self {
50 Self::AdditionalPropertiesFalse => add_additional_properties_false(schema),
51 Self::RemovePropertyNames => remove_property_names(schema),
52 Self::OpenAiStrict => return to_openai_strict(schema),
53 }
54 Ok(())
55 }
56
57 pub fn applied(self, mut schema: Value) -> Result<Value, SchemaError> {
64 self.apply(&mut schema)?;
65 Ok(schema)
66 }
67}
68
69pub fn add_additional_properties_false(schema: &mut Value) {
76 let Value::Object(obj) = schema else { return };
77 if type_includes(obj, "object") {
78 match obj.get_mut("additionalProperties") {
79 Some(nested @ Value::Object(_)) => add_additional_properties_false(nested),
80 _ => {
81 obj.insert("additionalProperties".to_owned(), Value::Bool(false));
82 }
83 }
84 if let Some(Value::Object(properties)) = obj.get_mut("properties") {
85 for property in properties.values_mut() {
86 add_additional_properties_false(property);
87 }
88 }
89 }
90 for_each_subschema(obj, &mut add_additional_properties_false);
91}
92
93pub fn remove_property_names(schema: &mut Value) {
95 let Value::Object(obj) = schema else { return };
96 obj.remove("propertyNames");
97 if let Some(Value::Object(properties)) = obj.get_mut("properties") {
98 for property in properties.values_mut() {
99 remove_property_names(property);
100 }
101 }
102 if let Some(nested @ Value::Object(_)) = obj.get_mut("additionalProperties") {
103 remove_property_names(nested);
104 }
105 for_each_subschema(obj, &mut remove_property_names);
106}
107
108pub fn to_openai_strict(schema: &mut Value) -> Result<(), SchemaError> {
122 validate_strict_maps(schema)?;
123 let mut moves = Vec::new();
124 rewrite_openai_strict(schema, "", &mut moves);
125 rewrite_local_references(schema, &moves);
126 Ok(())
127}
128
129fn validate_strict_maps(schema: &Value) -> Result<(), SchemaError> {
130 let Value::Object(obj) = schema else {
131 return Ok(());
132 };
133 for keyword in ["additionalProperties", "patternProperties"] {
134 let unsupported = matches!(obj.get(keyword), Some(Value::Object(_) | Value::Bool(true)));
135 if unsupported {
136 return Err(SchemaError::UnsupportedTransform {
137 transform: "openai strict",
138 keyword,
139 });
140 }
141 }
142 for key in [
143 "properties",
144 "definitions",
145 "$defs",
146 "dependentSchemas",
147 "dependencies",
148 ] {
149 if let Some(Value::Object(map)) = obj.get(key) {
150 for child in map.values() {
151 validate_strict_maps(child)?;
152 }
153 }
154 }
155 for key in [
156 "items",
157 "additionalItems",
158 "anyOf",
159 "allOf",
160 "oneOf",
161 "prefixItems",
162 "not",
163 "if",
164 "then",
165 "else",
166 "contains",
167 "unevaluatedItems",
168 "unevaluatedProperties",
169 ] {
170 if let Some(child) = obj.get(key) {
171 match child {
172 Value::Array(children) => {
173 for child in children {
174 validate_strict_maps(child)?;
175 }
176 }
177 child => validate_strict_maps(child)?,
178 }
179 }
180 }
181 Ok(())
182}
183
184fn rewrite_openai_strict(schema: &mut Value, path: &str, moves: &mut Vec<String>) {
185 let Value::Object(obj) = schema else { return };
186 obj.remove("propertyNames");
187 let is_object = match obj.get("type") {
188 Some(Value::String(kind)) => kind == "object",
189 Some(Value::Array(kinds)) => kinds.iter().any(|kind| kind == "object"),
190 _ => obj.contains_key("properties"),
191 };
192 if is_object {
193 let required: Vec<String> = obj
194 .get("required")
195 .and_then(Value::as_array)
196 .map(|items| {
197 items
198 .iter()
199 .filter_map(Value::as_str)
200 .map(str::to_owned)
201 .collect()
202 })
203 .unwrap_or_default();
204 if let Some(Value::Object(properties)) = obj.get_mut("properties") {
205 let names: Vec<String> = properties.keys().cloned().collect();
206 for (name, property) in properties.iter_mut() {
207 if !required.contains(name) {
208 moves.push(child_path(&child_path(path, "properties"), name));
209 make_nullable(property);
210 }
211 }
212 obj.insert("required".to_owned(), json!(names));
213 }
214 obj.insert("additionalProperties".to_owned(), Value::Bool(false));
215 }
216 visit_children(obj, path, &mut |child, child_path| {
217 rewrite_openai_strict(child, child_path, moves);
218 });
219}
220
221fn make_nullable(property: &mut Value) {
223 let original = std::mem::take(property);
226 *property = json!({ "anyOf": [original, { "type": "null" }] });
227}
228
229fn type_includes(obj: &Map<String, Value>, kind: &str) -> bool {
230 match obj.get("type") {
231 Some(Value::String(actual)) => actual == kind,
232 Some(Value::Array(kinds)) => kinds.iter().any(|actual| actual == kind),
233 _ => false,
234 }
235}
236
237fn for_each_subschema(obj: &mut Map<String, Value>, visit: &mut dyn FnMut(&mut Value)) {
239 if let Some(items) = obj.get_mut("items") {
240 match items {
241 Value::Array(list) => list.iter_mut().for_each(&mut *visit),
242 other => visit(other),
243 }
244 }
245 for key in ["anyOf", "allOf", "oneOf"] {
246 if let Some(Value::Array(list)) = obj.get_mut(key) {
247 list.iter_mut().for_each(&mut *visit);
248 }
249 }
250 for key in ["definitions", "$defs"] {
251 if let Some(Value::Object(map)) = obj.get_mut(key) {
252 map.values_mut().for_each(&mut *visit);
253 }
254 }
255}