use crate::action::Action;
use json::{object, JsonValue};
use std::collections::BTreeMap;
#[derive(Clone)]
pub struct Swagger {
openapi: String,
info: Info,
servers: Vec<Server>,
components: JsonValue,
tags: BTreeMap<String, Tag>,
security: Vec<JsonValue>,
paths: BTreeMap<String, BTreeMap<String, Api>>,
}
impl Swagger {
#[must_use]
pub fn new(version: &str, title: &str, description: &str) -> Self {
Self {
openapi: "3.0.0".to_string(),
info: Info::new(title, description, version),
servers: Vec::new(),
components: object! {},
tags: BTreeMap::new(),
security: Vec::new(),
paths: BTreeMap::new(),
}
}
#[must_use]
pub fn create_server(url: &str, description: &str) -> Server {
Server::new(url, description)
}
#[must_use]
pub fn add_server(&mut self, url: &str, description: &str) -> Server {
Server::new(url, description)
}
pub fn with_server(&mut self, url: &str, description: &str) -> &mut Self {
self.servers.push(Server::new(url, description));
self
}
pub fn push_server(&mut self, server: Server) -> &mut Self {
self.servers.push(server);
self
}
pub fn set_server(&mut self, server: Server) {
self.servers.push(server);
}
pub fn add_header(&mut self, key: &str, description: &str, example: &str) -> &mut Self {
self.components["parameters"]["GlobalHeader"] = object! {
"name": key,
"in": "header",
"description": description,
"required": true,
"schema": {
"type": "string",
"example": example
}
};
self
}
pub fn add_components_bearer_token(&mut self) -> &mut Self {
self.components["securitySchemes"]["BearerToken"] = object! {
"type": "http",
"scheme": "bearer",
"bearerFormat": "Token"
};
self.security.push(object! { "BearerToken": [] });
self
}
pub fn add_components_header(
&mut self,
key: &str,
description: &str,
example: &str,
) -> &mut Self {
self.components["securitySchemes"][key] = object! {
"type": "apiKey",
"in": "header",
"name": key,
"description": description,
};
let mut security = object! {};
security[key] = example.into();
self.security.push(security);
self
}
pub fn add_authorization_header(&mut self, token: &str) -> &mut Self {
self.components["parameters"]["AuthorizationHeader"] = object! {
"name": "Authorization",
"in": "header",
"required": true,
"description": "Bearer token for authentication",
"schema": {
"type": "string",
"example": format!("Bearer {}", token)
}
};
self
}
pub fn set_global(&mut self, key: &str, example: &str, description: &str) -> &mut Self {
self.components["schemas"][key]["type"] = "string".into();
self.components["schemas"][key]["description"] = description.into();
self.components["schemas"][key]["example"] = example.into();
self
}
pub fn add_tags(&mut self, name: &str, description: &str) -> &mut Self {
self.tags
.insert(name.to_string(), Tag::new(name, description));
self
}
pub fn add_paths(&mut self, mut action: Box<dyn Action>) -> &mut Self {
let path = format!("/{}", action.api().replace('.', "/"));
let method = action.method().str().to_lowercase();
let api = Api::from_action(&mut action, None, &self.components);
self.paths.entry(path).or_default().insert(method, api);
self
}
pub fn add_path(&mut self, mut action: Box<dyn Action>) -> &mut Self {
let path = format!("/{}", action.path());
let method = action.method().str().to_lowercase();
let api = Api::from_action(&mut action, None, &self.components);
self.paths.entry(path).or_default().insert(method, api);
self
}
pub fn add_tag_paths(&mut self, tag: &str, mut action: Box<dyn Action>) -> &mut Self {
let path = format!("/{tag}/{}", action.api().replace('.', "/"));
let method = action.method().str().to_lowercase();
let api = Api::from_action(&mut action, Some(tag), &self.components);
self.paths.entry(path).or_default().insert(method, api);
self
}
pub fn json(&self) -> JsonValue {
let paths: BTreeMap<_, _> = self
.paths
.iter()
.map(|(key, value)| {
let methods: BTreeMap<_, _> = value
.iter()
.map(|(method, api)| (method.clone(), api.json()))
.collect();
(key.clone(), methods)
})
.collect();
object! {
openapi: self.openapi.clone(),
info: self.info.json(),
servers: self.servers.iter().map(Server::json).collect::<Vec<_>>(),
components: self.components.clone(),
security: self.security.clone(),
tags: self.tags.values().map(Tag::json).collect::<Vec<_>>(),
paths: paths
}
}
}
#[derive(Clone)]
struct Info {
title: String,
description: String,
version: String,
}
impl Info {
fn new(title: &str, description: &str, version: &str) -> Self {
Self {
title: title.to_string(),
description: description.to_string(),
version: version.to_string(),
}
}
fn json(&self) -> JsonValue {
object! {
title: self.title.clone(),
description: self.description.clone(),
version: self.version.clone()
}
}
}
#[derive(Clone, Debug)]
pub struct Server {
url: String,
description: String,
variables: BTreeMap<String, JsonValue>,
}
impl Server {
fn new(url: &str, description: &str) -> Self {
Self {
url: url.to_string(),
description: description.to_string(),
variables: BTreeMap::new(),
}
}
#[must_use]
pub fn json(&self) -> JsonValue {
object! {
url: self.url.clone(),
description: self.description.clone(),
variables: self.variables.clone()
}
}
pub fn set_variable(&mut self, key: &str, value: JsonValue, description: &str) -> &mut Self {
self.variables.insert(
key.to_string(),
object! {
default: value,
description: description
},
);
self
}
}
#[derive(Clone)]
struct Tag {
name: String,
description: String,
}
impl Tag {
fn new(name: &str, description: &str) -> Self {
Self {
name: name.to_string(),
description: description.to_string(),
}
}
fn json(&self) -> JsonValue {
object! {
name: self.name.clone(),
description: self.description.clone(),
}
}
}
#[derive(Clone)]
struct Api {
tags: Vec<String>,
summary: String,
description: String,
operation_id: String,
request_body: RequestBody,
responses: JsonValue,
}
impl Api {
fn from_action(
action: &mut Box<dyn Action>,
tag_prefix: Option<&str>,
components: &JsonValue,
) -> Self {
let api_str = action.api();
let mut parts = api_str.split('.');
let first = parts.next().unwrap_or_default();
let second = parts.next().unwrap_or_default();
let third = parts.next().unwrap_or_default();
let tag = match tag_prefix {
Some(prefix) => format!("{prefix}.{first}.{second}"),
None => format!("{first}.{second}"),
};
let operation_id = format!("{first}_{second}_{third}");
let mut api = Self {
tags: vec![tag],
summary: action.title().to_string(),
description: action.description().to_string(),
operation_id,
request_body: RequestBody::new(components.clone()),
responses: object! {
"200": { "description": "Success" }
},
};
let params = action.params();
if !params.is_empty() {
api.request_body.set_required(true);
api.request_body
.set_content(action.content_type().str(), ¶ms);
}
api
}
#[allow(dead_code)]
pub fn new_tag(tag: &str, mut action: Box<dyn Action>, components: &JsonValue) -> Api {
Self::from_action(&mut action, Some(tag), components)
}
#[allow(dead_code)]
pub fn new(mut action: Box<dyn Action>, components: &JsonValue) -> Api {
Self::from_action(&mut action, None, components)
}
fn json(&self) -> JsonValue {
let mut result = object! {
tags: self.tags.clone(),
summary: self.summary.clone(),
description: self.description.clone(),
operationId: self.operation_id.clone(),
responses: self.responses.clone(),
};
if self.request_body.required {
result["requestBody"] = self.request_body.json();
}
result
}
}
#[derive(Clone)]
struct RequestBody {
required: bool,
content: JsonValue,
components: JsonValue,
}
impl RequestBody {
pub fn new(components: JsonValue) -> Self {
Self {
required: false,
content: object! {},
components,
}
}
pub fn set_required(&mut self, state: bool) {
self.required = state;
}
pub fn set_content(&mut self, content_type: &str, params: &JsonValue) {
let schema_type = if params.is_array() { "array" } else { "object" };
self.content[content_type] = object! {
schema: object! { "type": schema_type }
};
match schema_type {
"object" => {
let mut schema = self.content[content_type]["schema"].clone();
Self::build_schema_object(&mut schema, params);
self.content[content_type]["schema"] = schema;
}
"array" => {
self.content[content_type]["schema"]["items"] = params.clone();
}
_ => {}
}
for (field, data) in params.entries() {
let example = if self.components["schemas"][field].is_empty() {
data["example"].clone()
} else {
self.components["schemas"][field]["example"].clone()
};
self.content[content_type]["example"][field] = example;
}
}
fn build_schema_object(data: &mut JsonValue, params: &JsonValue) {
Self::build_properties(data, params, true);
}
fn build_properties(data: &mut JsonValue, params: &JsonValue, is_root: bool) {
for (key, value) in params.entries() {
let mode = value["mode"].as_str().unwrap_or("");
let prop = &mut data["properties"][key];
prop["type"] = Self::mode(mode);
if let Some(desc) = value["description"].as_str() {
if !desc.is_empty() {
prop["description"] = desc.into();
}
}
if matches!(mode, "radio" | "select") {
prop["enum"] = value["option"].clone();
}
match prop["type"].as_str().unwrap_or("") {
"object" => {
Self::build_properties(prop, &value["items"], false);
}
"array" => {
if is_root {
prop["example"] = value["example"].clone();
prop["default"] = value["example"].clone();
}
Self::set_array_items(prop, &value["items"]);
}
_ => {
prop["example"] = value["example"].clone();
prop["default"] = value["example"].clone();
}
}
}
}
fn set_array_items(data: &mut JsonValue, params: &JsonValue) {
data["items"]["type"] = params["mode"].as_str().unwrap_or("string").into();
}
fn mode(name: &str) -> JsonValue {
match name {
"int" | "integer" | "number" => "integer",
"float" | "double" | "decimal" => "number",
"switch" | "bool" | "boolean" => "boolean",
"radio" | "text" | "textarea" | "password" | "email" | "date" | "datetime" | "time" => {
"string"
}
"select" | "array" | "list" => "array",
"object" | "json" | "map" => "object",
"file" | "image" | "binary" => "string",
"" => "string",
_ => name,
}
.into()
}
pub fn json(&self) -> JsonValue {
object! {
required: self.required,
content: self.content.clone()
}
}
}