bloom-web-core 0.1.1

Core functionality for the Bloom web framework
use utoipa::openapi::{OpenApi, OpenApiBuilder, InfoBuilder, ComponentsBuilder};
use utoipa::openapi::{PathsBuilder, PathItemType};
use utoipa::openapi::path::{PathItemBuilder, OperationBuilder, ParameterBuilder, ParameterIn};
use utoipa::openapi::{Required, Schema, Object, SchemaType, RefOr, Ref};
use utoipa::openapi::request_body::RequestBodyBuilder;
use utoipa::openapi::response::{ResponseBuilder, ResponsesBuilder};
use utoipa::openapi::content::ContentBuilder;
use std::collections::{HashMap, BTreeMap};

/// Registry for OpenAPI path operations with request schema detection.
///
/// This struct represents an OpenAPI path operation that can be registered
/// for automatic documentation generation, including detected JSON payload schemas.
pub struct PathOperation {
    pub path: &'static str,
    pub method: &'static str,
    pub operation_id: &'static str,
    pub summary: &'static str,
    pub request_schema: Option<&'static str>,
}

/// Registry for OpenAPI schemas.
///
/// This struct represents a schema that can be registered for OpenAPI documentation.
pub struct SchemaInfo {
    pub name: &'static str,
}

inventory::collect!(PathOperation);
inventory::collect!(SchemaInfo);

/// Generates OpenAPI document from collected inventory.
///
/// This function creates a complete OpenAPI specification by collecting all registered
/// path operations and schemas, building proper request/response definitions, and
/// generating appropriate parameter and response schemas.
///
/// # Returns
/// * `OpenApi` - A complete OpenAPI specification document
pub fn generate_openapi() -> OpenApi {
    let mut openapi = OpenApiBuilder::new()
        .info(InfoBuilder::new()
            .title("Bloom")
            .version("1.0.0")
            .build())
        .build();

    let mut paths_map = HashMap::new();

    for path_op in inventory::iter::<PathOperation> {
        let path_params = extract_path_parameters(path_op.path);
        let mut parameters = vec![];

        for param_name in path_params {
            parameters.push(
                ParameterBuilder::new()
                    .name(param_name)
                    .parameter_in(ParameterIn::Path)
                    .required(Required::True)
                    .schema(Some(RefOr::T(
                        Schema::Object(
                            Object::with_type(SchemaType::String)
                        )
                    )))
                    .build()
            );
        }

        let mut operation = OperationBuilder::new()
            .operation_id(Some(path_op.operation_id.to_string()))
            .summary(Some(path_op.summary.to_string()));

        if !parameters.is_empty() {
            operation = operation.parameters(Some(parameters));
        }

        if matches!(path_op.method, "post" | "put" | "patch") {
            let schema = if let Some(schema_name) = path_op.request_schema {
                RefOr::Ref(Ref::from_schema_name(schema_name))
            } else {
                RefOr::T(Schema::Object(Object::with_type(SchemaType::Object)))
            };

            let request_body = RequestBodyBuilder::new()
                .content("application/json", ContentBuilder::new()
                    .schema(schema)
                    .build())
                .build();
            operation = operation.request_body(Some(request_body));
        }

        let (status_code, description) = match path_op.method {
            "get" => ("200", "Successfully retrieved data"),
            "post" => ("201", "Successfully created resource"),
            "put" => ("200", "Successfully updated resource"),
            "delete" => ("204", "Successfully deleted resource"),
            "patch" => ("200", "Successfully patched resource"),
            _ => ("200", "Success"),
        };

        let mut response_builder = ResponseBuilder::new()
            .description(description);

        if path_op.method != "delete" {
            let content = ContentBuilder::new()
                .schema(RefOr::T(Schema::Object(Object::with_type(SchemaType::Object))))
                .build();
            response_builder = response_builder.content("application/json", content);
        }

        let responses = ResponsesBuilder::new()
            .response(status_code, response_builder.build())
            .build();

        operation = operation.responses(responses);

        let path_item = paths_map.entry(path_op.path.to_string())
            .or_insert_with(|| PathItemBuilder::new().build());

        match path_op.method {
            "get" => path_item.operations.insert(PathItemType::Get, operation.build()),
            "post" => path_item.operations.insert(PathItemType::Post, operation.build()),
            "put" => path_item.operations.insert(PathItemType::Put, operation.build()),
            "delete" => path_item.operations.insert(PathItemType::Delete, operation.build()),
            "patch" => path_item.operations.insert(PathItemType::Patch, operation.build()),
            _ => None,
        };
    }

    let mut paths_builder = PathsBuilder::new();
    for (path_str, path_item) in paths_map {
        paths_builder = paths_builder.path(path_str, path_item);
    }
    openapi.paths = paths_builder.build();

    let mut schemas = HashMap::new();
    for schema_info in inventory::iter::<SchemaInfo> {
        let schema = generate_schema_for_type(schema_info.name);
        schemas.insert(schema_info.name.to_string(), RefOr::T(schema));
    }

    if !schemas.is_empty() {
        openapi.components = Some(
            ComponentsBuilder::new()
                .schemas_from_iter(schemas)
                .build()
        );
    }

    openapi
}

