satay-codegen 0.12.2

Generate Rust client code from OpenAPI 3.1 documents
Documentation
use oas3::Map as OasMap;
use oas3::spec::{
    ComponentTarget, MediaType as OasMediaType, ObjectOrReference, Operation as OasOperation,
    Parameter as OasParameter, PathItem as OasPathItem, RequestBody as OasRequestBody,
    ResolvableComponent, ResolveError, Resolver, Response as OasResponse, Schema as OasSchema,
    SecurityScheme as OasSecurityScheme, Spec as OasSpec,
};

use super::Document;
use crate::error::ValidationError;

#[derive(Debug, Clone, Copy)]
pub(crate) struct ResolvedDocument<'a> {
    pub(crate) spec: &'a OasSpec,
    resolver: Resolver<'a>,
}

impl ResolvedDocument<'_> {
    pub(crate) fn resolve<'a, T>(
        &'a self,
        component: &'a ObjectOrReference<T>,
        context: &str,
    ) -> Result<&'a T, ValidationError>
    where
        T: ResolvableComponent,
    {
        self.resolver.resolve(component).map_err(|source| {
            map_resolve_error::<T>(source, component_reference(component), context)
        })
    }

    pub(crate) fn resolve_schema<'a>(
        &'a self,
        schema: &'a OasSchema,
        context: &str,
    ) -> Result<&'a OasSchema, ValidationError> {
        self.resolver
            .resolve_schema(schema)
            .map_err(|source| map_resolve_error::<OasSchema>(source, schema.reference(), context))
    }

    pub(crate) fn resolve_path_item<'a>(
        &'a self,
        path_item: &'a OasPathItem,
        context: &str,
    ) -> Result<&'a OasPathItem, ValidationError> {
        self.resolver
            .resolve_path_item(path_item)
            .map_err(|source| {
                map_resolve_error::<OasPathItem>(source, path_item.reference.as_deref(), context)
            })
    }

    fn resolve_path_item_component<'a>(
        &'a self,
        path_item: &'a ObjectOrReference<OasPathItem>,
        context: &str,
    ) -> Result<&'a OasPathItem, ValidationError> {
        let path_item = self.resolve(path_item, context)?;
        self.resolve_path_item(path_item, context)
    }
}

pub(crate) fn resolve_document(
    document: &Document,
) -> Result<ResolvedDocument<'_>, ValidationError> {
    let resolved = ResolvedDocument {
        spec: &document.spec,
        resolver: Resolver::new(&document.spec),
    };

    validate_component_refs(&resolved)?;
    validate_path_refs(&resolved)?;

    Ok(resolved)
}

fn component_reference<T>(component: &ObjectOrReference<T>) -> Option<&str> {
    match component {
        ObjectOrReference::Object(_) => None,
        ObjectOrReference::Ref { ref_path, .. } => Some(ref_path),
    }
}

fn map_resolve_error<T>(
    source: ResolveError,
    root_reference: Option<&str>,
    context: &str,
) -> ValidationError
where
    T: ComponentTarget,
{
    let reference = match &source {
        ResolveError::InvalidReference(source) => source.reference().to_owned(),
        ResolveError::MissingComponent { reference, .. } => reference.clone(),
        ResolveError::Cycle { references } => references.first().cloned().unwrap_or_default(),
        _ => root_reference.unwrap_or_default().to_owned(),
    };

    let source = match source {
        ResolveError::InvalidReference(source) => ValidationError::InvalidComponentReference {
            reference: source.reference().to_owned(),
            section: T::COMPONENT_SECTION,
        },
        ResolveError::MissingComponent { name, .. } => {
            ValidationError::MissingJsonPointerToken { token: name }
        }
        ResolveError::Cycle { references } => ValidationError::CircularReference {
            reference: references
                .first()
                .cloned()
                .unwrap_or_else(|| reference.clone()),
        },
        _ => ValidationError::InvalidComponentReference {
            reference: reference.clone(),
            section: T::COMPONENT_SECTION,
        },
    };

    ValidationError::ResolveReference {
        reference,
        context: context.to_owned(),
        source: Box::new(source),
    }
}

fn validate_component_refs(document: &ResolvedDocument<'_>) -> Result<(), ValidationError> {
    let Some(components) = document.spec.components.as_ref() else {
        return Ok(());
    };

    for (schema_name, schema) in &components.schemas {
        validate_schema_refs(document, schema, &format!("schema `{schema_name}`"))?;
    }

    for (scheme_name, scheme) in &components.security_schemes {
        document
            .resolve::<OasSecurityScheme>(scheme, &format!("security scheme `{scheme_name}`"))?;
    }

    for (parameter_name, parameter) in &components.parameters {
        validate_parameter_ref(
            document,
            parameter,
            &format!("parameter `{parameter_name}`"),
        )?;
    }

    for (request_body_name, request_body) in &components.request_bodies {
        validate_request_body_ref(
            document,
            request_body,
            &format!("request body `{request_body_name}`"),
        )?;
    }

    for (response_name, response) in &components.responses {
        validate_response_ref(document, response, &format!("response `{response_name}`"))?;
    }

    for (path_item_name, path_item) in &components.path_items {
        validate_path_item_component_ref(
            document,
            path_item,
            &format!("path item `{path_item_name}`"),
        )?;
    }

    Ok(())
}

