openapi-nexus 0.2.1

OpenAPI 3.x multi-language code generator
Documentation
//! Shared structured runtime emission for Rust generators.

use sigil_stitch::code_block::CodeBlock;
use sigil_stitch::error::SigilStitchError;
use sigil_stitch::prelude::sigil_quote;
use sigil_stitch::spec::annotation_spec::AnnotationSpec;
use sigil_stitch::spec::enum_variant_spec::EnumVariantSpec;
use sigil_stitch::spec::field_spec::FieldSpec;
use sigil_stitch::spec::file_spec::FileSpec;
use sigil_stitch::spec::fun_spec::FunSpec;
use sigil_stitch::spec::modifiers::{TypeKind, Visibility};
use sigil_stitch::spec::parameter_spec::ParameterSpec;
use sigil_stitch::spec::type_spec::TypeSpec;
use sigil_stitch::spec::where_spec::TypeParamSpec;
use sigil_stitch::type_name::TypeName;

pub(crate) fn render_api_call_error(
    response_headers_type: TypeName,
) -> Result<String, SigilStitchError> {
    FileSpec::builder("error.rs")
        .add_type(api_call_http_error(response_headers_type.clone())?)
        .add_type(api_call_error_kind()?)
        .add_type(api_call_error(response_headers_type)?)
        .add_code(api_call_error_display()?)
        .add_code(api_call_error_source()?)
        .build()?
        .render(100)
}

fn api_call_http_error(response_headers_type: TypeName) -> Result<TypeSpec, SigilStitchError> {
    let api_error_of_t = TypeName::generic(
        TypeName::primitive("ApiError"),
        vec![TypeName::primitive("T")],
    );
    let from_api_error = FunSpec::builder("from_api_error")
        .add_type_param(TypeParamSpec::new("T"))
        .add_param(ParameterSpec::of("error", api_error_of_t))
        .returns(TypeName::primitive("Self"))
        .body(sigil_quote!(RustLang {
            Self {
                status_code: error.status_code,
                headers: error.headers,
                raw_body: error.raw_body,
                body_error: error.body.err(),
            }
        })?)
        .build()?;

    TypeSpec::builder("ApiCallHttpError", TypeKind::Struct)
        .annotate(AnnotationSpec::new("derive").args(["Debug"]))
        .add_field(FieldSpec::builder("status_code", TypeName::primitive("u16")).build()?)
        .add_field(FieldSpec::builder("headers", response_headers_type).build()?)
        .add_field(
            FieldSpec::builder(
                "raw_body",
                TypeName::generic(TypeName::primitive("Vec"), vec![TypeName::primitive("u8")]),
            )
            .build()?,
        )
        .add_field(
            FieldSpec::builder(
                "body_error",
                TypeName::optional(TypeName::primitive("Error")),
            )
            .build()?,
        )
        .add_method(from_api_error)
        .build()
}

fn api_call_error_kind() -> Result<TypeSpec, SigilStitchError> {
    let http = EnumVariantSpec::builder("Http")
        .associated_type(TypeName::primitive("ApiCallHttpError"))
        .build()?;
    let runtime = EnumVariantSpec::builder("Runtime")
        .associated_type(TypeName::primitive("Error"))
        .build()?;

    TypeSpec::builder("ApiCallErrorKind", TypeKind::Enum)
        .annotate(AnnotationSpec::new("derive").args(["Debug"]))
        .add_variant(http)
        .add_variant(runtime)
        .build()
}

