1use crate::{OpenApiAnswer, OpenApiArgument, OpenApiSource, OpenApiStated};
4use alux_http::{HttpMethod, HttpSelectorAlg, RouteAlg, RoutePath, SelectorAlg, describe_path, write_header_name};
5use serde_json::{Map, Value, json};
6use std::collections::BTreeMap;
7
8#[derive(Debug, Clone, PartialEq, Eq)]
9enum OpenApiSelectorPart {
10 Method(HttpMethod),
11 Path(RoutePath),
12 Prefix(RoutePath),
13}
14
15#[derive(Debug, Clone, Default, PartialEq, Eq)]
17pub struct OpenApiSelector {
18 parts: Vec<OpenApiSelectorPart>,
19}
20
21impl OpenApiSelector {
22 pub fn path(&self) -> String {
27 describe_path(self.parts.iter().filter_map(|part| match part {
28 OpenApiSelectorPart::Path(path) | OpenApiSelectorPart::Prefix(path) => Some(path),
29 OpenApiSelectorPart::Method(_) => None,
30 }))
31 }
32
33 pub fn label(&self) -> String {
35 let method = self.method().map_or("*", HttpMethod::label);
36 format!("{method} {}", self.path())
37 }
38
39 fn method(&self) -> Option<HttpMethod> {
40 self.parts.iter().rev().find_map(|part| match part {
41 OpenApiSelectorPart::Method(method) => Some(*method),
42 OpenApiSelectorPart::Path(_) | OpenApiSelectorPart::Prefix(_) => None,
43 })
44 }
45}
46
47#[derive(Debug, Clone)]
49pub struct OpenApiEndpoint {
50 pub(crate) operation: &'static str,
51 pub(crate) doc: &'static str,
52 pub(crate) arguments: Vec<(&'static str, OpenApiArgument)>,
53 pub(crate) answers: Vec<OpenApiAnswer>,
54}
55
56impl OpenApiEndpoint {
57 pub fn operation(&self) -> &'static str {
59 self.operation
60 }
61
62 fn documented(&self) -> (Option<&str>, Option<&str>) {
67 let doc = self.doc.trim();
68 let (summary, description) = doc.split_once('\n').unwrap_or((doc, ""));
69
70 (Some(summary.trim()).filter(|text| !text.is_empty()), Some(description.trim()).filter(|text| !text.is_empty()))
71 }
72
73 fn parameters(&self) -> Vec<Value> {
74 self.arguments
75 .iter()
76 .filter_map(|(name, argument)| {
77 let location = match argument.source {
78 OpenApiSource::Path => "path",
79 OpenApiSource::Query => "query",
80 OpenApiSource::Header => "header",
81 OpenApiSource::Cookie => "cookie",
82 OpenApiSource::Body(_) | OpenApiSource::Unstated => return None,
83 };
84
85 Some(match &argument.stated {
86 OpenApiStated::Whole(schema) => {
88 vec![json!({ "name": name, "in": location, "required": location == "path", "schema": schema })]
89 }
90 OpenApiStated::Named(named) => named
92 .iter()
93 .map(|named| {
94 json!({
95 "name": stated_as(argument.source, &named.name),
96 "in": location,
97 "required": named.required,
98 "schema": named.schema,
99 })
100 })
101 .collect(),
102 })
103 })
104 .flatten()
105 .collect()
106 }
107
108 fn request_body(&self) -> Option<Value> {
109 self.arguments.iter().find_map(|(_, argument)| match (argument.source, &argument.stated) {
110 (OpenApiSource::Body(content_type), OpenApiStated::Whole(schema)) => Some(json!({
111 "required": true,
112 "content": { content_type: { "schema": schema } },
113 })),
114 _ => None,
115 })
116 }
117
118 fn responses(&self) -> Value {
119 let mut responses = Map::new();
122 for answer in &self.answers {
123 let stated = if answer.status.is_success() { self.documented().0.unwrap_or_default() } else { "" };
124 let mut described = match (answer.content_type, &answer.schema) {
125 (Some(content_type), Some(schema)) => json!({
126 "description": stated,
127 "content": { content_type: { "schema": schema } },
128 }),
129 _ => json!({ "description": stated }),
130 };
131 if !answer.headers.is_empty()
133 && let Some(described) = described.as_object_mut()
134 {
135 let carried =
136 answer.headers.iter().map(|name| ((*name).to_owned(), json!({ "schema": { "type": "string" } })));
137 described.insert("headers".into(), Value::Object(carried.collect()));
138 }
139 responses.insert(answer.status.code().to_string(), described);
140 }
141
142 Value::Object(responses)
143 }
144
145 fn operation_object(&self) -> Value {
146 let mut described = Map::new();
147 described.insert("operationId".into(), self.operation.into());
148 let (summary, description) = self.documented();
149 if let Some(summary) = summary {
150 described.insert("summary".into(), summary.into());
151 }
152 if let Some(description) = description {
153 described.insert("description".into(), description.into());
154 }
155 let parameters = self.parameters();
156 if !parameters.is_empty() {
157 described.insert("parameters".into(), Value::Array(parameters));
158 }
159 if let Some(body) = self.request_body() {
160 described.insert("requestBody".into(), body);
161 }
162 described.insert("responses".into(), self.responses());
163
164 Value::Object(described)
165 }
166}
167
168fn stated_as(source: OpenApiSource, name: &str) -> String {
173 match source {
174 OpenApiSource::Header => write_header_name(name),
175 _ => name.to_owned(),
176 }
177}
178
179#[derive(Debug, Clone)]
180struct OpenApiRouteEntry {
181 selector: OpenApiSelector,
182 endpoint: OpenApiEndpoint,
183}
184
185#[derive(Debug, Clone, Default)]
187pub struct OpenApiRoute {
188 entries: Vec<OpenApiRouteEntry>,
189}
190
191impl OpenApiRoute {
192 pub fn labels(&self) -> Vec<String> {
194 self.entries.iter().map(|entry| entry.selector.label()).collect()
195 }
196
197 pub fn paths(&self) -> Vec<String> {
199 self.entries.iter().map(|entry| entry.selector.path()).collect()
200 }
201
202 pub fn operations(&self) -> Vec<&'static str> {
204 self.entries.iter().map(|entry| entry.endpoint.operation).collect()
205 }
206
207 pub fn paths_object(&self) -> Value {
211 let mut paths = BTreeMap::<String, Map<String, Value>>::new();
212 for entry in &self.entries {
213 let Some(method) = entry.selector.method() else { continue };
214 let methods = paths.entry(entry.selector.path()).or_default();
215 methods.insert(method.label().to_lowercase(), entry.endpoint.operation_object());
216 }
217
218 Value::Object(paths.into_iter().map(|(path, methods)| (path, Value::Object(methods))).collect())
219 }
220}
221
222#[derive(Debug, Default)]
224pub struct OpenApiRouteImpl;
225
226impl SelectorAlg for OpenApiRouteImpl {
227 type Selector = OpenApiSelector;
228
229 fn identity(&self) -> OpenApiSelector {
230 OpenApiSelector::default()
231 }
232
233 fn compose(&self, mut first: OpenApiSelector, second: OpenApiSelector) -> OpenApiSelector {
234 first.parts.extend(second.parts);
235 first
236 }
237}
238
239impl RouteAlg for OpenApiRouteImpl {
240 type Route = OpenApiRoute;
241 type Selector = OpenApiSelector;
242 type Endpoint = OpenApiEndpoint;
243
244 fn initial(&self) -> OpenApiRoute {
245 OpenApiRoute::default()
246 }
247
248 fn coproduct(&self, mut left: OpenApiRoute, right: OpenApiRoute) -> OpenApiRoute {
249 left.entries.extend(right.entries);
250 left
251 }
252
253 fn precompose(&self, selector: OpenApiSelector, mut route: OpenApiRoute) -> OpenApiRoute {
254 for entry in &mut route.entries {
255 entry.selector = self.compose(selector.clone(), core::mem::take(&mut entry.selector));
256 }
257 route
258 }
259
260 fn lift(&self, endpoint: OpenApiEndpoint) -> OpenApiRoute {
261 OpenApiRoute { entries: vec![OpenApiRouteEntry { selector: self.identity(), endpoint }] }
262 }
263}
264
265impl HttpSelectorAlg for OpenApiRouteImpl {
266 type Selector = OpenApiSelector;
267
268 fn http_method(&self, method: HttpMethod) -> OpenApiSelector {
269 OpenApiSelector { parts: vec![OpenApiSelectorPart::Method(method)] }
270 }
271
272 fn http_path(&self, path: &RoutePath) -> OpenApiSelector {
273 OpenApiSelector { parts: vec![OpenApiSelectorPart::Path(path.clone())] }
274 }
275
276 fn http_prefix(&self, prefix: &RoutePath) -> OpenApiSelector {
277 OpenApiSelector { parts: vec![OpenApiSelectorPart::Prefix(prefix.clone())] }
278 }
279}