satay-codegen 0.1.0

Generate Rust client code from OpenAPI 3.1 documents
Documentation
use oas3::spec::{
    ObjectOrReference, ObjectSchema as OasObjectSchema, Parameter as OasParameter,
    PathItem as OasPathItem, RequestBody as OasRequestBody, Response as OasResponse,
    Schema as OasSchema, SchemaType as OasSchemaType, SchemaTypeSet as OasSchemaTypeSet,
    SecurityScheme as OasSecurityScheme,
};

use super::resolve::{ResolvedDocument, refs::local_ref_name};
use crate::error::ValidationError;
use crate::ident::type_ident;

pub(super) fn schema_ref<'a>(
    schema: &'a OasSchema,
    _context: &str,
) -> Result<Option<&'a str>, ValidationError> {
    match schema {
        OasSchema::Boolean(_) => Ok(None),
        OasSchema::Object(object) => match object.as_ref() {
            ObjectOrReference::Ref { ref_path, .. } => Ok(Some(ref_path.as_str())),
            ObjectOrReference::Object(_) => Ok(None),
        },
    }
}

pub(super) fn schema_ref_type_name(reference: &str) -> Result<String, ValidationError> {
    let name = local_ref_name(reference, "schemas")?;
    Ok(type_ident(&name))
}

pub(super) fn object_schema<'a>(
    schema: &'a OasSchema,
    context: &str,
) -> Result<&'a OasObjectSchema, ValidationError> {
    match schema {
        OasSchema::Boolean(_) => Err(ValidationError::UnsupportedBooleanSchema {
            context: context.to_owned(),
        }),
        OasSchema::Object(object) => match object.as_ref() {
            ObjectOrReference::Object(schema) => Ok(schema),
            ObjectOrReference::Ref { .. } => Err(ValidationError::ExpectedObject {
                context: context.to_owned(),
            }),
        },
    }
}

pub(super) fn schema_type_and_nullable(
    schema: &OasObjectSchema,
    context: &str,
) -> Result<(Option<OasSchemaType>, bool), ValidationError> {
    let Some(schema_type) = schema.schema_type.as_ref() else {
        return Ok((None, false));
    };

    match schema_type {
        OasSchemaTypeSet::Single(OasSchemaType::Null) => {
            Err(ValidationError::UnsupportedSchemaType {
                context: context.to_owned(),
                kind: "null".to_owned(),
            })
        }
        OasSchemaTypeSet::Single(schema_type) => Ok((Some(*schema_type), false)),
        OasSchemaTypeSet::Multiple(types) => {
            let mut nullable = false;
            let mut non_null = None;
            let mut non_null_count = 0usize;
            for schema_type in types {
                if *schema_type == OasSchemaType::Null {
                    nullable = true;
                } else {
                    non_null_count += 1;
                    if non_null.is_none() {
                        non_null = Some(*schema_type);
                    }
                }
            }

            match non_null_count {
                0 => Err(ValidationError::UnsupportedSchemaType {
                    context: context.to_owned(),
                    kind: "null".to_owned(),
                }),
                1 => Ok((non_null, nullable)),
                _ => Err(ValidationError::MultipleNonNullSchemaTypesUnsupported {
                    context: context.to_owned(),
                }),
            }
        }
    }
}

pub(super) fn schema_type_wire(schema_type: OasSchemaType) -> &'static str {
    match schema_type {
        OasSchemaType::Boolean => "boolean",
        OasSchemaType::Integer => "integer",
        OasSchemaType::Number => "number",
        OasSchemaType::String => "string",
        OasSchemaType::Array => "array",
        OasSchemaType::Object => "object",
        OasSchemaType::Null => "null",
    }
}

pub(super) fn reject_composition(
    schema: &OasObjectSchema,
    context: &str,
) -> Result<(), ValidationError> {
    for (keyword, present) in [
        ("oneOf", !schema.one_of.is_empty()),
        ("anyOf", !schema.any_of.is_empty()),
        ("allOf", !schema.all_of.is_empty()),
    ] {
        if present {
            return Err(ValidationError::UnsupportedComposition {
                context: context.to_owned(),
                keyword,
            });
        }
    }
    Ok(())
}

pub(super) fn resolve_security_scheme<'a>(
    document: &ResolvedDocument<'a>,
    scheme: &'a ObjectOrReference<OasSecurityScheme>,
    context: &str,
) -> Result<&'a OasSecurityScheme, ValidationError> {
    match scheme {
        ObjectOrReference::Object(scheme) => Ok(scheme),
        ObjectOrReference::Ref { ref_path, .. } => {
            let reference = ref_path.clone();
            resolve_security_scheme_ref(document, ref_path, context).map_err(|source| {
                ValidationError::ResolveReference {
                    reference,
                    context: context.to_owned(),
                    source: Box::new(source),
                }
            })
        }
    }
}

fn resolve_security_scheme_ref<'a>(
    document: &ResolvedDocument<'a>,
    reference: &str,
    context: &str,
) -> Result<&'a OasSecurityScheme, ValidationError> {
    let name = local_ref_name(reference, "securitySchemes")?;

    let target = document
        .spec
        .components
        .as_ref()
        .and_then(|components| components.security_schemes.get(&name))
        .ok_or_else(|| ValidationError::MissingJsonPointerToken {
            token: name.clone(),
        })?;

    resolve_security_scheme(document, target, context)
}