/// Generates detailed schema for known DTO types.
///
/// This function creates appropriate OpenAPI schemas for registered DTO types
/// with proper field definitions, types, and required field specifications.
///
/// # Arguments
/// * `type_name` - The name of the type to generate schema for
///
/// # Returns
/// * `Schema` - The generated OpenAPI schema
fn generate_schema_for_type(type_name: &str) -> Schema {
    match type_name {
        "CreateUser" => {
            let mut properties = BTreeMap::new();
            properties.insert("username".to_string(), RefOr::T(
                Schema::Object(Object::with_type(SchemaType::String))
            ));
            properties.insert("email".to_string(), RefOr::T(
                Schema::Object(Object::with_type(SchemaType::String))
            ));

            let mut object = Object::with_type(SchemaType::Object);
            object.properties = properties;
            object.required = vec!["username".to_string(), "email".to_string()];
            Schema::Object(object)
        },
        "UpdateUser" => {
            let mut properties = BTreeMap::new();
            properties.insert("username".to_string(), RefOr::T(
                Schema::Object(Object::with_type(SchemaType::String))
            ));
            properties.insert("email".to_string(), RefOr::T(
                Schema::Object(Object::with_type(SchemaType::String))
            ));

            let mut object = Object::with_type(SchemaType::Object);
            object.properties = properties;
            object.required = vec!["username".to_string(), "email".to_string()];
            Schema::Object(object)
        },
        "UserDto" => {
            let mut properties = BTreeMap::new();
            properties.insert("id".to_string(), RefOr::T(
                Schema::Object(Object::with_type(SchemaType::Integer))
            ));
            properties.insert("username".to_string(), RefOr::T(
                Schema::Object(Object::with_type(SchemaType::String))
            ));
            properties.insert("email".to_string(), RefOr::T(
                Schema::Object(Object::with_type(SchemaType::String))
            ));

            let mut object = Object::with_type(SchemaType::Object);
            object.properties = properties;
            object.required = vec!["id".to_string(), "username".to_string(), "email".to_string()];
            Schema::Object(object)
        },
        _ => {
            // Fallback to generic object schema for unknown types
            Schema::Object(Object::with_type(SchemaType::Object))
        }
    }
}

/// Extracts path parameters from a path string.
///
/// This function parses a path string to find parameter placeholders (e.g., {id})
/// and returns a list of parameter names for OpenAPI documentation.
///
/// # Arguments
/// * `path` - The path string to parse
///
/// # Returns
/// * `Vec<String>` - A vector of parameter names found in the path
fn extract_path_parameters(path: &str) -> Vec<String> {
    let mut params = Vec::new();
    let mut chars = path.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch == '{' {
            let mut param_name = String::new();
            while let Some(ch) = chars.peek() {
                if *ch == '}' {
                    chars.next();
                    break;
                }
                param_name.push(chars.next().unwrap());
            }
            if !param_name.is_empty() {
                params.push(param_name);
            }
        }
    }

    params
}