1use alux_shape::ShapeOf;
4use alux_shape_jsonschema::{JsonSchema, JsonSchemaShape};
5use core::marker::PhantomData;
6use serde_json::Value;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum OpenApiSource {
11 Path,
13 Query,
15 Header,
17 Cookie,
19 Body(&'static str),
21 Unstated,
23}
24
25#[derive(Debug, Clone)]
27pub struct OpenApiArgument {
28 pub source: OpenApiSource,
30 pub stated: OpenApiStated,
32}
33
34#[derive(Debug, Clone)]
40pub enum OpenApiStated {
41 Whole(Value),
43 Named(Vec<OpenApiNamed>),
45}
46
47#[derive(Debug, Clone)]
49pub struct OpenApiNamed {
50 pub name: String,
52 pub schema: Value,
54 pub required: bool,
56}
57
58fn 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
86fn 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
116fn whole(_schema: &JsonSchemaShape, stated: Value) -> OpenApiStated {
118 OpenApiStated::Whole(stated)
119}
120
121pub trait OpenApiInputAlg {
123 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
138pub 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
151pub trait OpenApiInputsAlg {
153 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);