pub(super) fn resolve_parameter<'a>(
    document: &ResolvedDocument<'a>,
    parameter: &'a ObjectOrReference<OasParameter>,
    context: &str,
) -> Result<&'a OasParameter, ValidationError> {
    match parameter {
        ObjectOrReference::Object(parameter) => Ok(parameter),
        ObjectOrReference::Ref { ref_path, .. } => {
            let reference = ref_path.clone();
            resolve_parameter_ref(document, ref_path, context).map_err(|source| {
                ValidationError::ResolveReference {
                    reference,
                    context: context.to_owned(),
                    source: Box::new(source),
                }
            })
        }
    }
}

fn resolve_parameter_ref<'a>(
    document: &ResolvedDocument<'a>,
    reference: &str,
    context: &str,
) -> Result<&'a OasParameter, ValidationError> {
    let name = local_ref_name(reference, "parameters")?;

    let target = document
        .spec
        .components
        .as_ref()
        .and_then(|components| components.parameters.get(&name))
        .ok_or_else(|| ValidationError::MissingJsonPointerToken {
            token: name.clone(),
        })?;

    resolve_parameter(document, target, context)
}

pub(super) fn resolve_request_body<'a>(
    document: &ResolvedDocument<'a>,
    request_body: &'a ObjectOrReference<OasRequestBody>,
    context: &str,
) -> Result<&'a OasRequestBody, ValidationError> {
    match request_body {
        ObjectOrReference::Object(request_body) => Ok(request_body),
        ObjectOrReference::Ref { ref_path, .. } => {
            let reference = ref_path.clone();
            resolve_request_body_ref(document, ref_path, context).map_err(|source| {
                ValidationError::ResolveReference {
                    reference,
                    context: context.to_owned(),
                    source: Box::new(source),
                }
            })
        }
    }
}

fn resolve_request_body_ref<'a>(
    document: &ResolvedDocument<'a>,
    reference: &str,
    context: &str,
) -> Result<&'a OasRequestBody, ValidationError> {
    let name = local_ref_name(reference, "requestBodies")?;

    let target = document
        .spec
        .components
        .as_ref()
        .and_then(|components| components.request_bodies.get(&name))
        .ok_or_else(|| ValidationError::MissingJsonPointerToken {
            token: name.clone(),
        })?;

    resolve_request_body(document, target, context)
}

pub(super) fn resolve_response<'a>(
    document: &ResolvedDocument<'a>,
    response: &'a ObjectOrReference<OasResponse>,
    context: &str,
) -> Result<&'a OasResponse, ValidationError> {
    match response {
        ObjectOrReference::Object(response) => Ok(response),
        ObjectOrReference::Ref { ref_path, .. } => {
            let reference = ref_path.clone();
            resolve_response_ref(document, ref_path, context).map_err(|source| {
                ValidationError::ResolveReference {
                    reference,
                    context: context.to_owned(),
                    source: Box::new(source),
                }
            })
        }
    }
}

fn resolve_response_ref<'a>(
    document: &ResolvedDocument<'a>,
    reference: &str,
    context: &str,
) -> Result<&'a OasResponse, ValidationError> {
    let name = local_ref_name(reference, "responses")?;

    let target = document
        .spec
        .components
        .as_ref()
        .and_then(|components| components.responses.get(&name))
        .ok_or_else(|| ValidationError::MissingJsonPointerToken {
            token: name.clone(),
        })?;

    resolve_response(document, target, context)
}

pub(super) fn resolve_path_item<'a>(
    document: &ResolvedDocument<'a>,
    path_item: &'a OasPathItem,
    context: &str,
) -> Result<&'a OasPathItem, ValidationError> {
    let Some(reference) = path_item.reference.as_deref() else {
        return Ok(path_item);
    };

    let name = local_ref_name(reference, "pathItems").map_err(|source| {
        ValidationError::ResolveReference {
            reference: reference.to_owned(),
            context: context.to_owned(),
            source: Box::new(source),
        }
    })?;

    let target = document
        .spec
        .components
        .as_ref()
        .and_then(|components| components.path_items.get(&name))
        .ok_or_else(|| ValidationError::ResolveReference {
            reference: reference.to_owned(),
            context: context.to_owned(),
            source: Box::new(ValidationError::MissingJsonPointerToken {
                token: name.clone(),
            }),
        })?;

    resolve_path_item_reference(document, target, context, reference)
}

fn resolve_path_item_reference<'a>(
    document: &ResolvedDocument<'a>,
    path_item: &'a ObjectOrReference<OasPathItem>,
    context: &str,
    reference: &str,
) -> Result<&'a OasPathItem, ValidationError> {
    match path_item {
        ObjectOrReference::Object(path_item) => resolve_path_item(document, path_item, context),
        ObjectOrReference::Ref { ref_path, .. } => {
            let name = local_ref_name(ref_path, "pathItems").map_err(|source| {
                ValidationError::ResolveReference {
                    reference: reference.to_owned(),
                    context: context.to_owned(),
                    source: Box::new(source),
                }
            })?;
            let target = document
                .spec
                .components
                .as_ref()
                .and_then(|components| components.path_items.get(&name))
                .ok_or_else(|| ValidationError::ResolveReference {
                    reference: reference.to_owned(),
                    context: context.to_owned(),
                    source: Box::new(ValidationError::MissingJsonPointerToken { token: name }),
                })?;
            resolve_path_item_reference(document, target, context, reference)
        }
    }
}