Skip to main content

rustapi_openapi/
schema.rs

1//! JSON Schema 2020-12 support and RustApiSchema trait
2
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeMap;
5
6/// Type array for nullable types in JSON Schema 2020-12
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
8#[serde(untagged)]
9pub enum TypeArray {
10    Single(String),
11    Array(Vec<String>),
12}
13
14impl TypeArray {
15    pub fn single(ty: impl Into<String>) -> Self {
16        Self::Single(ty.into())
17    }
18
19    pub fn nullable(ty: impl Into<String>) -> Self {
20        Self::Array(vec![ty.into(), "null".to_string()])
21    }
22}
23
24/// JSON Schema 2020-12 schema definition
25#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
26#[serde(rename_all = "camelCase")]
27pub struct JsonSchema2020 {
28    #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
29    pub schema: Option<String>,
30    #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
31    pub id: Option<String>,
32    #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
33    pub reference: Option<String>,
34    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
35    pub schema_type: Option<TypeArray>,
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub title: Option<String>,
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub description: Option<String>,
40    #[serde(skip_serializing_if = "Option::is_none")]
41    pub default: Option<serde_json::Value>,
42    #[serde(rename = "const", skip_serializing_if = "Option::is_none")]
43    pub const_value: Option<serde_json::Value>,
44    #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
45    pub enum_values: Option<Vec<serde_json::Value>>,
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub format: Option<String>,
48
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub items: Option<Box<JsonSchema2020>>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub properties: Option<BTreeMap<String, JsonSchema2020>>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub required: Option<Vec<String>>,
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub additional_properties: Option<Box<AdditionalProperties>>,
57
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub one_of: Option<Vec<JsonSchema2020>>,
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub any_of: Option<Vec<JsonSchema2020>>,
62    #[serde(skip_serializing_if = "Option::is_none")]
63    pub all_of: Option<Vec<JsonSchema2020>>,
64
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub example: Option<serde_json::Value>,
67}
68
69impl JsonSchema2020 {
70    pub fn new() -> Self {
71        Self::default()
72    }
73    pub fn string() -> Self {
74        Self {
75            schema_type: Some(TypeArray::single("string")),
76            ..Default::default()
77        }
78    }
79    pub fn integer() -> Self {
80        Self {
81            schema_type: Some(TypeArray::single("integer")),
82            ..Default::default()
83        }
84    }
85    pub fn number() -> Self {
86        Self {
87            schema_type: Some(TypeArray::single("number")),
88            ..Default::default()
89        }
90    }
91    pub fn boolean() -> Self {
92        Self {
93            schema_type: Some(TypeArray::single("boolean")),
94            ..Default::default()
95        }
96    }
97    pub fn array(items: JsonSchema2020) -> Self {
98        Self {
99            schema_type: Some(TypeArray::single("array")),
100            items: Some(Box::new(items)),
101            ..Default::default()
102        }
103    }
104    pub fn object() -> Self {
105        Self {
106            schema_type: Some(TypeArray::single("object")),
107            properties: Some(BTreeMap::new()),
108            ..Default::default()
109        }
110    }
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
114#[serde(untagged)]
115pub enum AdditionalProperties {
116    Bool(bool),
117    Schema(Box<JsonSchema2020>),
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
121#[serde(untagged)]
122pub enum SchemaRef {
123    Ref {
124        #[serde(rename = "$ref")]
125        reference: String,
126    },
127    Schema(Box<JsonSchema2020>),
128    Inline(serde_json::Value),
129}
130
131pub struct SchemaCtx {
132    pub components: BTreeMap<String, JsonSchema2020>,
133}
134
135impl Default for SchemaCtx {
136    fn default() -> Self {
137        Self::new()
138    }
139}
140
141impl SchemaCtx {
142    pub fn new() -> Self {
143        Self {
144            components: BTreeMap::new(),
145        }
146    }
147}
148
149pub trait RustApiSchema {
150    fn schema(ctx: &mut SchemaCtx) -> SchemaRef;
151    fn component_name() -> Option<&'static str> {
152        None
153    }
154
155    /// Get field schemas if this type is a struct (for Query params extraction)
156    fn field_schemas(_ctx: &mut SchemaCtx) -> Option<BTreeMap<String, SchemaRef>> {
157        None
158    }
159}
160
161// Primitives
162impl RustApiSchema for String {
163    fn schema(_: &mut SchemaCtx) -> SchemaRef {
164        SchemaRef::Schema(Box::new(JsonSchema2020::string()))
165    }
166}
167impl RustApiSchema for &str {
168    fn schema(_: &mut SchemaCtx) -> SchemaRef {
169        SchemaRef::Schema(Box::new(JsonSchema2020::string()))
170    }
171}
172impl RustApiSchema for bool {
173    fn schema(_: &mut SchemaCtx) -> SchemaRef {
174        SchemaRef::Schema(Box::new(JsonSchema2020::boolean()))
175    }
176}
177impl RustApiSchema for i32 {
178    fn schema(_: &mut SchemaCtx) -> SchemaRef {
179        let mut s = JsonSchema2020::integer();
180        s.format = Some("int32".to_string());
181        SchemaRef::Schema(Box::new(s))
182    }
183}
184impl RustApiSchema for i64 {
185    fn schema(_: &mut SchemaCtx) -> SchemaRef {
186        let mut s = JsonSchema2020::integer();
187        s.format = Some("int64".to_string());
188        SchemaRef::Schema(Box::new(s))
189    }
190}
191impl RustApiSchema for f64 {
192    fn schema(_: &mut SchemaCtx) -> SchemaRef {
193        let mut s = JsonSchema2020::number();
194        s.format = Some("double".to_string());
195        SchemaRef::Schema(Box::new(s))
196    }
197}
198impl RustApiSchema for f32 {
199    fn schema(_: &mut SchemaCtx) -> SchemaRef {
200        let mut s = JsonSchema2020::number();
201        s.format = Some("float".to_string());
202        SchemaRef::Schema(Box::new(s))
203    }
204}
205
206impl RustApiSchema for i8 {
207    fn schema(_: &mut SchemaCtx) -> SchemaRef {
208        let mut s = JsonSchema2020::integer();
209        s.format = Some("int8".to_string());
210        SchemaRef::Schema(Box::new(s))
211    }
212}
213impl RustApiSchema for i16 {
214    fn schema(_: &mut SchemaCtx) -> SchemaRef {
215        let mut s = JsonSchema2020::integer();
216        s.format = Some("int16".to_string());
217        SchemaRef::Schema(Box::new(s))
218    }
219}
220impl RustApiSchema for isize {
221    fn schema(_: &mut SchemaCtx) -> SchemaRef {
222        let mut s = JsonSchema2020::integer();
223        s.format = Some("int64".to_string());
224        SchemaRef::Schema(Box::new(s))
225    }
226}
227impl RustApiSchema for u8 {
228    fn schema(_: &mut SchemaCtx) -> SchemaRef {
229        let mut s = JsonSchema2020::integer();
230        s.format = Some("uint8".to_string());
231        SchemaRef::Schema(Box::new(s))
232    }
233}
234impl RustApiSchema for u16 {
235    fn schema(_: &mut SchemaCtx) -> SchemaRef {
236        let mut s = JsonSchema2020::integer();
237        s.format = Some("uint16".to_string());
238        SchemaRef::Schema(Box::new(s))
239    }
240}
241impl RustApiSchema for u32 {
242    fn schema(_: &mut SchemaCtx) -> SchemaRef {
243        let mut s = JsonSchema2020::integer();
244        s.format = Some("uint32".to_string());
245        SchemaRef::Schema(Box::new(s))
246    }
247}
248impl RustApiSchema for u64 {
249    fn schema(_: &mut SchemaCtx) -> SchemaRef {
250        let mut s = JsonSchema2020::integer();
251        s.format = Some("uint64".to_string());
252        SchemaRef::Schema(Box::new(s))
253    }
254}
255impl RustApiSchema for usize {
256    fn schema(_: &mut SchemaCtx) -> SchemaRef {
257        let mut s = JsonSchema2020::integer();
258        s.format = Some("uint64".to_string());
259        SchemaRef::Schema(Box::new(s))
260    }
261}
262
263// Vec
264impl<T: RustApiSchema> RustApiSchema for Vec<T> {
265    fn schema(ctx: &mut SchemaCtx) -> SchemaRef {
266        match T::schema(ctx) {
267            SchemaRef::Schema(s) => SchemaRef::Schema(Box::new(JsonSchema2020::array(*s))),
268            SchemaRef::Ref { reference } => {
269                // If T is a ref, items: {$ref: ...}
270                let mut s = JsonSchema2020::new();
271                s.schema_type = Some(TypeArray::single("array"));
272                let mut ref_schema = JsonSchema2020::new();
273                ref_schema.reference = Some(reference);
274                s.items = Some(Box::new(ref_schema));
275                SchemaRef::Schema(Box::new(s))
276            }
277            SchemaRef::Inline(_) => SchemaRef::Schema(Box::new(JsonSchema2020 {
278                schema_type: Some(TypeArray::single("array")),
279                // Inline not easily convertible to JsonSchema2020 without parsing
280                // Fallback to minimal array
281                ..Default::default()
282            })),
283        }
284    }
285}
286
287// Option
288impl<T: RustApiSchema> RustApiSchema for Option<T> {
289    fn schema(ctx: &mut SchemaCtx) -> SchemaRef {
290        let inner = T::schema(ctx);
291        match inner {
292            SchemaRef::Schema(mut s) => {
293                if let Some(t) = s.schema_type {
294                    s.schema_type = Some(TypeArray::nullable(match t {
295                        TypeArray::Single(v) => v,
296                        TypeArray::Array(v) => v[0].clone(), // Approximate
297                    }));
298                }
299                SchemaRef::Schema(s)
300            }
301            SchemaRef::Ref { reference } => {
302                // oneOf [{$ref}, {type: null}]
303                let mut s = JsonSchema2020::new();
304                let mut ref_s = JsonSchema2020::new();
305                ref_s.reference = Some(reference);
306                let mut null_s = JsonSchema2020::new();
307                null_s.schema_type = Some(TypeArray::single("null"));
308                s.one_of = Some(vec![ref_s, null_s]);
309                SchemaRef::Schema(Box::new(s))
310            }
311            _ => inner,
312        }
313    }
314}
315
316// HashMap
317impl<T: RustApiSchema> RustApiSchema for std::collections::HashMap<String, T> {
318    fn schema(ctx: &mut SchemaCtx) -> SchemaRef {
319        let inner = T::schema(ctx);
320        let mut s = JsonSchema2020::object();
321
322        let add_prop = match inner {
323            SchemaRef::Schema(is) => AdditionalProperties::Schema(is),
324            SchemaRef::Ref { reference } => {
325                let mut rs = JsonSchema2020::new();
326                rs.reference = Some(reference);
327                AdditionalProperties::Schema(Box::new(rs))
328            }
329            _ => AdditionalProperties::Bool(true),
330        };
331
332        s.additional_properties = Some(Box::new(add_prop));
333        SchemaRef::Schema(Box::new(s))
334    }
335}
336
337// Add empty SchemaTransformer for spec.rs usage
338pub struct SchemaTransformer;
339impl SchemaTransformer {
340    pub fn transform_30_to_31(v: serde_json::Value) -> serde_json::Value {
341        v
342    }
343}