open-api-generator 0.0.1

An OpenAPI/Swagger schema generator
Documentation
use crate::v3_0::Spec;
use codegen::{Field, Scope};
use openapi::v3_0::{ObjectOrReference, Schema};
use std::io::{self, Write};

#[derive(Debug)]
pub struct GenerationOpts {
    pub indent_tabs: bool,
    pub indent_count: usize,
}

impl Default for GenerationOpts {
    fn default() -> Self {
        Self {
            indent_count: 4,
            indent_tabs: false,
        }
    }
}

/// Generate components with default `GenerationOpts`.
pub fn generate_components<W: Write>(w: &mut W, spec: &Spec) -> io::Result<()> {
    generate_components_with_opts(w, spec, &GenerationOpts::default())
}

/// Generate components with the given `GenerationOpts`.
pub fn generate_components_with_opts<W: Write>(
    w: &mut W,
    spec: &Spec,
    _opts: &GenerationOpts,
) -> io::Result<()> {
    let components = match spec.components {
        Some(ref c) => c,
        None => return Ok(()),
    };

    let mut scope = Scope::new();

    if let Some(ref schemas) = components.schemas {
        // TODO: any assertions?

        let components_schemas = scope
            .new_module("components")
            .vis("pub")
            .new_module("schemas")
            .vis("pub");

        // I want to have this, but it is not valid rust yet
        // `use super::super as base;`
        // so the [workaround] is:
        components_schemas
            .new_module("base")
            .vis("pub")
            .attr("allow(unused_imports, non_snake_case)")
            .scope()
            .raw("pub use super::super::super::*;");

        // also not valid
        components_schemas
            .new_module("base__super")
            .vis("pub")
            .attr("allow(unused_imports, non_snake_case)")
            .scope()
            .raw("pub use super::super::super::super::*;");

        for (ident, schema) in schemas {
            match schema {
                ObjectOrReference::Object(o) => {
                    gen_schema_as_type_in(components_schemas.scope(), ident, o)
                }
                ObjectOrReference::Ref { ref_path } => {
                    dbg!(ref_path);
                    todo!()
                }
            }
        }
    }

    writeln!(w, "{}\n", scope.to_string())
}

