ngyn-swagger 0.1.0

Shuttle Runtime Platform for ngyn web framework
Documentation
use ngyn::{prelude::*, shared::traits::NgynModule};
use serde_json::{json, Value};

use crate::SwaggerController;

pub struct SwaggerModule;

pub struct SwaggerConfig<AppModule: NgynModule + Clone> {
    pub spec_url: String,
    pub app_module: Box<AppModule>,
    pub title: String,
    pub version: String,
    pub server_url: String,
    pub description: Option<String>,
    pub terms_of_service: Option<String>,
    pub contact: Option<String>,
    pub license: String,
    pub license_url: Option<String>,
}

impl<AppModule: NgynModule + Clone + Default> Default for SwaggerConfig<AppModule> {
    fn default() -> Self {
        SwaggerConfig {
            spec_url: "/docs/openapi.json".to_string(),
            app_module: Box::<AppModule>::default(),
            title: "API Documentation".to_string(),
            version: "1.0.0".to_string(),
            server_url: '/'.to_string(),
            description: None,
            terms_of_service: Some("".to_string()),
            contact: None,
            license: "MIT".to_string(),
            license_url: None,
        }
    }
}

#[controller("/docs")]
pub struct SwaggerRoutesController<AppModule: Default + NgynModule + Clone + 'static> {
    config: SwaggerConfig<AppModule>,
    spec: Value,
}

impl<AppModule: Default + NgynModule + Clone + 'static> SwaggerRoutesController<AppModule> {
    pub fn build(&mut self) {
        let app_module = &mut self.config.app_module;
        let (paths_spec, tags, components) = {
            let controllers = app_module.get_controllers();
            let mut paths = json!({});
            let mut components = json!({});
            let mut tags = Vec::new();
            for controller in controllers {
                let swagger_ctrl = controller
                    .as_any()
                    .downcast_ref::<Box<dyn SwaggerController>>();
                if let Some(swagger_ctrl) = swagger_ctrl {
                    let meta = swagger_ctrl.swagger_meta();
                    println!("{:?}", meta.components.len());
                    let components_spec = meta.components.iter().fold(json!({}), |mut acc, x| {
                        println!("{:?}", x);
                        merge(&mut acc, x.clone());
                        acc
                    });
                    merge(&mut components, components_spec);
                }
                let routes = controller.routes();
                let tag_name = controller.prefix().trim_matches('/').to_string();
                tags.push(tag_name.clone());
                let controller_spec = routes
                    .iter()
                    .map(|(path, method, operation_id)| {
                        json!({
                            path.to_lowercase(): {
                                method.to_lowercase(): {
                                    "summary": format!("{} {}", method.to_uppercase(), path),
                                    "description": "",
                                    "operationId": operation_id,
                                    "responses": {
                                        "200": {
                                            "description": ""
                                        }
                                    },
                                    "tags": [tag_name],
                                }
                            }
                        })
                    })
                    .fold(json!({}), |mut acc, x| {
                        merge(&mut acc, x);
                        acc
                    });
                merge(&mut paths, controller_spec);
            }
            (paths, tags, components)
        };
        self.spec = json!({
            "openapi": "3.0.0",
            "info": {
                "title": self.config.title,
                "version": self.config.version,
                "description": self.config.description,
                "termsOfService": self.config.terms_of_service,
                "contact": {
                    "name": self.config.contact,
                },
                "license": {
                    "name": self.config.license,
                    "url": self.config.license_url,
                },
            },
            "servers": [{
                "url": self.config.server_url,
            }],
            "paths": paths_spec,
            "components": {
                "schemas": components,
            },
            "tags": tags,
        });
    }
}

impl SwaggerModule {
    pub fn with_config<AppModule: Default + NgynModule + Clone + 'static>(
        config: SwaggerConfig<AppModule>,
    ) -> SwaggerRoutesController<AppModule> {
        let mut ctrl = SwaggerRoutesController {
            config,
            spec: Value::Null,
        };
        ctrl.build();
        ctrl
    }
}

#[routes]
impl<AppModule: Default + NgynModule + Clone + 'static> SwaggerRoutesController<AppModule> {
    #[get("/")]
    async fn index(&self, res: &mut NgynResponse) -> String {
        res.set_header("Content-Type", "text/html");

        let html = include_str!("templates/swagger.html");
        html.replace("% SWAGGER_SPEC_URL %", &self.config.spec_url)
            .replace("% SWAGGER_DOC_TITLE %", &self.config.title)
            .replace(
                "% SWAGGER_DOC_DESCRIPTION %",
                &self.config.description.clone().unwrap_or("".to_string()),
            )
    }

    #[get("/openapi.json")]
    async fn openapi_spec(&mut self) -> String {
        serde_json::to_string_pretty(&self.spec).unwrap_or("{}".to_string())
    }
}

fn merge(a: &mut Value, b: Value) {
    match (a, b) {
        (a @ &mut Value::Object(_), Value::Object(b)) => {
            let a = a.as_object_mut().unwrap();
            for (k, v) in b {
                merge(a.entry(k).or_insert(Value::Null), v);
            }
        }
        (a, b) => *a = b,
    }
}