feat(codegen): various improvements to robustness
This commit is contained in:
parent
a90bcb00c4
commit
cf98d24090
7 changed files with 344 additions and 96 deletions
|
|
@ -1,12 +1,12 @@
|
|||
use heck::{ToSnakeCase, ToUpperCamelCase};
|
||||
use indexmap::IndexMap;
|
||||
use indexmap::{map::Entry, IndexMap};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::{format_ident, quote, ToTokens};
|
||||
use syn::Ident;
|
||||
|
||||
use crate::openapi::r#type::OpenApiType;
|
||||
|
||||
use super::{r#enum::Enum, ResolvedSchema};
|
||||
use super::{r#enum::Enum, ResolvedSchema, WarningReporter};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PrimitiveType {
|
||||
|
|
@ -85,6 +85,7 @@ impl PropertyType {
|
|||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Property {
|
||||
pub field_name: String,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub required: bool,
|
||||
|
|
@ -99,24 +100,31 @@ impl Property {
|
|||
required: bool,
|
||||
schema: &OpenApiType,
|
||||
schemas: &IndexMap<&str, OpenApiType>,
|
||||
warnings: WarningReporter,
|
||||
) -> Option<Self> {
|
||||
let name = name.to_owned();
|
||||
let field_name = name.to_snake_case();
|
||||
let description = schema.description.as_deref().map(ToOwned::to_owned);
|
||||
|
||||
match schema {
|
||||
OpenApiType {
|
||||
r#enum: Some(_), ..
|
||||
} => Some(Self {
|
||||
r#type: PropertyType::Enum(Enum::from_schema(
|
||||
&name.clone().to_upper_camel_case(),
|
||||
schema,
|
||||
)?),
|
||||
name,
|
||||
description,
|
||||
required,
|
||||
deprecated: schema.deprecated,
|
||||
nullable: false,
|
||||
}),
|
||||
} => {
|
||||
let Some(r#enum) = Enum::from_schema(&name.clone().to_upper_camel_case(), schema)
|
||||
else {
|
||||
warnings.push("Failed to create enum");
|
||||
return None;
|
||||
};
|
||||
Some(Self {
|
||||
r#type: PropertyType::Enum(r#enum),
|
||||
name,
|
||||
field_name,
|
||||
description,
|
||||
required,
|
||||
deprecated: schema.deprecated,
|
||||
nullable: false,
|
||||
})
|
||||
}
|
||||
OpenApiType {
|
||||
one_of: Some(types),
|
||||
..
|
||||
|
|
@ -125,7 +133,7 @@ impl Property {
|
|||
r#type: Some("null"),
|
||||
..
|
||||
}] => {
|
||||
let mut inner = Self::from_schema(&name, required, left, schemas)?;
|
||||
let mut inner = Self::from_schema(&name, required, left, schemas, warnings)?;
|
||||
inner.nullable = true;
|
||||
Some(inner)
|
||||
}
|
||||
|
|
@ -137,14 +145,19 @@ impl Property {
|
|||
one_of: Some(left.to_owned()),
|
||||
..schema.clone()
|
||||
};
|
||||
let mut inner = Self::from_schema(&name, required, &rest, schemas)?;
|
||||
let mut inner = Self::from_schema(&name, required, &rest, schemas, warnings)?;
|
||||
inner.nullable = true;
|
||||
Some(inner)
|
||||
}
|
||||
cases => {
|
||||
let r#enum = Enum::from_one_of(&name.to_upper_camel_case(), cases)?;
|
||||
let Some(r#enum) = Enum::from_one_of(&name.to_upper_camel_case(), cases) else {
|
||||
warnings.push("Failed to create oneOf enum");
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
field_name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
|
|
@ -157,9 +170,12 @@ impl Property {
|
|||
all_of: Some(types),
|
||||
..
|
||||
} => {
|
||||
let composite = Object::from_all_of(&name.to_upper_camel_case(), types, schemas)?;
|
||||
let obj_name = name.to_upper_camel_case();
|
||||
let composite =
|
||||
Object::from_all_of(&obj_name, types, schemas, warnings.child(&obj_name));
|
||||
Some(Self {
|
||||
name,
|
||||
field_name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
|
|
@ -170,23 +186,29 @@ impl Property {
|
|||
OpenApiType {
|
||||
r#type: Some("object"),
|
||||
..
|
||||
} => Some(Self {
|
||||
r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
|
||||
&name.clone().to_upper_camel_case(),
|
||||
schema,
|
||||
schemas,
|
||||
)?)),
|
||||
name,
|
||||
description,
|
||||
required,
|
||||
deprecated: schema.deprecated,
|
||||
nullable: false,
|
||||
}),
|
||||
} => {
|
||||
let obj_name = name.to_upper_camel_case();
|
||||
Some(Self {
|
||||
r#type: PropertyType::Nested(Box::new(Object::from_schema_object(
|
||||
&obj_name,
|
||||
schema,
|
||||
schemas,
|
||||
warnings.child(&obj_name),
|
||||
))),
|
||||
name,
|
||||
field_name,
|
||||
description,
|
||||
required,
|
||||
deprecated: schema.deprecated,
|
||||
nullable: false,
|
||||
})
|
||||
}
|
||||
OpenApiType {
|
||||
ref_path: Some(path),
|
||||
..
|
||||
} => Some(Self {
|
||||
name,
|
||||
field_name,
|
||||
description,
|
||||
r#type: PropertyType::Ref((*path).to_owned()),
|
||||
required,
|
||||
|
|
@ -198,10 +220,11 @@ impl Property {
|
|||
items: Some(items),
|
||||
..
|
||||
} => {
|
||||
let inner = Self::from_schema(&name, required, items, schemas)?;
|
||||
let inner = Self::from_schema(&name, required, items, schemas, warnings)?;
|
||||
|
||||
Some(Self {
|
||||
name,
|
||||
field_name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
|
|
@ -226,6 +249,7 @@ impl Property {
|
|||
|
||||
Some(Self {
|
||||
name,
|
||||
field_name,
|
||||
description,
|
||||
required,
|
||||
nullable: false,
|
||||
|
|
@ -233,7 +257,10 @@ impl Property {
|
|||
r#type: PropertyType::Primitive(prim),
|
||||
})
|
||||
}
|
||||
_ => None,
|
||||
_ => {
|
||||
warnings.push("Could not resolve property type");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -247,11 +274,11 @@ impl Property {
|
|||
let name = &self.name;
|
||||
let (name, serde_attr) = match name.as_str() {
|
||||
"type" => (format_ident!("r#type"), None),
|
||||
name if name != name.to_snake_case() => (
|
||||
format_ident!("{}", name.to_snake_case()),
|
||||
name if name != self.field_name => (
|
||||
format_ident!("{}", self.field_name),
|
||||
Some(quote! { #[serde(rename = #name)]}),
|
||||
),
|
||||
_ => (format_ident!("{name}"), None),
|
||||
_ => (format_ident!("{}", self.field_name), None),
|
||||
};
|
||||
|
||||
let ty_inner = self.r#type.codegen(namespace, resolved)?;
|
||||
|
|
@ -283,7 +310,7 @@ impl Property {
|
|||
pub struct Object {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub properties: Vec<Property>,
|
||||
pub properties: IndexMap<String, Property>,
|
||||
}
|
||||
|
||||
impl Object {
|
||||
|
|
@ -291,7 +318,8 @@ impl Object {
|
|||
name: &str,
|
||||
schema: &OpenApiType,
|
||||
schemas: &IndexMap<&str, OpenApiType>,
|
||||
) -> Option<Self> {
|
||||
warnings: WarningReporter,
|
||||
) -> Self {
|
||||
let mut result = Object {
|
||||
name: name.to_owned(),
|
||||
description: schema.description.as_deref().map(ToOwned::to_owned),
|
||||
|
|
@ -299,39 +327,54 @@ impl Object {
|
|||
};
|
||||
|
||||
let Some(props) = &schema.properties else {
|
||||
return None;
|
||||
warnings.push("Missing properties");
|
||||
return result;
|
||||
};
|
||||
|
||||
let required = schema.required.clone().unwrap_or_default();
|
||||
|
||||
for (prop_name, prop) in props {
|
||||
// HACK: This will cause a duplicate key otherwise
|
||||
if ["itemDetails", "sci-fi", "non-attackers", "co-leader_id"].contains(prop_name) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// TODO: implement custom enum for this (depends on overrides being added)
|
||||
// Maybe this is an issue with the schema instead?
|
||||
if *prop_name == "value" && name == "TornHof" {
|
||||
continue;
|
||||
}
|
||||
|
||||
result.properties.push(Property::from_schema(
|
||||
let Some(prop) = Property::from_schema(
|
||||
prop_name,
|
||||
required.contains(prop_name),
|
||||
prop,
|
||||
schemas,
|
||||
)?);
|
||||
warnings.child(prop_name),
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let field_name = prop.field_name.clone();
|
||||
|
||||
let entry = result.properties.entry(field_name.clone());
|
||||
if let Entry::Occupied(mut entry) = entry {
|
||||
let other_name = entry.get().name.clone();
|
||||
warnings.push(format!(
|
||||
"Property name collision: {other_name} and {field_name}"
|
||||
));
|
||||
// deprioritise kebab and camelcase
|
||||
if other_name.contains('-')
|
||||
|| other_name
|
||||
.chars()
|
||||
.filter(|c| c.is_alphabetic())
|
||||
.all(|c| c.is_ascii_lowercase())
|
||||
{
|
||||
entry.insert(prop);
|
||||
}
|
||||
} else {
|
||||
entry.insert_entry(prop);
|
||||
}
|
||||
}
|
||||
|
||||
Some(result)
|
||||
result
|
||||
}
|
||||
|
||||
pub fn from_all_of(
|
||||
name: &str,
|
||||
types: &[OpenApiType],
|
||||
schemas: &IndexMap<&str, OpenApiType>,
|
||||
) -> Option<Self> {
|
||||
warnings: WarningReporter,
|
||||
) -> Self {
|
||||
let mut result = Self {
|
||||
name: name.to_owned(),
|
||||
..Default::default()
|
||||
|
|
@ -339,22 +382,29 @@ impl Object {
|
|||
|
||||
for r#type in types {
|
||||
let r#type = if let OpenApiType {
|
||||
ref_path: Some(path),
|
||||
ref_path: Some(ref_path),
|
||||
..
|
||||
} = r#type
|
||||
{
|
||||
let name = path.strip_prefix("#/components/schemas/")?;
|
||||
schemas.get(name)?
|
||||
let Some(name) = ref_path.strip_prefix("#/components/schemas/") else {
|
||||
warnings.push(format!("Malformed ref {ref_path}"));
|
||||
continue;
|
||||
};
|
||||
let Some(schema) = schemas.get(name) else {
|
||||
warnings.push(format!("Missing schema for ref {name}"));
|
||||
continue;
|
||||
};
|
||||
schema
|
||||
} else {
|
||||
r#type
|
||||
};
|
||||
let obj = Self::from_schema_object(name, r#type, schemas)?;
|
||||
let obj = Self::from_schema_object(name, r#type, schemas, warnings.child("variant"));
|
||||
|
||||
result.description = result.description.or(obj.description);
|
||||
result.properties.extend(obj.properties);
|
||||
}
|
||||
|
||||
Some(result)
|
||||
result
|
||||
}
|
||||
|
||||
pub fn codegen(&self, resolved: &ResolvedSchema) -> Option<TokenStream> {
|
||||
|
|
@ -371,7 +421,7 @@ impl Object {
|
|||
};
|
||||
|
||||
let mut props = Vec::with_capacity(self.properties.len());
|
||||
for prop in &self.properties {
|
||||
for (_, prop) in &self.properties {
|
||||
props.push(prop.codegen(&mut namespace, resolved)?);
|
||||
}
|
||||
|
||||
|
|
@ -441,7 +491,14 @@ mod test {
|
|||
for (name, desc) in &schema.components.schemas {
|
||||
if desc.r#type == Some("object") {
|
||||
objects += 1;
|
||||
if Object::from_schema_object(name, desc, &schema.components.schemas).is_none() {
|
||||
let reporter = WarningReporter::new();
|
||||
Object::from_schema_object(
|
||||
name,
|
||||
desc,
|
||||
&schema.components.schemas,
|
||||
reporter.clone(),
|
||||
);
|
||||
if !reporter.is_empty() {
|
||||
unresolved.push(name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue