1use std::collections::{BTreeMap, HashSet};
3
4use schemars::schema::{
5 ArrayValidation, InstanceType, ObjectValidation, RootSchema, Schema, SchemaObject, SingleOrVec,
6};
7
8pub fn merge_schemas(schemas: impl Iterator<Item = RootSchema>) -> RootSchema {
36 let mut merged = RootSchema::default();
37 for schema in schemas {
38 for (id, definition) in schema.definitions {
39 merged.definitions.insert(id, definition);
40 }
41
42 let Some(id) = schema.schema.metadata.as_ref().and_then(|m| m.title.as_ref())
43 else { continue; };
44
45 merged
46 .definitions
47 .insert(id.to_owned(), Schema::Object(schema.schema));
48 }
49
50 merged
51}
52
53pub fn convert(schema: RootSchema) -> BTreeMap<String, String> {
78 let mut definitions = BTreeMap::new();
79
80 for (id, definition) in schema.definitions {
81 if let Some(definition) = add_converted_schema(&id, definition.into_object()) {
82 definitions.insert(id, definition);
83 }
84 }
85
86 definitions
87}
88
89fn add_converted_schema(id: &str, schema: SchemaObject) -> Option<String> {
90 let mut rv = String::new();
91
92 let Some(generated) = convert_schema_object_to_zod(schema) else { panic!("could not generate {id}"); };
93
94 rv.push_str(&format!("export const {id} = memoizeOne(() => {generated});\n"));
95 rv.push_str(&format!("export type {id} = z.infer<ReturnType<typeof {id}>>;\n"));
96
97 Some(rv)
98}
99
100fn convert_schema_object_to_zod(schema: SchemaObject) -> Option<String> {
101 if let Some(reference) = schema.reference.as_ref() {
103 let reference = reference.replace("#/definitions/", "");
104 return Some(format!("z.lazy({reference})"));
105 }
106
107 if let Some(enum_values) = schema.enum_values.as_ref() {
109 if enum_values.len() == 1 {
110 return Some(format!(
111 "z.literal({})",
112 serde_json::to_string_pretty(enum_values.first().unwrap()).unwrap()
113 ));
114 }
115
116 let mut rv = String::new();
117 rv.push_str("z.enum([");
118 for value in enum_values {
119 rv.push_str(&format!(
120 "{}, ",
121 serde_json::to_string_pretty(&value).unwrap()
122 ));
123 }
124 rv.push_str("])");
125
126 return Some(rv);
127 }
128
129 let one_of = schema.subschemas.as_ref().and_then(|x| x.one_of.as_ref());
131 let any_of = schema.subschemas.as_ref().and_then(|x| x.any_of.as_ref());
132 let all_of = schema.subschemas.as_ref().and_then(|x| x.all_of.as_ref());
133
134 if let Some(one_of) = one_of.or(any_of).or(all_of) {
135 if one_of.len() == 1 {
136 return convert_schema_object_to_zod(one_of.first().unwrap().clone().into_object());
137 }
138
139 let mut rv = if let Some(field) = all_schemas_share_a_field(one_of) {
140 format!("z.discriminatedUnion('{field}', [")
141 } else {
142 format!("z.union([")
143 };
144
145 for schema in one_of {
146 let Some(generated) = convert_schema_object_to_zod(schema.clone().into_object()) else { continue; };
147 rv.push_str(&format!("{generated}, "));
148 }
149
150 rv.push_str("])");
151 return Some(rv);
152 }
153
154 let Some(instance_type) = schema.instance_type.as_ref() else {
155 return Some("z.any()".to_string());
157 };
158
159 convert_schema_type_to_zod(instance_type, &schema)
160}
161
162fn all_schemas_share_a_field(any_of: &[Schema]) -> Option<String> {
163 let mut results = Vec::<HashSet<String>>::new();
164 for schema in any_of {
165 let schema = schema.clone().into_object();
166 if schema.instance_type.as_ref()
167 == Some(&SingleOrVec::Single(Box::new(InstanceType::Object)))
168 {
169 results.push(schema.object.unwrap().properties.keys().cloned().collect());
170 } else {
171 results.push(HashSet::default());
172 }
173 }
174
175 results.first().and_then(|first_props| {
176 let found = first_props
177 .iter()
178 .filter(|prop_name| {
179 results
180 .iter()
181 .skip(1)
182 .all(|props| props.contains(*prop_name))
183 })
184 .cloned()
185 .collect::<HashSet<_>>();
186
187 if found.contains("type") {
188 Some("type".to_owned())
189 } else if found.contains("kind") {
190 Some("kind".to_owned())
191 } else {
192 found.iter().next().map(|x| x.to_owned())
193 }
194 })
195}
196
197fn convert_schema_type_to_zod(
198 instance_type: &SingleOrVec<InstanceType>,
199 schema: &SchemaObject,
200) -> Option<String> {
201 match instance_type {
202 SingleOrVec::Single(single_type) => {
203 convert_single_instance_type_schema_to_zod(single_type, &schema)
204 }
205 SingleOrVec::Vec(multiple_types) => {
206 convert_union_type_schema_to_zod(multiple_types, &schema)
207 }
208 }
209}
210
211fn convert_single_instance_type_schema_to_zod(
212 instance_type: &Box<InstanceType>,
213 schema: &SchemaObject,
214) -> Option<String> {
215 if let Some(literal_value) = schema.const_value.as_ref() {
216 return Some(format!(
217 "z.literal({})",
218 serde_json::to_string_pretty(literal_value).unwrap()
219 ));
220 }
221
222 match instance_type.as_ref() {
223 InstanceType::Null => Some(format!("z.null()")),
224 InstanceType::Boolean => Some(format!("z.boolean()")),
225 InstanceType::Object => convert_object_type_to_zod(schema.object.as_ref().unwrap(), schema),
226 InstanceType::Array => convert_array_type_to_zod(schema.array.as_ref().unwrap(), schema),
227 InstanceType::Number => Some(format!("z.number()")),
228 InstanceType::String => {
229 if matches!(schema.format.as_ref(), Some(format) if format == "date-time") {
230 return Some(format!("z.coerce.date()"));
231 }
232 Some(format!("z.string()"))
233 }
234 InstanceType::Integer => Some(format!("z.number().int()")),
235 }
236}
237
238fn convert_array_type_to_zod(
239 array_type: &Box<ArrayValidation>,
240 _schema: &SchemaObject,
241) -> Option<String> {
242 let Some(items) = array_type.items.as_ref() else { return None; };
243
244 if array_type.min_items.is_some() && array_type.min_items == array_type.max_items {
245 convert_schema_or_ref_to_zod(items, "tuple")
246 } else {
247 let mut rv = String::new();
248 rv.push_str("z.array(");
249 let Some(generated) = convert_schema_or_ref_to_zod(items, "union") else { return None; };
250 rv.push_str(&format!("{generated})"));
251 Some(rv)
252 }
253}
254
255fn convert_schema_or_ref_to_zod(schema: &SingleOrVec<Schema>, zod_mode: &str) -> Option<String> {
256 match schema {
257 SingleOrVec::Single(schema_or_ref) => {
258 convert_schema_object_to_zod(schema_or_ref.clone().into_object())
259 }
260 SingleOrVec::Vec(schemas) => {
261 if schemas.len() == 1 {
262 return convert_schema_object_to_zod(
263 schemas.first().unwrap().clone().into_object(),
264 );
265 }
266
267 let mut rv = String::new();
268 rv.push_str(&format!("z.{zod_mode}(["));
269 for schema in schemas {
270 if let Some(schema) = convert_schema_object_to_zod(schema.clone().into_object()) {
271 rv.push_str(&format!("{schema}, ",));
272 }
273 }
274 rv.push_str("])");
275 Some(rv)
276 }
277 }
278}
279
280fn convert_object_type_to_zod(
281 object_type: &Box<ObjectValidation>,
282 _schema: &SchemaObject,
283) -> Option<String> {
284 let mut rv = String::new();
285
286 if object_type.additional_properties.is_some() && object_type.properties.is_empty() {
288 let Some(additional_properties) = object_type.additional_properties.as_ref() else { return None; };
289 let Some(additional_properties) = convert_schema_object_to_zod(additional_properties.clone().into_object()) else { return None; };
290 return Some(format!("z.record({additional_properties})"));
291 }
292
293 rv.push_str("z.object({");
294
295 for (property_name, property) in &object_type.properties {
296 let Some(property_type) = convert_schema_object_to_zod(property.clone().into_object()) else { return None; };
297 rv.push_str(&format!("{property_name}: {property_type}, ",));
298 }
299
300 rv.push_str("})");
301
302 Some(rv)
303}
304
305fn convert_union_type_schema_to_zod(
306 instance_types: &Vec<InstanceType>,
307 schema: &SchemaObject,
308) -> Option<String> {
309 let mut rv = String::new();
310
311 rv.push_str("z.union([");
312 for instance_type in instance_types {
313 let Some(generated) = convert_single_instance_type_schema_to_zod(&Box::new(instance_type.clone()), schema) else { return None; };
314 rv.push_str(&format!("{generated}, "));
315 }
316
317 rv.push_str("])");
318
319 Some(rv)
320}