paperclip_core/v2/
schema.rs

1//! Traits used for code and spec generation.
2
3use super::models::{
4    DataType, DataTypeFormat, DefaultOperationRaw, DefaultSchemaRaw, Either, Resolvable,
5    SecurityScheme,
6};
7
8use std::collections::{BTreeMap, BTreeSet};
9
10/// Interface for the [`Schema`](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#schemaObject) object.
11///
12/// This is only used for resolving the definitions.
13///
14/// **NOTE:** Don't implement this by yourself! Please use the `#[api_v2_schema]`
15/// proc macro attribute instead.
16pub trait Schema: Sized {
17    /// Description for this schema, if any (`description` field).
18    fn description(&self) -> Option<&str>;
19
20    /// Reference to some other schema, if any (`$ref` field).
21    fn reference(&self) -> Option<&str>;
22
23    /// Data type of this schema, if any (`type` field).
24    fn data_type(&self) -> Option<DataType>;
25
26    /// Data type format used by this schema, if any (`format` field).
27    fn format(&self) -> Option<&DataTypeFormat>;
28
29    /// Schema for array definitions, if any (`items` field).
30    fn items(&self) -> Option<&Resolvable<Self>>;
31
32    /// Mutable access to the `items` field, if it exists.
33    fn items_mut(&mut self) -> Option<&mut Resolvable<Self>>;
34
35    /// Value schema for maps (`additional_properties` field).
36    fn additional_properties(&self) -> Option<&Either<bool, Resolvable<Self>>>;
37
38    /// Mutable access to `additional_properties` field, if it's a map.
39    fn additional_properties_mut(&mut self) -> Option<&mut Either<bool, Resolvable<Self>>>;
40
41    /// Map of names and schema for properties, if it's an object (`properties` field)
42    fn properties(&self) -> Option<&BTreeMap<String, Resolvable<Self>>>;
43
44    /// Mutable access to `properties` field.
45    fn properties_mut(&mut self) -> Option<&mut BTreeMap<String, Resolvable<Self>>>;
46
47    /// Returns the required properties (if any) for this object.
48    fn required_properties(&self) -> Option<&BTreeSet<String>>;
49
50    /// Enum variants in this schema (if any). It's `serde_json::Value`
51    /// because:
52    ///
53    /// - Enum variants are allowed to have any type of value.
54    /// - `serde_json::Value` works for both JSON and YAML.
55    fn enum_variants(&self) -> Option<&[serde_json::Value]>;
56
57    /// Returns whether this definition "is" or "has" `Any` type.
58    fn contains_any(&self) -> bool {
59        _schema_contains_any(self, vec![])
60    }
61
62    /* MARK: Resolver-specific methods. */
63
64    /// Set the reference to this schema.
65    fn set_reference(&mut self, ref_: String);
66
67    /// Set whether this definition is cyclic. This is done by the resolver.
68    fn set_cyclic(&mut self, cyclic: bool);
69
70    /// Returns whether this definition is cyclic.
71    ///
72    /// **NOTE:** This is not part of the schema object, but it's
73    /// set by the resolver using `set_cyclic` for codegen.
74    fn is_cyclic(&self) -> bool;
75
76    /// Name of this schema, if any.
77    ///
78    /// **NOTE:** This is not part of the schema object, but it's
79    /// set by the resolver using `set_name` for codegen.
80    fn name(&self) -> Option<&str>;
81
82    /// Sets the name for this schema. This is done by the resolver.
83    fn set_name(&mut self, name: &str);
84}
85
86fn _schema_contains_any<'a, S: Schema>(schema: &'a S, mut nodes: Vec<&'a str>) -> bool {
87    if schema.data_type().is_none() {
88        return true;
89    }
90
91    if let Some(name) = schema.name() {
92        if nodes.iter().any(|&n| n == name) {
93            return false; // We've encountered a cycle.
94        } else {
95            nodes.push(name);
96        }
97    }
98
99    schema
100        .properties()
101        .map(|t| {
102            t.values()
103                .any(|s| _schema_contains_any(&*s.read().unwrap(), nodes.clone()))
104        })
105        .unwrap_or(false)
106        || schema
107            .items()
108            .map(|s| _schema_contains_any(&*s.read().unwrap(), nodes.clone()))
109            .unwrap_or(false)
110        || schema
111            .additional_properties()
112            .map(|e| match e {
113                Either::Left(extra_props_allowed) => *extra_props_allowed,
114                Either::Right(s) => _schema_contains_any(&*s.read().unwrap(), nodes),
115            })
116            .unwrap_or(false)
117}
118
119/// Trait for returning OpenAPI data type and format for the implementor.
120pub trait TypedData {
121    /// The OpenAPI type for this implementor.
122    fn data_type() -> DataType {
123        DataType::Object
124    }
125
126    /// The optional OpenAPI data format for this implementor.
127    fn format() -> Option<DataTypeFormat> {
128        None
129    }
130
131    fn max() -> Option<f32> {
132        None
133    }
134
135    fn min() -> Option<f32> {
136        None
137    }
138}
139
140macro_rules! impl_type_simple {
141    ($ty:ty) => {
142        impl TypedData for $ty {}
143    };
144    ($ty:ty, $dt:expr) => {
145        impl TypedData for $ty {
146            fn data_type() -> DataType {
147                $dt
148            }
149        }
150    };
151    ($ty:ty, $dt:expr, $df:expr) => {
152        impl TypedData for $ty {
153            fn data_type() -> DataType {
154                $dt
155            }
156            fn format() -> Option<DataTypeFormat> {
157                Some($df)
158            }
159        }
160    };
161    ($ty:ty, $dt:expr, $df:expr, $min:expr, $max:expr) => {
162        impl TypedData for $ty {
163            fn data_type() -> DataType {
164                $dt
165            }
166            fn format() -> Option<DataTypeFormat> {
167                Some($df)
168            }
169            fn max() -> Option<f32> {
170                Some($max)
171            }
172            fn min() -> Option<f32> {
173                Some($min)
174            }
175        }
176    };
177}
178
179impl TypedData for &str {
180    fn data_type() -> DataType {
181        DataType::String
182    }
183}
184
185impl<T: TypedData> TypedData for &T {
186    fn data_type() -> DataType {
187        T::data_type()
188    }
189
190    fn format() -> Option<DataTypeFormat> {
191        T::format()
192    }
193}
194
195impl_type_simple!(char, DataType::String);
196impl_type_simple!(String, DataType::String);
197impl_type_simple!(PathBuf, DataType::String);
198#[cfg(feature = "camino")]
199impl_type_simple!(
200    camino::Utf8PathBuf,
201    DataType::String,
202    DataTypeFormat::Binary
203);
204impl_type_simple!(bool, DataType::Boolean);
205impl_type_simple!(f32, DataType::Number, DataTypeFormat::Float);
206impl_type_simple!(f64, DataType::Number, DataTypeFormat::Double);
207impl_type_simple!(
208    i8,
209    DataType::Integer,
210    DataTypeFormat::Int32,
211    i8::MIN as f32,
212    i8::MAX as f32
213);
214impl_type_simple!(
215    i16,
216    DataType::Integer,
217    DataTypeFormat::Int32,
218    i16::MIN as f32,
219    i16::MAX as f32
220);
221impl_type_simple!(i32, DataType::Integer, DataTypeFormat::Int32);
222impl_type_simple!(
223    u8,
224    DataType::Integer,
225    DataTypeFormat::Int32,
226    u8::MIN as f32,
227    u8::MAX as f32
228);
229impl_type_simple!(
230    u16,
231    DataType::Integer,
232    DataTypeFormat::Int32,
233    u16::MIN as f32,
234    u16::MAX as f32
235);
236impl_type_simple!(u32, DataType::Integer, DataTypeFormat::Int32);
237impl_type_simple!(i64, DataType::Integer, DataTypeFormat::Int64);
238impl_type_simple!(
239    i128,
240    DataType::Integer,
241    DataTypeFormat::Int64,
242    i128::MIN as f32,
243    i128::MAX as f32
244);
245impl_type_simple!(isize, DataType::Integer, DataTypeFormat::Int64);
246impl_type_simple!(u64, DataType::Integer, DataTypeFormat::Int64);
247impl_type_simple!(
248    u128,
249    DataType::Integer,
250    DataTypeFormat::Int64,
251    u128::MIN as f32,
252    u128::MAX as f32
253);
254impl_type_simple!(usize, DataType::Integer, DataTypeFormat::Int64);
255
256#[cfg(feature = "actix-multipart")]
257impl_type_simple!(
258    actix_multipart::Multipart,
259    DataType::File,
260    DataTypeFormat::Binary
261);
262#[cfg(feature = "actix-session")]
263impl_type_simple!(actix_session::Session);
264#[cfg(feature = "actix-identity")]
265impl_type_simple!(actix_identity::Identity);
266#[cfg(feature = "actix-files")]
267impl_type_simple!(
268    actix_files::NamedFile,
269    DataType::File,
270    DataTypeFormat::Binary
271);
272#[cfg(feature = "jiff01")]
273impl_type_simple!(jiff::Timestamp, DataType::String, DataTypeFormat::DateTime);
274#[cfg(feature = "jiff01")]
275impl_type_simple!(
276    jiff::Zoned,
277    DataType::String,
278    DataTypeFormat::Other //RFC 8536
279);
280#[cfg(feature = "jiff01")]
281impl_type_simple!(
282    jiff::civil::DateTime,
283    DataType::String,
284    DataTypeFormat::Other //2024-06-19T15:22:45
285);
286#[cfg(feature = "jiff01")]
287impl_type_simple!(jiff::civil::Date, DataType::String, DataTypeFormat::Date);
288#[cfg(feature = "jiff01")]
289impl_type_simple!(
290    jiff::civil::Time,
291    DataType::String,
292    DataTypeFormat::Other //15:22:45
293);
294#[cfg(feature = "jiff01")]
295impl_type_simple!(
296    jiff::Span,
297    DataType::String,
298    DataTypeFormat::Other //ISO 8601
299);
300#[cfg(feature = "chrono")]
301impl_type_simple!(
302    chrono::NaiveDateTime,
303    DataType::String,
304    DataTypeFormat::DateTime
305);
306#[cfg(feature = "chrono")]
307impl_type_simple!(chrono::NaiveDate, DataType::String, DataTypeFormat::Date);
308#[cfg(feature = "chrono")]
309impl_type_simple!(chrono::NaiveTime, DataType::String);
310#[cfg(feature = "rust_decimal")]
311impl_type_simple!(
312    rust_decimal::Decimal,
313    DataType::Number,
314    DataTypeFormat::Float
315);
316
317#[cfg(feature = "url")]
318impl_type_simple!(url::Url, DataType::String, DataTypeFormat::Url);
319
320#[cfg(feature = "uuid0")]
321impl_type_simple!(uuid0_dep::Uuid, DataType::String, DataTypeFormat::Uuid);
322#[cfg(feature = "uuid1")]
323impl_type_simple!(uuid1_dep::Uuid, DataType::String, DataTypeFormat::Uuid);
324
325#[cfg(feature = "chrono")]
326impl<T: chrono::offset::TimeZone> TypedData for chrono::DateTime<T> {
327    fn data_type() -> DataType {
328        DataType::String
329    }
330    fn format() -> Option<DataTypeFormat> {
331        Some(DataTypeFormat::DateTime)
332    }
333}
334
335#[cfg(feature = "chrono")]
336#[allow(deprecated)]
337impl<T: chrono::offset::TimeZone> TypedData for chrono::Date<T> {
338    fn data_type() -> DataType {
339        DataType::String
340    }
341    fn format() -> Option<DataTypeFormat> {
342        Some(DataTypeFormat::Date)
343    }
344}
345
346impl_type_simple!(std::net::IpAddr, DataType::String, DataTypeFormat::Ip);
347impl_type_simple!(std::net::Ipv4Addr, DataType::String, DataTypeFormat::IpV4);
348impl_type_simple!(std::net::Ipv6Addr, DataType::String, DataTypeFormat::IpV6);
349
350/// Represents a OpenAPI v2 schema object convertible. This is auto-implemented by
351/// framework-specific macros:
352///
353/// - [`Apiv2Schema`](https://paperclip-rs.github.io/paperclip/paperclip_actix/derive.Apiv2Schema.html)
354///   for schema objects.
355/// - [`Apiv2Security`](https://paperclip-rs.github.io/paperclip/paperclip_actix/derive.Apiv2Security.html)
356///   for security scheme objects.
357/// - [`Apiv2Header`](https://paperclip-rs.github.io/paperclip/paperclip_actix/derive.Apiv2Header.html)
358///   for header parameters objects.
359///
360/// This is implemented for primitive types by default.
361pub trait Apiv2Schema {
362    /// Name of this schema. This is the name to which the definition of the object is mapped.
363    fn name() -> Option<String> {
364        None
365    }
366
367    /// Description of this schema. In case the trait is derived, uses the documentation on the type.
368    fn description() -> &'static str {
369        ""
370    }
371
372    /// Indicates the requirement of this schema.
373    fn required() -> bool {
374        true
375    }
376
377    /// Returns the raw schema for this object.
378    fn raw_schema() -> DefaultSchemaRaw {
379        Default::default()
380    }
381
382    /// Returns the schema with a reference (if this is an object).
383    ///
384    /// Here, we set the global reference to this object using its name,
385    /// and we either remove the reference (`remove_refs`) or remove all
386    /// properties other than the reference (`retain_ref`) based on where
387    /// we're storing this object in the spec i.e., in an operation/response
388    /// or in the map of definitions.
389    ///
390    /// And we do that because at the time of this writing, statically
391    /// collecting all models for all operations involved a lot of work,
392    /// and so I went for runtime collection. Even though this happens at
393    /// runtime, we're only doing this once at the start of the application,
394    /// so it won't affect the incoming requests at all.
395    fn schema_with_ref() -> DefaultSchemaRaw {
396        let mut def = Self::raw_schema();
397        if let Some(n) = Self::name() {
398            def.reference =
399                Some(String::from("#/definitions/") + &n.replace('<', "%3C").replace('>', "%3E"));
400        } else if let Some(n) = def.name.as_ref() {
401            def.reference =
402                Some(String::from("#/definitions/") + &n.replace('<', "%3C").replace('>', "%3E"));
403        }
404        if !Self::description().is_empty() {
405            def.description = Some(Self::description().to_owned());
406        }
407
408        def
409    }
410
411    /// Returns the security scheme for this object.
412    fn security_scheme() -> Option<SecurityScheme> {
413        None
414    }
415
416    fn header_parameter_schema() -> Vec<Parameter<DefaultSchemaRaw>> {
417        vec![]
418    }
419}
420
421impl Apiv2Schema for () {}
422impl Apiv2Schema for serde_json::Value {}
423impl Apiv2Schema for serde_yaml::Value {}
424
425impl<T: TypedData> Apiv2Schema for T {
426    fn raw_schema() -> DefaultSchemaRaw {
427        DefaultSchemaRaw {
428            data_type: Some(T::data_type()),
429            format: T::format(),
430            maximum: T::max(),
431            minimum: T::min(),
432            ..Default::default()
433        }
434    }
435}
436
437#[cfg(feature = "nightly")]
438impl<T> Apiv2Schema for Option<T> {
439    default fn name() -> Option<String> {
440        None
441    }
442
443    default fn required() -> bool {
444        false
445    }
446
447    default fn raw_schema() -> DefaultSchemaRaw {
448        Default::default()
449    }
450
451    default fn security_scheme() -> Option<SecurityScheme> {
452        None
453    }
454
455    default fn header_parameter_schema() -> Vec<Parameter<DefaultSchemaRaw>> {
456        vec![]
457    }
458}
459
460impl<T: Apiv2Schema> Apiv2Schema for Option<T> {
461    fn name() -> Option<String> {
462        T::name()
463    }
464
465    fn required() -> bool {
466        false
467    }
468
469    fn raw_schema() -> DefaultSchemaRaw {
470        T::raw_schema()
471    }
472
473    fn security_scheme() -> Option<SecurityScheme> {
474        T::security_scheme()
475    }
476
477    fn header_parameter_schema() -> Vec<Parameter<DefaultSchemaRaw>> {
478        T::header_parameter_schema()
479    }
480}
481
482#[cfg(feature = "nightly")]
483impl<T, E> Apiv2Schema for Result<T, E> {
484    default fn name() -> Option<String> {
485        None
486    }
487
488    default fn raw_schema() -> DefaultSchemaRaw {
489        Default::default()
490    }
491
492    default fn security_scheme() -> Option<SecurityScheme> {
493        Default::default()
494    }
495
496    default fn header_parameter_schema() -> Vec<Parameter<DefaultSchemaRaw>> {
497        Default::default()
498    }
499}
500
501impl<T: Apiv2Schema, E> Apiv2Schema for Result<T, E> {
502    fn name() -> Option<String> {
503        T::name()
504    }
505
506    fn raw_schema() -> DefaultSchemaRaw {
507        T::raw_schema()
508    }
509
510    fn security_scheme() -> Option<SecurityScheme> {
511        T::security_scheme()
512    }
513
514    fn header_parameter_schema() -> Vec<Parameter<DefaultSchemaRaw>> {
515        T::header_parameter_schema()
516    }
517}
518
519impl<T: Apiv2Schema + Clone> Apiv2Schema for std::borrow::Cow<'_, T> {
520    fn name() -> Option<String> {
521        T::name()
522    }
523
524    fn raw_schema() -> DefaultSchemaRaw {
525        T::raw_schema()
526    }
527
528    fn security_scheme() -> Option<SecurityScheme> {
529        T::security_scheme()
530    }
531
532    fn header_parameter_schema() -> Vec<Parameter<DefaultSchemaRaw>> {
533        T::header_parameter_schema()
534    }
535}
536
537impl<T: Apiv2Schema> Apiv2Schema for &[T] {
538    fn raw_schema() -> DefaultSchemaRaw {
539        Vec::<T>::raw_schema()
540    }
541}
542
543impl<T: Apiv2Schema, const N: usize> Apiv2Schema for [T; N] {
544    fn raw_schema() -> DefaultSchemaRaw {
545        DefaultSchemaRaw {
546            data_type: Some(DataType::Array),
547            items: Some(T::schema_with_ref().into()),
548            ..Default::default()
549        }
550    }
551}
552
553macro_rules! impl_schema_array {
554    ($ty:ty) => {
555        impl<T: Apiv2Schema> Apiv2Schema for $ty {
556            fn raw_schema() -> DefaultSchemaRaw {
557                DefaultSchemaRaw {
558                    data_type: Some(DataType::Array),
559                    items: Some(T::schema_with_ref().into()),
560                    ..Default::default()
561                }
562            }
563        }
564    };
565}
566
567macro_rules! impl_schema_map {
568    ($ty:ty) => {
569        impl<K: ToString, V: Apiv2Schema> Apiv2Schema for $ty {
570            fn raw_schema() -> DefaultSchemaRaw {
571                DefaultSchemaRaw {
572                    data_type: Some(DataType::Object),
573                    extra_props: Some(Either::Right(V::schema_with_ref().into())),
574                    ..Default::default()
575                }
576            }
577        }
578    };
579}
580
581use crate::v2::models::Parameter;
582use std::{collections::*, path::PathBuf};
583
584impl_schema_array!(Vec<T>);
585impl_schema_array!(HashSet<T>);
586impl_schema_array!(LinkedList<T>);
587impl_schema_array!(VecDeque<T>);
588impl_schema_array!(BTreeSet<T>);
589impl_schema_array!(BinaryHeap<T>);
590
591impl_schema_map!(HashMap<K, V>);
592impl_schema_map!(BTreeMap<K, V>);
593
594/// Represents a OpenAPI v2 operation convertible. This is auto-implemented by
595/// framework-specific macros:
596///
597/// - [`paperclip_actix::api_v2_operation`](https://paperclip-rs.github.io/paperclip/paperclip_actix/attr.api_v2_operation.html).
598///
599/// **NOTE:** The type parameters specified here aren't used by the trait itself,
600/// but *can* be used for constraining stuff in framework-related impls.
601pub trait Apiv2Operation {
602    /// Returns the definition for this operation.
603    fn operation() -> DefaultOperationRaw;
604
605    /// Returns a map of security definitions that will be merged globally.
606    fn security_definitions() -> BTreeMap<String, SecurityScheme>;
607
608    /// Returns the definitions used by this operation.
609    fn definitions() -> BTreeMap<String, DefaultSchemaRaw>;
610
611    fn is_visible() -> bool {
612        true
613    }
614}
615
616/// Represents a OpenAPI v2 error convertible. This is auto-implemented by
617/// framework-specific macros:
618///
619/// - [`paperclip_actix::api_v2_errors`](https://paperclip-rs.github.io/paperclip/paperclip_actix/attr.api_v2_errors.html).
620pub trait Apiv2Errors {
621    const ERROR_MAP: &'static [(u16, &'static str)] = &[];
622    fn update_error_definitions(_op: &mut DefaultOperationRaw) {}
623    fn update_definitions(_map: &mut BTreeMap<String, DefaultSchemaRaw>) {}
624}
625
626impl Apiv2Errors for () {}
627#[cfg(feature = "actix-base")]
628impl Apiv2Errors for actix_web::Error {}