fn api_call_error(response_headers_type: TypeName) -> Result<TypeSpec, SigilStitchError> {
    let static_str = operation_id_type();
    let api_error_of_t = TypeName::generic(
        TypeName::primitive("ApiError"),
        vec![TypeName::primitive("T")],
    );
    let optional_status = TypeName::optional(TypeName::primitive("u16"));
    let optional_headers = api_call_headers_type(response_headers_type.clone());
    let optional_body = api_call_raw_body_type();

    let from_api_error = FunSpec::builder("from_api_error")
        .visibility(Visibility::PublicCrate)
        .add_type_param(TypeParamSpec::new("T"))
        .add_param(ParameterSpec::of("operation_id", static_str.clone()))
        .add_param(ParameterSpec::of("error", api_error_of_t))
        .returns(TypeName::primitive("Self"))
        .body(sigil_quote!(RustLang {
            Self {
                operation_id,
                kind: ApiCallErrorKind::Http(ApiCallHttpError::from_api_error(error)),
            }
        })?)
        .build()?;
    let from_runtime_error = FunSpec::builder("from_runtime_error")
        .visibility(Visibility::PublicCrate)
        .add_param(ParameterSpec::of("operation_id", static_str.clone()))
        .add_param(ParameterSpec::of("error", TypeName::primitive("Error")))
        .returns(TypeName::primitive("Self"))
        .body(sigil_quote!(RustLang {
            Self {
                operation_id,
                kind: ApiCallErrorKind::Runtime(error),
            }
        })?)
        .build()?;
    let operation_id = FunSpec::builder("operation_id")
        .visibility(Visibility::Public)
        .doc(
            "Operation identifier, generated from the HTTP method and path when OpenAPI omits one.",
        )
        .add_param(ParameterSpec::of("&self", TypeName::primitive("")))
        .returns(static_str)
        .body(sigil_quote!(RustLang { self.operation_id })?)
        .build()?;
    let status_code = FunSpec::builder("status_code")
        .visibility(Visibility::Public)
        .doc("HTTP response status, if the operation reached the server and received an error response.")
        .add_param(ParameterSpec::of("&self", TypeName::primitive("")))
        .returns(optional_status)
        .body(
            sigil_quote!(RustLang {
                match &self.kind {
                    ApiCallErrorKind::Http(error) => Some(error.status_code),
                    ApiCallErrorKind::Runtime(_) => None,
                }
            })?,
        )
        .build()?;
    let headers = FunSpec::builder("headers")
        .visibility(Visibility::Public)
        .doc("Native HTTP response headers, if an error response was received.")
        .add_param(ParameterSpec::of("&self", TypeName::primitive("")))
        .returns(optional_headers)
        .body(sigil_quote!(RustLang {
            match &self.kind {
                ApiCallErrorKind::Http(error) => Some(&error.headers),
                ApiCallErrorKind::Runtime(_) => None,
            }
        })?)
        .build()?;
    let raw_body = FunSpec::builder("raw_body")
        .visibility(Visibility::Public)
        .doc("Raw HTTP response body, if an error response was received.")
        .add_param(ParameterSpec::of("&self", TypeName::primitive("")))
        .returns(optional_body)
        .body(sigil_quote!(RustLang {
            match &self.kind {
                ApiCallErrorKind::Http(error) => Some(&error.raw_body),
                ApiCallErrorKind::Runtime(_) => None,
            }
        })?)
        .build()?;

    TypeSpec::builder("ApiCallError", TypeKind::Struct)
        .visibility(Visibility::Public)
        .doc("Type-erased error from any generated API operation.")
        .annotate(AnnotationSpec::new("derive").args(["Debug"]))
        .add_field(FieldSpec::builder("operation_id", operation_id_type()).build()?)
        .add_field(FieldSpec::builder("kind", TypeName::primitive("ApiCallErrorKind")).build()?)
        .add_method(from_api_error)
        .add_method(from_runtime_error)
        .add_method(operation_id)
        .add_method(status_code)
        .add_method(headers)
        .add_method(raw_body)
        .build()
}

fn operation_id_type() -> TypeName {
    TypeName::reference_with_lifetime(TypeName::primitive("str"), "'static")
}

fn api_call_headers_type(response_headers_type: TypeName) -> TypeName {
    TypeName::optional(TypeName::reference(response_headers_type))
}

fn api_call_raw_body_type() -> TypeName {
    TypeName::optional(TypeName::slice(TypeName::primitive("u8")))
}

fn api_call_error_display() -> Result<CodeBlock, SigilStitchError> {
    sigil_quote!(RustLang {
        impl std::fmt::Display for ApiCallError {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                match &self.kind {
                    ApiCallErrorKind::Http(error) => {
                        write!(f, "operation {} failed with HTTP status {}", self.operation_id, error.status_code)
                    }
                    ApiCallErrorKind::Runtime(error) => {
                        write!(f, "operation {} failed: {error}", self.operation_id)
                    }
                }
            }
        }
    })
}

fn api_call_error_source() -> Result<CodeBlock, SigilStitchError> {
    sigil_quote!(RustLang {
        impl std::error::Error for ApiCallError {
            fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
                if let ApiCallErrorKind::Http(error) = &self.kind {
                    if let Some(error) = &error.body_error {
                        return Some(error);
                    }
                }
                if let ApiCallErrorKind::Runtime(error) = &self.kind {
                    return Some(error);
                }
                None
            }
        }
    })
}

#[cfg(test)]
mod tests {
    use sigil_stitch::assert_rendered;
    use sigil_stitch::lang::rust::Rust;

    use super::*;

    #[test]
    fn api_call_error_borrowed_types_render_exactly() {
        let operation_id = operation_id_type();
        let accessors = TypeName::tuple(vec![
            api_call_headers_type(TypeName::qualified("reqwest::header", "HeaderMap")),
            api_call_raw_body_type(),
        ]);
        let block = sigil_quote!(RustLang {
            fn inspect(operation_id: $T(operation_id)) -> $T(accessors) {
                todo!()
            }
        })
        .expect("ApiCallError borrowed type probe builds");

        assert_rendered!(
            Rust::new(),
            width = 100,
            block,
            r#"fn inspect(operation_id: &'static str) -> (Option<&reqwest::header::HeaderMap>, Option<&[u8]>) {
    todo!()
}
"#,
        );
    }
}