Skip to main content

alux_http_openapi/
input.rs

1//! States where each argument comes from, in the vocabulary a document reads.
2
3use alux_shape::ShapeOf;
4use alux_shape_jsonschema::{JsonSchema, JsonSchemaShape};
5use core::marker::PhantomData;
6use serde_json::Value;
7
8/// Where a document says one argument comes from.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum OpenApiSource {
11    /// A segment the path binds.
12    Path,
13    /// A value in the query string.
14    Query,
15    /// A header the caller sent.
16    Header,
17    /// A cookie the caller sent.
18    Cookie,
19    /// The request body, read as the stated media type.
20    Body(&'static str),
21    /// Something the caller never states, which a document therefore does not describe.
22    Unstated,
23}
24
25/// One argument, as a document describes it.
26#[derive(Debug, Clone)]
27pub struct OpenApiArgument {
28    /// Where the argument comes from.
29    pub source: OpenApiSource,
30    /// What the argument states there.
31    pub stated: OpenApiStated,
32}
33
34/// What one argument states where it comes from.
35///
36/// A body is one value, so an argument read from it states one schema. A query string, a header
37/// collection, and a cookie collection are names and values, so an argument read from one of those
38/// states a member for each name, which is what a document keys a parameter by.
39#[derive(Debug, Clone)]
40pub enum OpenApiStated {
41    /// One value, which is what a body carries.
42    Whole(Value),
43    /// One value per name, which is what a collection of names and values carries.
44    Named(Vec<OpenApiNamed>),
45}
46
47/// One named value an argument is read from.
48#[derive(Debug, Clone)]
49pub struct OpenApiNamed {
50    /// The name this value is stated under.
51    pub name: String,
52    /// The schema this value carries.
53    pub schema: Value,
54    /// Whether a caller must state it.
55    pub required: bool,
56}
57
58/// Reads what a shape states as the named values a collection carries.
59///
60/// A product read from names and values is one parameter per member, not one parameter carrying the
61/// product. Anything else is stated whole, which is the only thing a document can say about it.
62fn named_values(schema: &JsonSchemaShape, stated: Value) -> OpenApiStated {
63    let resolved = resolved(schema, &stated);
64    let Some(object) = resolved.as_object() else {
65        return OpenApiStated::Whole(stated);
66    };
67    if object.get("type").and_then(Value::as_str) != Some("object") {
68        return OpenApiStated::Whole(stated);
69    }
70    let Some(properties) = object.get("properties").and_then(Value::as_object) else {
71        return OpenApiStated::Whole(stated);
72    };
73    let required = object.get("required").and_then(Value::as_array).cloned().unwrap_or_default();
74    let named = properties
75        .iter()
76        .map(|(name, schema)| OpenApiNamed {
77            name: name.clone(),
78            schema: schema.clone(),
79            required: required.iter().any(|stated| stated.as_str() == Some(name)),
80        })
81        .collect();
82
83    OpenApiStated::Named(named)
84}
85
86/// Returns the shape a reference names, or the shape itself where it names none.
87fn resolved(schema: &JsonSchemaShape, stated: &Value) -> Value {
88    let Some(reference) = stated.get("$ref").and_then(Value::as_str) else {
89        return stated.clone();
90    };
91    let named = reference.rsplit('/').next().unwrap_or_default();
92
93    schema.definitions().get(named).cloned().unwrap_or_else(|| stated.clone())
94}
95
96macro_rules! openapi_inputs {
97    ($($marker:ident => $source:expr, $stated:ident, $meaning:literal),+ $(,)?) => {
98        $(
99            #[doc = concat!("Describes ", $meaning, " in a document.")]
100            pub struct $marker<Input>(PhantomData<Input>);
101
102            impl<Input> OpenApiInputAlg for $marker<Input>
103            where
104                Input: ShapeOf<JsonSchemaShape, Shape = JsonSchema>,
105            {
106                fn describe(schema: &JsonSchemaShape) -> OpenApiArgument {
107                    let stated = Input::shape_of(schema).into_value();
108
109                    OpenApiArgument { source: $source, stated: $stated(schema, stated) }
110                }
111            }
112        )+
113    };
114}
115
116/// Reads a shape stated whole, which is what a body carries.
117fn whole(_schema: &JsonSchemaShape, stated: Value) -> OpenApiStated {
118    OpenApiStated::Whole(stated)
119}
120
121/// States how a document describes one argument.
122pub trait OpenApiInputAlg {
123    /// Describes this argument, naming the shapes it states along the way.
124    fn describe(schema: &JsonSchemaShape) -> OpenApiArgument;
125}
126
127openapi_inputs! {
128    OpenApiPathInput      => OpenApiSource::Path, whole, "a segment the path binds",
129    OpenApiQueryInput     => OpenApiSource::Query, named_values, "a value in the query string",
130    OpenApiHeaderInput    => OpenApiSource::Header, named_values, "a header the caller sent",
131    OpenApiCookieInput    => OpenApiSource::Cookie, named_values, "a cookie the caller sent",
132    OpenApiBodyInput      => OpenApiSource::Body("application/json"), whole, "a request body read as a document",
133    OpenApiFormInput      => OpenApiSource::Body("application/x-www-form-urlencoded"), whole, "a form-encoded request body",
134    OpenApiMultipartInput => OpenApiSource::Body("multipart/form-data"), whole, "a request body arriving as parts",
135    OpenApiRawBodyInput   => OpenApiSource::Body("application/octet-stream"), whole, "a request body taken as it arrived",
136}
137
138/// Describes an argument a caller never states.
139///
140/// An endpoint context is how a request reaches a handler, not something a caller sends, so a
141/// document describes no parameter for it. The shape is still read, because the argument still has
142/// one.
143pub struct OpenApiUnstatedInput<Input>(PhantomData<Input>);
144
145impl<Input> OpenApiInputAlg for OpenApiUnstatedInput<Input> {
146    fn describe(_schema: &JsonSchemaShape) -> OpenApiArgument {
147        OpenApiArgument { source: OpenApiSource::Unstated, stated: OpenApiStated::Whole(Value::Null) }
148    }
149}
150
151/// Describes the whole argument product one endpoint states, in declaration order.
152pub trait OpenApiInputsAlg {
153    /// Describes each argument, in the order the declaration reads them.
154    fn describe(schema: &JsonSchemaShape) -> Vec<OpenApiArgument>;
155}
156
157impl OpenApiInputsAlg for () {
158    fn describe(_schema: &JsonSchemaShape) -> Vec<OpenApiArgument> {
159        Vec::new()
160    }
161}
162
163macro_rules! openapi_products {
164    ($($input:ident),+ $(,)?) => {
165        impl<$($input),+> OpenApiInputsAlg for ($($input,)+)
166        where
167            $($input: OpenApiInputAlg,)+
168        {
169            fn describe(schema: &JsonSchemaShape) -> Vec<OpenApiArgument> {
170                vec![$($input::describe(schema),)+]
171            }
172        }
173    };
174}
175
176openapi_products!(I1);
177openapi_products!(I1, I2);
178openapi_products!(I1, I2, I3);
179openapi_products!(I1, I2, I3, I4);
180openapi_products!(I1, I2, I3, I4, I5);
181openapi_products!(I1, I2, I3, I4, I5, I6);
182openapi_products!(I1, I2, I3, I4, I5, I6, I7);
183openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8);
184openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9);
185openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9, I10);
186openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11);
187openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12);
188openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13);
189openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14);
190openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15);
191openapi_products!(I1, I2, I3, I4, I5, I6, I7, I8, I9, I10, I11, I12, I13, I14, I15, I16);