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 a unique name for this type, including generic parameters.
156    /// Used for preventing name collisions in schema registry.
157    fn name() -> std::borrow::Cow<'static, str> {
158        std::borrow::Cow::Borrowed("Unknown")
159    }
160
161    /// Get field schemas if this type is a struct (for Query params extraction)
162    fn field_schemas(_ctx: &mut SchemaCtx) -> Option<BTreeMap<String, SchemaRef>> {
163        None
164    }
165}
166
167// Primitives
168impl RustApiSchema for String {
169    fn schema(_: &mut SchemaCtx) -> SchemaRef {
170        SchemaRef::Schema(Box::new(JsonSchema2020::string()))
171    }
172    fn name() -> std::borrow::Cow<'static, str> {
173        std::borrow::Cow::Borrowed("String")
174    }
175}
176impl RustApiSchema for &str {
177    fn schema(_: &mut SchemaCtx) -> SchemaRef {
178        SchemaRef::Schema(Box::new(JsonSchema2020::string()))
179    }
180    fn name() -> std::borrow::Cow<'static, str> {
181        std::borrow::Cow::Borrowed("String")
182    }
183}
184impl RustApiSchema for bool {
185    fn schema(_: &mut SchemaCtx) -> SchemaRef {
186        SchemaRef::Schema(Box::new(JsonSchema2020::boolean()))
187    }
188    fn name() -> std::borrow::Cow<'static, str> {
189        std::borrow::Cow::Borrowed("Boolean")
190    }
191}
192impl RustApiSchema for i32 {
193    fn schema(_: &mut SchemaCtx) -> SchemaRef {
194        let mut s = JsonSchema2020::integer();
195        s.format = Some("int32".to_string());
196        SchemaRef::Schema(Box::new(s))
197    }
198    fn name() -> std::borrow::Cow<'static, str> {
199        std::borrow::Cow::Borrowed("Int32")
200    }
201}
202impl RustApiSchema for i64 {
203    fn schema(_: &mut SchemaCtx) -> SchemaRef {
204        let mut s = JsonSchema2020::integer();
205        s.format = Some("int64".to_string());
206        SchemaRef::Schema(Box::new(s))
207    }
208    fn name() -> std::borrow::Cow<'static, str> {
209        std::borrow::Cow::Borrowed("Int64")
210    }
211}
212impl RustApiSchema for f64 {
213    fn schema(_: &mut SchemaCtx) -> SchemaRef {
214        let mut s = JsonSchema2020::number();
215        s.format = Some("double".to_string());
216        SchemaRef::Schema(Box::new(s))
217    }
218    fn name() -> std::borrow::Cow<'static, str> {
219        std::borrow::Cow::Borrowed("Float64")
220    }
221}
222impl RustApiSchema for f32 {
223    fn schema(_: &mut SchemaCtx) -> SchemaRef {
224        let mut s = JsonSchema2020::number();
225        s.format = Some("float".to_string());
226        SchemaRef::Schema(Box::new(s))
227    }
228    fn name() -> std::borrow::Cow<'static, str> {
229        std::borrow::Cow::Borrowed("Float32")
230    }
231}
232
233impl RustApiSchema for i8 {
234    fn schema(_: &mut SchemaCtx) -> SchemaRef {
235        let mut s = JsonSchema2020::integer();
236        s.format = Some("int8".to_string());
237        SchemaRef::Schema(Box::new(s))
238    }
239    fn name() -> std::borrow::Cow<'static, str> {
240        std::borrow::Cow::Borrowed("Int8")
241    }
242}
243impl RustApiSchema for i16 {
244    fn schema(_: &mut SchemaCtx) -> SchemaRef {
245        let mut s = JsonSchema2020::integer();
246        s.format = Some("int16".to_string());
247        SchemaRef::Schema(Box::new(s))
248    }
249    fn name() -> std::borrow::Cow<'static, str> {
250        std::borrow::Cow::Borrowed("Int16")
251    }
252}
253impl RustApiSchema for isize {
254    fn schema(_: &mut SchemaCtx) -> SchemaRef {
255        let mut s = JsonSchema2020::integer();
256        s.format = Some("int64".to_string());
257        SchemaRef::Schema(Box::new(s))
258    }
259    fn name() -> std::borrow::Cow<'static, str> {
260        std::borrow::Cow::Borrowed("Int64")
261    }
262}
263impl RustApiSchema for u8 {
264    fn schema(_: &mut SchemaCtx) -> SchemaRef {
265        let mut s = JsonSchema2020::integer();
266        s.format = Some("uint8".to_string());
267        SchemaRef::Schema(Box::new(s))
268    }
269    fn name() -> std::borrow::Cow<'static, str> {
270        std::borrow::Cow::Borrowed("Uint8")
271    }
272}
273impl RustApiSchema for u16 {
274    fn schema(_: &mut SchemaCtx) -> SchemaRef {
275        let mut s = JsonSchema2020::integer();
276        s.format = Some("uint16".to_string());
277        SchemaRef::Schema(Box::new(s))
278    }
279    fn name() -> std::borrow::Cow<'static, str> {
280        std::borrow::Cow::Borrowed("Uint16")
281    }
282}
283impl RustApiSchema for u32 {
284    fn schema(_: &mut SchemaCtx) -> SchemaRef {
285        let mut s = JsonSchema2020::integer();
286        s.format = Some("uint32".to_string());
287        SchemaRef::Schema(Box::new(s))
288    }
289    fn name() -> std::borrow::Cow<'static, str> {
290        std::borrow::Cow::Borrowed("Uint32")
291    }
292}
293impl RustApiSchema for u64 {
294    fn schema(_: &mut SchemaCtx) -> SchemaRef {
295        let mut s = JsonSchema2020::integer();
296        s.format = Some("uint64".to_string());
297        SchemaRef::Schema(Box::new(s))
298    }
299    fn name() -> std::borrow::Cow<'static, str> {
300        std::borrow::Cow::Borrowed("Uint64")
301    }
302}
303impl RustApiSchema for usize {
304    fn schema(_: &mut SchemaCtx) -> SchemaRef {
305        let mut s = JsonSchema2020::integer();
306        s.format = Some("uint64".to_string());
307        SchemaRef::Schema(Box::new(s))
308    }
309    fn name() -> std::borrow::Cow<'static, str> {
310        std::borrow::Cow::Borrowed("Uint64")
311    }
312}
313
314// Vec
315impl<T: RustApiSchema> RustApiSchema for Vec<T> {
316    fn schema(ctx: &mut SchemaCtx) -> SchemaRef {
317        match T::schema(ctx) {
318            SchemaRef::Schema(s) => SchemaRef::Schema(Box::new(JsonSchema2020::array(*s))),
319            SchemaRef::Ref { reference } => {
320                // If T is a ref, items: {$ref: ...}
321                let mut s = JsonSchema2020::new();
322                s.schema_type = Some(TypeArray::single("array"));
323                let mut ref_schema = JsonSchema2020::new();
324                ref_schema.reference = Some(reference);
325                s.items = Some(Box::new(ref_schema));
326                SchemaRef::Schema(Box::new(s))
327            }
328            SchemaRef::Inline(_) => SchemaRef::Schema(Box::new(JsonSchema2020 {
329                schema_type: Some(TypeArray::single("array")),
330                // Inline not easily convertible to JsonSchema2020 without parsing
331                // Fallback to minimal array
332                ..Default::default()
333            })),
334        }
335    }
336    fn name() -> std::borrow::Cow<'static, str> {
337        format!("Array_{}", T::name()).into()
338    }
339}
340
341// Option
342impl<T: RustApiSchema> RustApiSchema for Option<T> {
343    fn schema(ctx: &mut SchemaCtx) -> SchemaRef {
344        let inner = T::schema(ctx);
345        match inner {
346            SchemaRef::Schema(mut s) => {
347                if let Some(t) = s.schema_type {
348                    s.schema_type = Some(TypeArray::nullable(match t {
349                        TypeArray::Single(v) => v,
350                        TypeArray::Array(v) => v[0].clone(), // Approximate
351                    }));
352                }
353                SchemaRef::Schema(s)
354            }
355            SchemaRef::Ref { reference } => {
356                // oneOf [{$ref}, {type: null}]
357                let mut s = JsonSchema2020::new();
358                let mut ref_s = JsonSchema2020::new();
359                ref_s.reference = Some(reference);
360                let mut null_s = JsonSchema2020::new();
361                null_s.schema_type = Some(TypeArray::single("null"));
362                s.one_of = Some(vec![ref_s, null_s]);
363                SchemaRef::Schema(Box::new(s))
364            }
365            _ => inner,
366        }
367    }
368    fn name() -> std::borrow::Cow<'static, str> {
369        format!("Option_{}", T::name()).into()
370    }
371}
372
373// HashMap
374impl<T: RustApiSchema> RustApiSchema for std::collections::HashMap<String, T> {
375    fn schema(ctx: &mut SchemaCtx) -> SchemaRef {
376        let inner = T::schema(ctx);
377        let mut s = JsonSchema2020::object();
378
379        let add_prop = match inner {
380            SchemaRef::Schema(is) => AdditionalProperties::Schema(is),
381            SchemaRef::Ref { reference } => {
382                let mut rs = JsonSchema2020::new();
383                rs.reference = Some(reference);
384                AdditionalProperties::Schema(Box::new(rs))
385            }
386            _ => AdditionalProperties::Bool(true),
387        };
388
389        s.additional_properties = Some(Box::new(add_prop));
390        SchemaRef::Schema(Box::new(s))
391    }
392    fn name() -> std::borrow::Cow<'static, str> {
393        format!("Map_{}", T::name()).into()
394    }
395}
396
397// Add empty SchemaTransformer for spec.rs usage
398pub struct SchemaTransformer;
399impl SchemaTransformer {
400    pub fn transform_30_to_31(v: serde_json::Value) -> serde_json::Value {
401        v
402    }
403}