fn validate_path_refs(document: &ResolvedDocument<'_>) -> Result<(), ValidationError> {
    let Some(paths) = document.spec.paths.as_ref() else {
        return Ok(());
    };

    for (path, path_item) in paths {
        validate_path_item(document, path_item, &format!("path item `{path}`"))?;
    }

    Ok(())
}

fn validate_schema_refs(
    document: &ResolvedDocument<'_>,
    schema: &OasSchema,
    context: &str,
) -> Result<(), ValidationError> {
    document.resolve_schema(schema, context)?;

    for (index, subschema) in schema.subschemas().enumerate() {
        validate_schema_refs(
            document,
            subschema,
            &format!("{context}.subschemas[{index}]"),
        )?;
    }

    Ok(())
}

fn validate_parameter_ref(
    document: &ResolvedDocument<'_>,
    parameter: &ObjectOrReference<OasParameter>,
    context: &str,
) -> Result<(), ValidationError> {
    let parameter = document.resolve(parameter, context)?;
    if let Some(schema) = parameter.schema.as_ref() {
        validate_schema_refs(document, schema, &format!("{context}.schema"))?;
    }
    Ok(())
}

fn validate_request_body_ref(
    document: &ResolvedDocument<'_>,
    request_body: &ObjectOrReference<OasRequestBody>,
    context: &str,
) -> Result<(), ValidationError> {
    let request_body = document.resolve(request_body, context)?;
    validate_content_schema_refs(
        document,
        &request_body.content,
        &format!("{context}.content"),
    )
}

fn validate_response_ref(
    document: &ResolvedDocument<'_>,
    response: &ObjectOrReference<OasResponse>,
    context: &str,
) -> Result<(), ValidationError> {
    let response = document.resolve(response, context)?;
    validate_content_schema_refs(document, &response.content, &format!("{context}.content"))
}

fn validate_path_item_component_ref(
    document: &ResolvedDocument<'_>,
    path_item: &ObjectOrReference<OasPathItem>,
    context: &str,
) -> Result<(), ValidationError> {
    let path_item = document.resolve_path_item_component(path_item, context)?;
    validate_resolved_path_item(document, path_item, context)
}

fn validate_path_item(
    document: &ResolvedDocument<'_>,
    path_item: &OasPathItem,
    context: &str,
) -> Result<(), ValidationError> {
    let path_item = document.resolve_path_item(path_item, context)?;
    validate_resolved_path_item(document, path_item, context)
}

fn validate_resolved_path_item(
    document: &ResolvedDocument<'_>,
    path_item: &OasPathItem,
    context: &str,
) -> Result<(), ValidationError> {
    for parameter in &path_item.parameters {
        validate_parameter_ref(document, parameter, &format!("{context}.parameters"))?;
    }

    for (method, operation) in [
        ("get", path_item.get.as_ref()),
        ("post", path_item.post.as_ref()),
        ("put", path_item.put.as_ref()),
        ("patch", path_item.patch.as_ref()),
        ("delete", path_item.delete.as_ref()),
        ("head", path_item.head.as_ref()),
        ("options", path_item.options.as_ref()),
        ("trace", path_item.trace.as_ref()),
    ] {
        validate_operation_refs(document, operation, &format!("{context}.{method}"))?;
    }

    Ok(())
}

fn validate_operation_refs(
    document: &ResolvedDocument<'_>,
    operation: Option<&OasOperation>,
    context: &str,
) -> Result<(), ValidationError> {
    let Some(operation) = operation else {
        return Ok(());
    };

    let operation_context = operation
        .operation_id
        .as_ref()
        .map(|operation_id| format!("operation `{operation_id}`"))
        .unwrap_or_else(|| context.to_owned());

    for parameter in &operation.parameters {
        validate_parameter_ref(
            document,
            parameter,
            &format!("{operation_context} parameters"),
        )?;
    }

    if let Some(request_body) = operation.request_body.as_ref() {
        validate_request_body_ref(
            document,
            request_body,
            &format!("{operation_context} requestBody"),
        )?;
    }

    if let Some(responses) = operation.responses.as_ref() {
        for (status, response) in responses {
            validate_response_ref(
                document,
                response,
                &format!("{operation_context} responses {status}"),
            )?;
        }
    }

    Ok(())
}

fn validate_content_schema_refs(
    document: &ResolvedDocument<'_>,
    content: &OasMap<String, OasMediaType>,
    location: &str,
) -> Result<(), ValidationError> {
    for (media_type, media) in content {
        if let Some(schema) = media.schema.as_ref() {
            validate_schema_refs(document, schema, &format!("{location}.{media_type}.schema"))?;
        }
    }

    Ok(())
}