Skip to main content

br_addon/
swagger.rs

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(), &params);
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}