1use serde::{Deserialize, Serialize};
9use serde_json::{Map, Value};
10use std::any::TypeId;
11use std::collections::BTreeMap;
12use thiserror::Error;
13
14use crate::types::{BoundedI64, ConstrainedString, Email, PositiveInt, SecretString};
15
16pub const COMPONENTS_REF_PREFIX: &str = "#/components/schemas/";
18
19#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
21#[serde(transparent)]
22pub struct SchemaObject(
23 pub Map<String, Value>,
25);
26
27impl SchemaObject {
28 pub fn of_type(ty: &str) -> Self {
30 Self::default().with("type", ty)
31 }
32
33 pub fn reference(target: impl Into<String>) -> Self {
35 Self::default().with("$ref", target.into())
36 }
37
38 #[must_use]
40 pub fn with(mut self, key: impl Into<String>, value: impl Into<Value>) -> Self {
41 self.0.insert(key.into(), value.into());
42 self
43 }
44
45 pub fn get(&self, key: &str) -> Option<&Value> {
47 self.0.get(key)
48 }
49
50 pub fn into_value(self) -> Value {
52 Value::Object(self.0)
53 }
54}
55
56impl From<SchemaObject> for Value {
57 fn from(s: SchemaObject) -> Self {
58 s.into_value()
59 }
60}
61
62pub trait Schema {
64 fn schema_name() -> Option<&'static str> {
67 None
68 }
69
70 fn schema(registry: &mut SchemaRegistry) -> SchemaObject;
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Error)]
77#[error("schema name `{name}` is used by both `{first}` and `{second}`")]
78pub struct SchemaConflict {
79 pub name: String,
81 pub first: &'static str,
83 pub second: &'static str,
85}
86
87#[derive(Debug, Clone)]
88struct Entry {
89 type_id: TypeId,
90 type_name: &'static str,
91 definition: Option<SchemaObject>,
93}
94
95#[derive(Debug, Clone, Default)]
97pub struct SchemaRegistry {
98 entries: BTreeMap<String, Entry>,
99 conflicts: Vec<SchemaConflict>,
100 security_schemes: BTreeMap<String, serde_json::Value>,
101}
102
103impl SchemaRegistry {
104 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn subschema<T: Schema + ?Sized + 'static>(&mut self) -> SchemaObject {
111 let Some(name) = T::schema_name() else {
112 return T::schema(self);
113 };
114 let reference = SchemaObject::reference(format!("{COMPONENTS_REF_PREFIX}{name}"));
115 let type_id = TypeId::of::<T>();
116 let type_name = std::any::type_name::<T>();
117 if let Some(existing) = self.entries.get(name) {
118 if existing.type_id != type_id {
119 self.conflicts.push(SchemaConflict {
120 name: name.to_owned(),
121 first: existing.type_name,
122 second: type_name,
123 });
124 }
125 return reference;
126 }
127 self.entries.insert(
129 name.to_owned(),
130 Entry {
131 type_id,
132 type_name,
133 definition: None,
134 },
135 );
136 let definition = T::schema(self);
137 if let Some(entry) = self.entries.get_mut(name) {
138 entry.definition = Some(definition);
139 }
140 reference
141 }
142
143 pub fn add_security_scheme(&mut self, name: &str, scheme: serde_json::Value) {
149 match self.security_schemes.get(name) {
150 Some(existing) if *existing != scheme => self.conflicts.push(SchemaConflict {
151 name: name.to_owned(),
152 first: "security scheme",
153 second: "security scheme",
154 }),
155 Some(_) => {}
156 None => {
157 self.security_schemes.insert(name.to_owned(), scheme);
158 }
159 }
160 }
161
162 pub fn take_security_schemes(&mut self) -> BTreeMap<String, serde_json::Value> {
164 std::mem::take(&mut self.security_schemes)
165 }
166
167 pub fn into_components(self) -> Result<BTreeMap<String, SchemaObject>, Vec<SchemaConflict>> {
172 if !self.conflicts.is_empty() {
173 return Err(self.conflicts);
174 }
175 Ok(self
176 .entries
177 .into_iter()
178 .filter_map(|(name, e)| e.definition.map(|d| (name, d)))
179 .collect())
180 }
181}
182
183pub fn schema_for<T: Schema + ?Sized + 'static>() -> (SchemaObject, SchemaRegistry) {
188 let mut registry = SchemaRegistry::new();
189 let root = registry.subschema::<T>();
190 (root, registry)
191}
192
193macro_rules! primitive_schema {
194 ($($t:ty => $ty:literal $(, $k:literal = $v:expr)*);* $(;)?) => {$(
195 impl Schema for $t {
196 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
197 SchemaObject::of_type($ty)$(.with($k, $v))*
198 }
199 }
200 )*};
201}
202
203primitive_schema! {
204 String => "string";
205 str => "string";
206 bool => "boolean";
207 i8 => "integer", "format" = "int8";
208 i16 => "integer", "format" = "int16";
209 i32 => "integer", "format" = "int32";
210 i64 => "integer", "format" = "int64";
211 u8 => "integer", "minimum" = 0_i64, "maximum" = 255_i64;
212 u16 => "integer", "minimum" = 0_i64, "maximum" = i64::from(u16::MAX);
213 u32 => "integer", "minimum" = 0_i64, "maximum" = i64::from(u32::MAX);
214 u64 => "integer", "minimum" = 0_i64;
215 f32 => "number", "format" = "float";
216 f64 => "number", "format" = "double";
217 i128 => "integer", "minimum" = i64::MIN, "maximum" = u64::MAX;
220 u128 => "integer", "minimum" = 0_i64, "maximum" = u64::MAX;
221 isize => "integer", "minimum" = isize::MIN as i64, "maximum" = isize::MAX as i64;
222 usize => "integer", "minimum" = 0_i64, "maximum" = usize::MAX as u64;
223}
224
225impl Schema for serde_json::Value {
227 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
228 SchemaObject::default()
229 }
230}
231
232impl<T: Schema + ?Sized + 'static> Schema for &T {
233 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
234 registry.subschema::<T>()
235 }
236}
237
238impl<T: Schema + 'static> Schema for Option<T> {
239 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
240 let inner = registry.subschema::<T>();
241 SchemaObject::default().with(
242 "anyOf",
243 Value::Array(vec![
244 inner.into_value(),
245 SchemaObject::of_type("null").into_value(),
246 ]),
247 )
248 }
249}
250
251impl<T: Schema + 'static> Schema for Vec<T> {
252 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
253 SchemaObject::of_type("array").with("items", registry.subschema::<T>())
254 }
255}
256
257impl<T: Schema + 'static> Schema for [T] {
258 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
259 SchemaObject::of_type("array").with("items", registry.subschema::<T>())
260 }
261}
262
263impl<T: Schema + ?Sized + 'static> Schema for Box<T> {
264 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
265 registry.subschema::<T>()
266 }
267}
268
269impl<T: Schema + 'static> Schema for BTreeMap<String, T> {
270 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
271 SchemaObject::of_type("object").with("additionalProperties", registry.subschema::<T>())
272 }
273}
274
275impl<T: Schema + 'static, S: 'static> Schema for std::collections::HashMap<String, T, S> {
276 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
277 SchemaObject::of_type("object").with("additionalProperties", registry.subschema::<T>())
278 }
279}
280
281macro_rules! tuple_schema {
282 ($($t:ident),+) => {
283 impl<$($t: Schema + 'static),+> Schema for ($($t,)+) {
284 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
285 let items = vec![$(registry.subschema::<$t>().into_value()),+];
286 let len = items.len() as u64;
287 SchemaObject::of_type("array")
288 .with("prefixItems", Value::Array(items))
289 .with("minItems", len)
290 .with("maxItems", len)
291 }
292 }
293 };
294}
295tuple_schema!(A);
296tuple_schema!(A, B);
297tuple_schema!(A, B, C);
298tuple_schema!(A, B, C, D);
299
300impl<const MIN: usize, const MAX: usize> Schema for ConstrainedString<MIN, MAX> {
301 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
302 SchemaObject::of_type("string")
303 .with("minLength", MIN as u64)
304 .with("maxLength", MAX as u64)
305 }
306}
307
308impl<const MIN: i64, const MAX: i64> Schema for BoundedI64<MIN, MAX> {
309 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
310 SchemaObject::of_type("integer")
311 .with("minimum", MIN)
312 .with("maximum", MAX)
313 }
314}
315
316impl Schema for PositiveInt {
317 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
318 SchemaObject::of_type("integer").with("exclusiveMinimum", 0_i64)
319 }
320}
321
322impl Schema for Email {
323 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
324 SchemaObject::of_type("string").with("format", "email")
325 }
326}
327
328impl Schema for SecretString {
329 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
330 SchemaObject::of_type("string").with("format", "password")
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use serde_json::json;
338
339 fn inline<T: Schema + ?Sized + 'static>() -> Value {
340 schema_for::<T>().0.into_value()
341 }
342
343 #[test]
344 fn primitive_schemas() {
345 assert_eq!(inline::<String>(), json!({"type": "string"}));
346 assert_eq!(inline::<bool>(), json!({"type": "boolean"}));
347 assert_eq!(inline::<serde_json::Value>(), json!({}));
348 assert_eq!(
349 inline::<usize>(),
350 json!({"type": "integer", "minimum": 0, "maximum": usize::MAX})
351 );
352 assert_eq!(inline::<u128>()["maximum"], json!(u64::MAX));
353 assert_eq!(
354 inline::<i32>(),
355 json!({"type": "integer", "format": "int32"})
356 );
357 assert_eq!(
358 inline::<u8>(),
359 json!({"type": "integer", "minimum": 0, "maximum": 255})
360 );
361 assert_eq!(
362 inline::<u32>(),
363 json!({"type": "integer", "minimum": 0, "maximum": 4294967295_u64})
364 );
365 assert_eq!(
366 inline::<f64>(),
367 json!({"type": "number", "format": "double"})
368 );
369 }
370
371 #[test]
372 fn containers() {
373 assert_eq!(
374 inline::<Option<String>>(),
375 json!({"anyOf": [{"type": "string"}, {"type": "null"}]})
376 );
377 assert_eq!(
378 inline::<Vec<bool>>(),
379 json!({"type": "array", "items": {"type": "boolean"}})
380 );
381 }
382
383 #[test]
384 fn constrained_type_keywords() {
385 assert_eq!(
386 inline::<ConstrainedString<1, 16>>(),
387 json!({"type": "string", "minLength": 1, "maxLength": 16})
388 );
389 assert_eq!(
390 inline::<BoundedI64<0, 100>>(),
391 json!({"type": "integer", "minimum": 0, "maximum": 100})
392 );
393 assert_eq!(
394 inline::<PositiveInt>(),
395 json!({"type": "integer", "exclusiveMinimum": 0})
396 );
397 assert_eq!(
398 inline::<Email>(),
399 json!({"type": "string", "format": "email"})
400 );
401 assert_eq!(
402 inline::<SecretString>(),
403 json!({"type": "string", "format": "password"})
404 );
405 }
406
407 struct Node;
409 impl Schema for Node {
410 fn schema_name() -> Option<&'static str> {
411 Some("Node")
412 }
413 fn schema(registry: &mut SchemaRegistry) -> SchemaObject {
414 SchemaObject::of_type("object").with(
415 "properties",
416 json!({ "children": registry.subschema::<Vec<Node>>() }),
417 )
418 }
419 }
420
421 struct OtherNode;
422 impl Schema for OtherNode {
423 fn schema_name() -> Option<&'static str> {
424 Some("Node")
425 }
426 fn schema(_: &mut SchemaRegistry) -> SchemaObject {
427 SchemaObject::of_type("string")
428 }
429 }
430
431 #[test]
432 fn named_types_are_referenced_once_and_recursion_terminates() {
433 let mut registry = SchemaRegistry::new();
434 let a = registry.subschema::<Node>();
435 let b = registry.subschema::<Vec<Node>>();
436 assert_eq!(a.into_value(), json!({"$ref": "#/components/schemas/Node"}));
437 assert_eq!(
438 b.into_value(),
439 json!({"type": "array", "items": {"$ref": "#/components/schemas/Node"}})
440 );
441 let components = registry.into_components().unwrap();
442 assert_eq!(components.len(), 1);
443 assert_eq!(
444 components["Node"].clone().into_value(),
445 json!({"type": "object", "properties": {"children": {
446 "type": "array", "items": {"$ref": "#/components/schemas/Node"}}}})
447 );
448 }
449
450 #[test]
451 fn name_conflicts_are_reported() {
452 let mut registry = SchemaRegistry::new();
453 registry.subschema::<Node>();
454 registry.subschema::<OtherNode>();
455 let err = registry.into_components().unwrap_err();
456 assert_eq!(err.len(), 1);
457 assert_eq!(err[0].name, "Node");
458 }
459}