use crate::datatypes::{DataType, FieldType, StructField};
use crate::filter::FilterConfig;
use crate::openapi::{OpenApi, Schema};
use std::error::Error;
pub fn process_components(
spec: &OpenApi,
filter: &FilterConfig,
) -> Result<Vec<DataType>, Box<dyn Error>> {
let mut dependencies = vec![];
let mut datatypes = vec![];
for (schema_name, definition) in spec.components.schemas.iter() {
if !filter.is_schema_accepted(schema_name) {
continue;
}
let datatype = process_schema(schema_name, definition, filter)?;
datatypes.push(datatype);
if filter.auto_include_dependencies {
find_dependend_schemas(&schema_name, spec, filter, &mut dependencies);
}
}
for schema_name in dependencies {
if datatypes.iter().any(|dt| dt.schema_name() == schema_name) {
continue;
}
if let Some(definition) = spec.components.schemas.get(&schema_name) {
datatypes.push(process_schema(&schema_name, definition, filter)?);
}
}
Ok(datatypes)
}
fn process_schema(
schema_name: &str,
definition: &Schema,
filter: &FilterConfig,
) -> Result<DataType, Box<dyn Error>> {
match definition {
Schema::Ref { .. } => {
let msg = format!("unexpected reference in root of {schema_name:?} definition");
Err(msg.into())
}
Schema::Typed {
properties,
enum_items,
required,
..
} => {
if let Some(props) = properties {
let mut fields = vec![];
for (prop_name, prop_definition) in props {
if !filter.is_property_accepted(schema_name, prop_name) {
continue;
}
let field = process_schema_property(
schema_name,
&prop_name,
prop_definition,
required.contains(prop_name),
)?;
fields.push(field);
}
Ok(DataType::Struct {
name: schema_name.to_owned(),
fields,
})
} else if let Some(items) = enum_items {
Ok(DataType::Enum {
name: schema_name.to_owned(),
items: items.clone(),
})
} else {
Ok(DataType::Alias {
alias: schema_name.to_owned(),
info: process_schema_property(schema_name, "", definition, true)?,
})
}
}
}
}
fn process_schema_property(
schema_name: &str,
name: &str,
definition: &Schema,
is_required: bool,
) -> Result<StructField, Box<dyn Error>> {
let translated_name = translate_name(name);
if !translated_name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_')
{
let msg = format!("property {schema_name:?}.{name:?} has untranslatable name");
return Err(msg.into());
}
match definition {
Schema::Ref { ref_ } => {
return Ok(StructField {
name: name.to_owned(),
translated_name,
type_: FieldType::Plain(
ref_.strip_prefix("#/components/schemas/")
.unwrap()
.to_owned(),
),
type_format: String::new(),
array_dimensions: 0,
is_nullable: !is_required,
descr: String::new(),
});
}
Schema::Typed {
schema_type,
format,
nullable,
description,
properties,
items,
enum_items: _,
all_of,
any_of,
one_of,
..
} => {
if let Some(_) = properties {
let msg = format!(
"property {schema_name:?}.{name:?} cannot be a nested object, use $ref instead"
);
Err(msg.into())
} else if let Some(items) = items {
let mut field = process_schema_property(schema_name, name, items, is_required)?;
field.array_dimensions += 1;
field.is_nullable = field.is_nullable | *nullable;
if field.descr.is_empty() && !description.is_empty() {
field.descr = description.clone();
}
Ok(field)
} else if let Some(schemas) = all_of {
if schemas.len() > 1 {
return Err(
format!("{schema_name:?}.{name:?}: expected one ref in `allOf`").into(),
);
}
let mut field =
process_schema_property(schema_name, name, &schemas[0], is_required)?;
field.is_nullable = field.is_nullable | *nullable;
if field.descr.is_empty() && !description.is_empty() {
field.descr = description.clone();
}
Ok(field)
} else if let Some(schemas) = one_of {
let mut types = vec![];
for schema in schemas {
let field = process_schema_property(schema_name, name, schema, is_required)?;
types.extend(field.type_.to_vec());
}
Ok(StructField {
name: name.to_owned(),
translated_name,
type_: FieldType::OneOf(types),
type_format: String::new(),
array_dimensions: 0,
is_nullable: *nullable,
descr: description.clone(),
})
} else if let Some(_) = any_of {
Err(format!("{schema_name:?}.{name:?}: `anyOf` is not supported").into())
} else if !schema_type.is_empty() {
return Ok(StructField {
name: name.to_owned(),
translated_name,
type_: FieldType::Plain(schema_type.clone()),
type_format: format.clone(),
is_nullable: *nullable | !is_required,
descr: description.clone(),
array_dimensions: 0,
});
} else {
Ok(StructField {
name: String::new(),
translated_name,
type_: FieldType::Plain("object".into()),
type_format: String::new(),
array_dimensions: 0,
is_nullable: *nullable | !is_required,
descr: description.clone(),
})
}
}
}
}
pub fn find_missing_schemas(datatypes: &[DataType]) -> Vec<String> {
let mut missing_schemas = vec![];
for dt in datatypes {
match dt {
DataType::Enum { .. } => (),
DataType::Struct { fields, .. } => {
for field in fields {
for t in field.type_.to_vec() {
if !is_primitive_type(&t) {
if !datatypes.iter().any(|dt| dt.schema_name() == t) {
missing_schemas.push(t.clone());
}
}
}
}
}
DataType::Alias { info, .. } => {
for t in info.type_.to_vec() {
if !is_primitive_type(&t) {
if !datatypes.iter().any(|dt| dt.schema_name() == t) {
missing_schemas.push(t.clone());
}
}
}
}
}
}
missing_schemas.dedup();
missing_schemas
}
fn is_primitive_type(typename: &str) -> bool {
matches!(
typename,
"string" | "number" | "boolean" | "integer" | "array" | "object"
)
}
fn find_dependend_schemas(
schema_name: &str,
spec: &OpenApi,
filter: &FilterConfig,
dependencies: &mut Vec<String>,
) {
if let Some(definition) = spec.components.schemas.get(schema_name) {
if let Ok(dt) = process_schema(schema_name, definition, filter) {
match dt {
DataType::Struct { fields, .. } => {
for field in fields {
for t in field.type_.to_vec() {
if is_primitive_type(&t) {
continue;
}
if !dependencies.contains(&t) {
dependencies.push(t.clone());
find_dependend_schemas(&t, spec, filter, dependencies);
}
}
}
}
DataType::Alias { info, .. } => {
for t in info.type_.to_vec() {
if is_primitive_type(&t) {
continue;
}
if !dependencies.contains(&t) {
dependencies.push(t.clone());
find_dependend_schemas(&t, spec, filter, dependencies);
}
}
}
DataType::Enum { .. } => (),
}
}
}
}
fn translate_name(name: &str) -> String {
let mut result = String::with_capacity(name.len());
for c in name.chars() {
match c {
'$' => result += "_dollar_",
'#' => result += "_number_",
'%' => result += "_percent_",
'&' => result += "_ampersand_",
'(' => result += "_lparen_",
')' => result += "_rparen_",
'*' => result += "_asterisk_",
'-' => result += "_minus_",
'@' => result += "_at_",
'?' => result += "_question_",
_ => result.push(c),
}
}
result
}