tapioca-codegen 0.0.1

Type-safe REST client using the OpenAPI Specification
Documentation
use ::quote::Tokens;
use ::syn::Ident;
use ::yaml_rust::Yaml;

use infer::auth;
use infer::datatype;
use infer::path;
use infer::TokensResult;

type FieldsSupport = (Vec<Tokens>, Vec<Tokens>);

fn infer_ref_obj(schema: &Yaml, required: &Vec<Yaml>) -> FieldsSupport {
    let mut fields: Vec<Tokens> = Vec::new();
    let mut additional_types: Vec<Tokens> = Vec::new();

    for (field, schema) in schema["properties"].as_hash()
        .expect("Properties must be a map.")
    {
        let field_name = field.as_str()
            .expect("Property must be a string.");
        let field_ident = Ident::new(field_name);
        let (ty, maybe_at) = datatype::infer_v3(&schema).unwrap();
        let mandate: Tokens;

        if let Some(true) = schema["required"].as_bool() {
            mandate = quote!(::tapioca::datatype::Required);
        } else if required.contains(field) {
            mandate = quote!(::tapioca::datatype::Required);
        } else {
            mandate = quote!(::tapioca::datatype::Optional);
        }

        fields.push(quote!{ #field_ident: #mandate<#ty> });

        if let Some(additional_type) = maybe_at {
            additional_types.push(additional_type);
        }
    }

    (fields, additional_types)
}

fn infer_ref(ident: &Ident, schema: &Yaml, required: &Vec<Yaml>) -> TokensResult {
    match schema["properties"].as_hash() {
        Some(_) => {
            let (fields, additionals) = infer_ref_obj(&schema, &required);

            Ok(quote! {
                #(#additionals)*

                #[derive(Debug, Deserialize, PartialEq, Serialize)]
                pub struct #ident {
                    #(pub #fields),*
                }
            })
        },
        None => {
            let (alias_to, maybe_at) = datatype::infer_v3(&schema)?;
            let additional_type = match maybe_at {
                Some(at) => at,
                None => quote!(),
            };

            Ok(quote! {
                #additional_type

                #[allow(dead_code)]
                pub type #ident = #alias_to;
            })
        },
    }
}

pub(super) fn infer_v3(schema: &Yaml) -> TokensResult {
    let paths = schema["paths"].clone();
    let path_impls: Vec<Tokens> = paths.as_hash()
        .expect("Paths must be a map.")
        .iter()
        .map(|(path, path_schema)| path::infer_v3(
            path.as_str().expect("Path must be a string."), &path_schema
        ).unwrap())
        .collect();

    let api_url = schema["servers"][0]["url"].as_str()
        .expect("Must have at least one server URL.");

    let mut schema_ref_defs: Vec<Tokens> = Vec::new();
    let schema_refs = &schema["components"]["schemas"];

    if !schema_refs.is_badvalue() {
        for (schema_ref, schema) in schema_refs.as_hash()
            .expect("#/components/schemas must be a map.")
        {
            let schema_ref_name = schema_ref.as_str()
                .expect("$ref name must be a string.");
            schema_ref_defs.push(infer_ref(
                    &Ident::new(schema_ref_name),
                    &schema,
                    &schema["required"].as_vec().unwrap_or(&Vec::new())
            )?);
        }
    }

    let mut auth_scheme_defs: Vec<Tokens> = Vec::new();
    let auth_schemes = &schema["components"]["securitySchemes"];

    if !auth_schemes.is_badvalue() {
        for (auth_scheme, schema) in auth_schemes.as_hash()
            .expect("#/components/securitySchemes must be a map.")
        {
            let auth_scheme_name = auth_scheme.as_str()
                .expect("security scheme name must be a string");

            auth_scheme_defs.push(auth::infer_v3_component(auth_scheme_name, &schema)?);
        }
    }

    let server_auth_impl: Tokens;
    let auth_struct = Ident::new("ServerAuth");
    let security_reqs = &schema["security"];

    if !security_reqs.is_badvalue() {
        server_auth_impl = auth::infer_v3(&auth_struct, &security_reqs).0;
    } else {
        server_auth_impl = quote!{
            #[derive(Clone, Copy, Debug, Default)]
            pub struct #auth_struct(());

            impl #auth_struct {
                pub fn new() -> Self {
                    Self { 0: () }
                }
            }

            impl header::Header for #auth_struct {
                fn header_name() -> &'static str {
                    "X-No-Auth"
                }

                fn parse_header(_: &[Vec<u8>]) -> HeaderResult<Self> {
                    Ok(Self::new())
                }
            }

            impl header::HeaderFormat for #auth_struct {
                fn fmt_header(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result {
                    f.write_str("")
                }
            }
        }
    }

    Ok(quote! {
        pub mod schema_ref {
            #[allow(unused_imports)]
            use super::schema_ref;

            #(#schema_ref_defs)*
        }

        pub mod auth_scheme {
            #[allow(unused_imports)]
            use ::tapioca::header;
            #[allow(unused_imports)]
            use ::tapioca::HeaderResult;

            #(#auth_scheme_defs)*
        }

        use ::tapioca::header;
        use ::tapioca::HeaderResult;

        const API_URL: &'static str = #api_url;

        #[allow(dead_code)]
        #server_auth_impl

        #(#path_impls)*
    })
}