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,
}
}
}
pub fn generate_components<W: Write>(w: &mut W, spec: &Spec) -> io::Result<()> {
generate_components_with_opts(w, spec, &GenerationOpts::default())
}
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 {
let components_schemas = scope
.new_module("components")
.vis("pub")
.new_module("schemas")
.vis("pub");
components_schemas
.new_module("base")
.vis("pub")
.attr("allow(unused_imports, non_snake_case)")
.scope()
.raw("pub use super::super::super::*;");
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) {
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 {
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\")]");
}
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 {
scope.raw(format!("pub type {} = ();", ident));
}
}
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!(),
}
}
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",
"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`"),
};
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() || false
}
fn is_rust_keyword(ident: &str) -> bool {
ident == "type" || ident == "in"
}