pub fn gen_schema_as_type_in(scope: &mut codegen::Scope, ident: &str, schema: &Schema) {
    // first generate any nested structures
    if let Some(ref props) = schema.properties {
        let parent_ident = ident;
        for (ident, prop) in props {
            let is_nested = is_new_type(prop);
            if is_nested {
                let nested_ident = format!("{}{}", parent_ident, heck::AsPascalCase(ident));
                gen_schema_as_type_in(scope, &nested_ident, prop);
            }
        }
    }

    let required_props = schema.required.as_deref().unwrap_or_default();

    if let Some(ref path) = schema.ref_path {
        // TODO: use this when it's implemented instead
        // let ty = codegen::Type::new(ident).path(&path);

        let path = path_as_rust_path(path);
        let raw = format!("pub type {} = {};", heck::AsPascalCase(ident), path);
        scope.raw(&raw);
    } else if let Some(ref vals) = schema.enum_values {
        let e = scope
            .new_enum(heck::AsPascalCase(ident).to_string())
            .vis("pub")
            .derive("Clone, Debug, ::serde::Serialize, ::serde::Deserialize, PartialEq");

        if let Some(ref desc) = schema.description {
            e.doc(desc);
        }

        for val in vals {
            e.new_variant(&heck::AsPascalCase(val).to_string())
                .attr(format!(r#"serde(rename = "{}")"#, val));
        }
    } else if schema
        .schema_type
        .as_ref()
        .filter(|&x| x != "object")
        .is_some()
    {
        let raw = format!(
            "pub type {} = {};",
            heck::AsPascalCase(ident),
            as_type_name(true, schema)
        );
        scope.raw(&raw);
    } else if let Some(ref props) = schema.properties {
        let s = scope.new_struct(ident).vis("pub");

        s.derive("Clone, Debug, ::serde::Serialize, ::serde::Deserialize, PartialEq");

        if let Some(ref desc) = schema.description {
            s.doc(desc);
        }

        s.attr("serde(deny_unknown_fields)");

        let parent_ident = ident;
        for (ident, prop) in props {
            let snake_case_ident = heck::AsSnakeCase(ident).to_string();

            let snake_case_ident = if is_rust_keyword(&snake_case_ident) {
                format!("r#{}", snake_case_ident)
            } else {
                snake_case_ident
            };

            let is_required = required_props.contains(ident);

            let is_nested = is_new_type(prop);
            let ty = if is_nested {
                format!("{}{}", parent_ident, heck::AsPascalCase(ident))
            } else {
                as_type_name(is_required, prop)
            };

            let mut field = Field::new(&snake_case_ident, ty);

            field.vis("pub");

            if !is_required {
                field.annotation("#[serde(skip_serializing_if = \"Option::is_none\")]");
            }

            // Doc comments
            if let Some(ref desc) = prop.description {
                field.doc(desc);
            }

            if &snake_case_ident != ident {
                field.annotation(format!("#[serde(rename = \"{}\")]", ident));
            }

            s.push_field(field);
        }
    } else {
        // just an empty unit type alias
        // TODO: doc comment
        scope.raw(format!("pub type {} = ();", ident));

        // dbg!(ident, schema);
        // unimplemented!();
    }
}

pub fn path_as_rust_path(path: &str) -> String {
    match path_as_rust_path_parts(path) {
        (Some(f_mod_name), Some(p_mod_path)) => {
            format!("base__super::{}::{}", f_mod_name, p_mod_path)
        }
        (_, Some(p_mod_path)) => format!("base::{}", p_mod_path),
        _ => unreachable!(),
    }
}

/// Returns (file path as rust mod path, obj ref path as rust path) parts
fn path_as_rust_path_parts(path: &str) -> (Option<&str>, Option<String>) {
    let mut path_split = path.split('#');
    let f = path_split.next().filter(|x| !x.is_empty());
    let p = path_split.next();

    let p_as_mod_path = |p: &str| {
        p.split('/').fold(String::new(), |mut s, part| {
            if !s.is_empty() {
                s.push_str("::");
            }
            s.push_str(part);
            s
        })
    };

    match (f, p) {
        (Some(f), Some(p)) => {
            let f_as_mod = std::path::Path::new(f);
            let f_mod_name = f_as_mod
                .file_stem()
                .expect("No file stem handling missing, this is a bug")
                .to_str()
                .expect("Invalid file stem name");

            let p_mod_path = p_as_mod_path(p);

            (Some(f_mod_name), Some(p_mod_path))
        }
        (_, Some(p)) => (None, Some(p_as_mod_path(p))),
        _ => (None, None),
    }
}

pub fn as_type_name(is_required: bool, schema: &Schema) -> String {
    fn inner(is_required: bool, schema: &Schema, out: &mut String) {
        if !is_required {
            out.push_str("Option<");
        }

        if let Some(ref path) = schema.ref_path {
            out.push_str(&path_as_rust_path(path));
        } else if let Some(ty) = schema.schema_type.as_deref() {
            let ty_str = match ty {
                "string" => "String",
                // TODO: integer specifics, max, min, etc.
                "integer" => "i64",
                "number" if schema.format.as_deref() == Some("double") => "f64",
                "boolean" => "bool",
                "array" => {
                    out.push_str("Vec<");

                    let items = match schema.items {
                        Some(ref i) => i,
                        None => panic!("No items type for `array`"),
                    };

                    // recurse
                    inner(true, items, out);

                    out.push('>');
                    ""
                }
                _ => {
                    dbg!(ty, schema);
                    unimplemented!();
                }
            };
            out.push_str(ty_str)
        } else {
            dbg!(schema);
            panic!();
        }

        if !is_required {
            out.push('>');
        }
    }

    let mut s = String::new();
    inner(is_required, schema, &mut s);
    s
}

fn is_new_type(schema: &Schema) -> bool {
    schema.enum_values.is_some() || /* TODO the rest of the checks */ false
}

fn is_rust_keyword(ident: &str) -> bool {
    // check for other keywords, mod, match, etc.
    ident == "type" || ident == "in"
}