1use crate::action::Action;
2use json::{object, JsonValue};
3use std::collections::BTreeMap;
4
5#[derive(Clone)]
6pub struct Swagger {
7 openapi: String,
8 info: Info,
9 servers: Vec<Server>,
10 components: JsonValue,
11 tags: BTreeMap<String, Tag>,
12 security: Vec<JsonValue>,
13 paths: BTreeMap<String, BTreeMap<String, Api>>,
14}
15
16impl Swagger {
17 #[must_use]
18 pub fn new(version: &str, title: &str, description: &str) -> Self {
19 Self {
20 openapi: "3.0.0".to_string(),
21 info: Info::new(title, description, version),
22 servers: Vec::new(),
23 components: object! {},
24 tags: BTreeMap::new(),
25 security: Vec::new(),
26 paths: BTreeMap::new(),
27 }
28 }
29
30 #[must_use]
31 pub fn create_server(url: &str, description: &str) -> Server {
32 Server::new(url, description)
33 }
34
35 #[must_use]
36 pub fn add_server(&mut self, url: &str, description: &str) -> Server {
37 Server::new(url, description)
38 }
39
40 pub fn with_server(&mut self, url: &str, description: &str) -> &mut Self {
41 self.servers.push(Server::new(url, description));
42 self
43 }
44
45 pub fn push_server(&mut self, server: Server) -> &mut Self {
46 self.servers.push(server);
47 self
48 }
49
50 pub fn set_server(&mut self, server: Server) {
51 self.servers.push(server);
52 }
53
54 pub fn add_header(&mut self, key: &str, description: &str, example: &str) -> &mut Self {
55 self.components["parameters"]["GlobalHeader"] = object! {
56 "name": key,
57 "in": "header",
58 "description": description,
59 "required": true,
60 "schema": {
61 "type": "string",
62 "example": example
63 }
64 };
65 self
66 }
67
68 pub fn add_components_bearer_token(&mut self) -> &mut Self {
69 self.components["securitySchemes"]["BearerToken"] = object! {
70 "type": "http",
71 "scheme": "bearer",
72 "bearerFormat": "Token"
73 };
74 self.security.push(object! { "BearerToken": [] });
75 self
76 }
77
78 pub fn add_components_header(
79 &mut self,
80 key: &str,
81 description: &str,
82 example: &str,
83 ) -> &mut Self {
84 self.components["securitySchemes"][key] = object! {
85 "type": "apiKey",
86 "in": "header",
87 "name": key,
88 "description": description,
89 };
90 let mut security = object! {};
91 security[key] = example.into();
92 self.security.push(security);
93 self
94 }
95
96 pub fn add_authorization_header(&mut self, token: &str) -> &mut Self {
97 self.components["parameters"]["AuthorizationHeader"] = object! {
98 "name": "Authorization",
99 "in": "header",
100 "required": true,
101 "description": "Bearer token for authentication",
102 "schema": {
103 "type": "string",
104 "example": format!("Bearer {}", token)
105 }
106 };
107 self
108 }
109
110 pub fn set_global(&mut self, key: &str, example: &str, description: &str) -> &mut Self {
111 self.components["schemas"][key]["type"] = "string".into();
112 self.components["schemas"][key]["description"] = description.into();
113 self.components["schemas"][key]["example"] = example.into();
114 self
115 }
116
117 pub fn add_tags(&mut self, name: &str, description: &str) -> &mut Self {
118 self.tags
119 .insert(name.to_string(), Tag::new(name, description));
120 self
121 }
122
123 pub fn add_paths(&mut self, mut action: Box<dyn Action>) -> &mut Self {
124 let path = format!("/{}", action.api().replace('.', "/"));
125 let method = action.method().str().to_lowercase();
126 let api = Api::from_action(&mut action, None, &self.components);
127 self.paths.entry(path).or_default().insert(method, api);
128 self
129 }
130
131 pub fn add_path(&mut self, mut action: Box<dyn Action>) -> &mut Self {
132 let path = format!("/{}", action.path());
133 let method = action.method().str().to_lowercase();
134 let api = Api::from_action(&mut action, None, &self.components);
135 self.paths.entry(path).or_default().insert(method, api);
136 self
137 }
138
139 pub fn add_tag_paths(&mut self, tag: &str, mut action: Box<dyn Action>) -> &mut Self {
140 let path = format!("/{tag}/{}", action.api().replace('.', "/"));
141 let method = action.method().str().to_lowercase();
142 let api = Api::from_action(&mut action, Some(tag), &self.components);
143 self.paths.entry(path).or_default().insert(method, api);
144 self
145 }
146
147 pub fn json(&self) -> JsonValue {
148 let paths: BTreeMap<_, _> = self
149 .paths
150 .iter()
151 .map(|(key, value)| {
152 let methods: BTreeMap<_, _> = value
153 .iter()
154 .map(|(method, api)| (method.clone(), api.json()))
155 .collect();
156 (key.clone(), methods)
157 })
158 .collect();
159
160 object! {
161 openapi: self.openapi.clone(),
162 info: self.info.json(),
163 servers: self.servers.iter().map(Server::json).collect::<Vec<_>>(),
164 components: self.components.clone(),
165 security: self.security.clone(),
166 tags: self.tags.values().map(Tag::json).collect::<Vec<_>>(),
167 paths: paths
168 }
169 }
170}
171
172#[derive(Clone)]
173struct Info {
174 title: String,
175 description: String,
176 version: String,
177}
178
179impl Info {
180 fn new(title: &str, description: &str, version: &str) -> Self {
181 Self {
182 title: title.to_string(),
183 description: description.to_string(),
184 version: version.to_string(),
185 }
186 }
187
188 fn json(&self) -> JsonValue {
189 object! {
190 title: self.title.clone(),
191 description: self.description.clone(),
192 version: self.version.clone()
193 }
194 }
195}
196
197#[derive(Clone, Debug)]
198pub struct Server {
199 url: String,
200 description: String,
201 variables: BTreeMap<String, JsonValue>,
202}
203
204impl Server {
205 fn new(url: &str, description: &str) -> Self {
206 Self {
207 url: url.to_string(),
208 description: description.to_string(),
209 variables: BTreeMap::new(),
210 }
211 }
212
213 #[must_use]
214 pub fn json(&self) -> JsonValue {
215 object! {
216 url: self.url.clone(),
217 description: self.description.clone(),
218 variables: self.variables.clone()
219 }
220 }
221
222 pub fn set_variable(&mut self, key: &str, value: JsonValue, description: &str) -> &mut Self {
223 self.variables.insert(
224 key.to_string(),
225 object! {
226 default: value,
227 description: description
228 },
229 );
230 self
231 }
232}
233
234#[derive(Clone)]
235struct Tag {
236 name: String,
237 description: String,
238}
239
240impl Tag {
241 fn new(name: &str, description: &str) -> Self {
242 Self {
243 name: name.to_string(),
244 description: description.to_string(),
245 }
246 }
247
248 fn json(&self) -> JsonValue {
249 object! {
250 name: self.name.clone(),
251 description: self.description.clone(),
252 }
253 }
254}
255
256#[derive(Clone)]
257struct Api {
258 tags: Vec<String>,
259 summary: String,
260 description: String,
261 operation_id: String,
262 request_body: RequestBody,
263 responses: JsonValue,
264}
265
266impl Api {
267 fn from_action(
268 action: &mut Box<dyn Action>,
269 tag_prefix: Option<&str>,
270 components: &JsonValue,
271 ) -> Self {
272 let api_str = action.api();
273 let mut parts = api_str.split('.');
274 let first = parts.next().unwrap_or_default();
275 let second = parts.next().unwrap_or_default();
276 let third = parts.next().unwrap_or_default();
277
278 let tag = match tag_prefix {
279 Some(prefix) => format!("{prefix}.{first}.{second}"),
280 None => format!("{first}.{second}"),
281 };
282
283 let operation_id = format!("{first}_{second}_{third}");
284
285 let mut api = Self {
286 tags: vec![tag],
287 summary: action.title().to_string(),
288 description: action.description().to_string(),
289 operation_id,
290 request_body: RequestBody::new(components.clone()),
291 responses: object! {
292 "200": { "description": "Success" }
293 },
294 };
295
296 let params = action.params();
297 if !params.is_empty() {
298 api.request_body.set_required(true);
299 api.request_body
300 .set_content(action.content_type().str(), ¶ms);
301 }
302
303 api
304 }
305
306 #[allow(dead_code)]
307 pub fn new_tag(tag: &str, mut action: Box<dyn Action>, components: &JsonValue) -> Api {
308 Self::from_action(&mut action, Some(tag), components)
309 }
310
311 #[allow(dead_code)]
312 pub fn new(mut action: Box<dyn Action>, components: &JsonValue) -> Api {
313 Self::from_action(&mut action, None, components)
314 }
315
316 fn json(&self) -> JsonValue {
317 let mut result = object! {
318 tags: self.tags.clone(),
319 summary: self.summary.clone(),
320 description: self.description.clone(),
321 operationId: self.operation_id.clone(),
322 responses: self.responses.clone(),
323 };
324
325 if self.request_body.required {
326 result["requestBody"] = self.request_body.json();
327 }
328
329 result
330 }
331}
332
333#[derive(Clone)]
334struct RequestBody {
335 required: bool,
336 content: JsonValue,
337 components: JsonValue,
338}
339
340impl RequestBody {
341 pub fn new(components: JsonValue) -> Self {
342 Self {
343 required: false,
344 content: object! {},
345 components,
346 }
347 }
348
349 pub fn set_required(&mut self, state: bool) {
350 self.required = state;
351 }
352
353 pub fn set_content(&mut self, content_type: &str, params: &JsonValue) {
354 let schema_type = if params.is_array() { "array" } else { "object" };
355 self.content[content_type] = object! {
356 schema: object! { "type": schema_type }
357 };
358
359 match schema_type {
360 "object" => {
361 let mut schema = self.content[content_type]["schema"].clone();
362 Self::build_schema_object(&mut schema, params);
363 self.content[content_type]["schema"] = schema;
364 }
365 "array" => {
366 self.content[content_type]["schema"]["items"] = params.clone();
367 }
368 _ => {}
369 }
370
371 for (field, data) in params.entries() {
372 let example = if self.components["schemas"][field].is_empty() {
373 data["example"].clone()
374 } else {
375 self.components["schemas"][field]["example"].clone()
376 };
377 self.content[content_type]["example"][field] = example;
378 }
379 }
380
381 fn build_schema_object(data: &mut JsonValue, params: &JsonValue) {
382 Self::build_properties(data, params, true);
383 }
384
385 fn build_properties(data: &mut JsonValue, params: &JsonValue, is_root: bool) {
386 for (key, value) in params.entries() {
387 let mode = value["mode"].as_str().unwrap_or("");
388 let prop = &mut data["properties"][key];
389
390 prop["type"] = Self::mode(mode);
391
392 if let Some(desc) = value["description"].as_str() {
393 if !desc.is_empty() {
394 prop["description"] = desc.into();
395 }
396 }
397
398 if matches!(mode, "radio" | "select") {
399 prop["enum"] = value["option"].clone();
400 }
401
402 match prop["type"].as_str().unwrap_or("") {
403 "object" => {
404 Self::build_properties(prop, &value["items"], false);
405 }
406 "array" => {
407 if is_root {
408 prop["example"] = value["example"].clone();
409 prop["default"] = value["example"].clone();
410 }
411 Self::set_array_items(prop, &value["items"]);
412 }
413 _ => {
414 prop["example"] = value["example"].clone();
415 prop["default"] = value["example"].clone();
416 }
417 }
418 }
419 }
420
421 fn set_array_items(data: &mut JsonValue, params: &JsonValue) {
422 data["items"]["type"] = params["mode"].as_str().unwrap_or("string").into();
423 }
424
425 fn mode(name: &str) -> JsonValue {
426 match name {
427 "int" | "integer" | "number" => "integer",
428 "float" | "double" | "decimal" => "number",
429 "switch" | "bool" | "boolean" => "boolean",
430 "radio" | "text" | "textarea" | "password" | "email" | "date" | "datetime" | "time" => {
431 "string"
432 }
433 "select" | "array" | "list" => "array",
434 "object" | "json" | "map" => "object",
435 "file" | "image" | "binary" => "string",
436 "" => "string",
437 _ => name,
438 }
439 .into()
440 }
441
442 pub fn json(&self) -> JsonValue {
443 object! {
444 required: self.required,
445 content: self.content.clone()
446 }
447 }
448}