satay-codegen 0.1.0

Generate Rust client code from OpenAPI 3.1 documents
Documentation
use oas3::{
    Map as OasMap,
    spec::{
        MediaType as OasMediaType, ObjectOrReference, ObjectSchema as OasObjectSchema,
        Schema as OasSchema,
    },
};

use crate::error::ValidationError;

pub(super) fn optional_description(description: &Option<String>) -> Option<String> {
    description
        .as_deref()
        .filter(|description| !description.trim().is_empty())
        .map(str::to_owned)
}

pub(super) fn schema_description(schema: &OasSchema) -> Option<String> {
    match schema {
        OasSchema::Boolean(_) => None,
        OasSchema::Object(object) => match object.as_ref() {
            ObjectOrReference::Object(schema) => optional_description(&schema.description),
            ObjectOrReference::Ref { description, .. } => optional_description(description),
        },
    }
}

pub(super) fn satay_object<'a>(
    schema: &'a OasObjectSchema,
    context: &str,
) -> Result<Option<&'a serde_json::Map<String, serde_json::Value>>, ValidationError> {
    let Some(value) = schema.extensions.get("satay") else {
        return Ok(None);
    };
    value
        .as_object()
        .map(Some)
        .ok_or_else(|| ValidationError::ExpectedObjectField {
            context: context.to_owned(),
            field: "x-satay",
        })
}

pub(super) fn json_media_type(
    content: &OasMap<String, OasMediaType>,
) -> Option<(&str, &OasMediaType)> {
    content
        .get("application/json")
        .map(|value| ("application/json", value))
        .or_else(|| {
            content
                .iter()
                .find(|(media_type, _)| is_json_media_type(media_type))
                .map(|(media_type, value)| (media_type.as_str(), value))
        })
}

pub(super) fn is_json_media_type(value: &str) -> bool {
    let media_type = value.split(';').next().unwrap_or(value).trim();

    if media_type.eq_ignore_ascii_case("application/json") {
        return true;
    }

    let Some((_, subtype)) = media_type.rsplit_once('/') else {
        return false;
    };

    ends_with_ignore_ascii_case(subtype, "+json")
}

fn ends_with_ignore_ascii_case(value: &str, suffix: &str) -> bool {
    let value = value.as_bytes();
    let suffix = suffix.as_bytes();

    value.len() >= suffix.len() && value[value.len() - suffix.len()..].eq_ignore_ascii_case(suffix)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_blank_descriptions() {
        assert_eq!(optional_description(&None), None);
        assert_eq!(optional_description(&Some(String::new())), None);
        assert_eq!(optional_description(&Some(" \n\t ".to_owned())), None);
        assert_eq!(
            optional_description(&Some("  useful text  ".to_owned())),
            Some("  useful text  ".to_owned())
        );
    }

    #[test]
    fn matches_json_media_types_case_insensitively() {
        assert!(is_json_media_type("application/json"));
        assert!(is_json_media_type("Application/JSON; charset=utf-8"));
        assert!(is_json_media_type("application/vnd.satay.user+json"));
        assert!(is_json_media_type("application/problem+JSON"));
        assert!(!is_json_media_type("text/json"));
        assert!(!is_json_media_type("application/xml"));
        assert!(!is_json_media_type("not-a-media-type"));
    }

    #[test]
    fn selects_explicit_json_before_suffix_json_media_type() {
        let mut content = OasMap::new();
        content.insert(
            "application/vnd.satay.user+json".to_owned(),
            OasMediaType::default(),
        );
        content.insert("application/json".to_owned(), OasMediaType::default());

        let (media_type, _) = json_media_type(&content).expect("json media type");
        assert_eq!(media_type, "application/json");
    }

    #[test]
    fn selects_first_suffix_json_media_type_when_exact_json_is_absent() {
        let mut content = OasMap::new();
        content.insert("application/xml".to_owned(), OasMediaType::default());
        content.insert(
            "application/vnd.satay.user+json; charset=utf-8".to_owned(),
            OasMediaType::default(),
        );

        let (media_type, _) = json_media_type(&content).expect("json media type");
        assert_eq!(media_type, "application/vnd.satay.user+json; charset=utf-8");
    }
}