use inflections::Inflect;
use itertools::Itertools;
use oas3::{
Spec,
spec::{ObjectOrReference, ObjectSchema, Schema, SchemaType, SchemaTypeSet},
};
use crate::generator::{
ast::Documentation,
converter::union_types::{EnumValueEntry, UnionKind},
naming::{
constants::{REQUEST_BODY_SUFFIX, RESPONSE_PREFIX, RESPONSE_SUFFIX},
identifiers::{sanitize, to_rust_type_name},
inference::{NormalizedVariant, extract_common_variant_prefix},
},
schema_registry::{RefCollector, SchemaRegistry},
};
pub(crate) trait SchemaExt {
fn is_primitive(&self) -> bool;
fn is_null(&self) -> bool;
fn is_nullable_object(&self) -> bool;
fn is_array(&self) -> bool;
fn is_string(&self) -> bool;
fn is_object(&self) -> bool;
fn is_numeric(&self) -> bool;
fn is_nullable_array(&self) -> bool;
fn is_single_type(&self, schema_type: SchemaType) -> bool;
fn single_type(&self) -> Option<SchemaType>;
fn non_null_type(&self) -> Option<SchemaType>;
fn is_inline_object(&self) -> bool;
fn is_discriminated_base_type(&self) -> bool;
fn is_empty_object(&self) -> bool;
fn has_union(&self) -> bool;
fn has_inline_union_array_items(&self, spec: &Spec) -> bool;
fn inline_array_items<'a>(&'a self, spec: &'a Spec) -> Option<ObjectSchema>;
fn has_enum_values(&self) -> bool;
fn has_inline_enum(&self, spec: &Spec) -> bool;
fn has_intersection(&self) -> bool;
fn requires_type_definition(&self) -> bool;
fn has_relaxed_anyof_enum(&self) -> bool;
fn union_variants(&self) -> impl Iterator<Item = &ObjectOrReference<ObjectSchema>>;
fn union_variants_with_kind(&self) -> Option<(&[ObjectOrReference<ObjectSchema>], UnionKind)>;
fn has_null_variant(&self, spec: &Spec) -> bool;
fn find_non_null_variant<'a>(&'a self, spec: &Spec) -> Option<&'a ObjectOrReference<ObjectSchema>>;
fn single_non_null_variant<'a>(&'a self, spec: &Spec) -> Option<&'a ObjectOrReference<ObjectSchema>>;
fn has_inline_single_variant(&self, spec: &Spec) -> bool;
fn single_type_or_nullable(&self) -> Option<SchemaType>;
fn is_string_type(&self) -> bool;
fn is_freeform_string(&self) -> bool;
fn is_constrained(&self) -> bool;
fn is_relaxed_enum_pattern(&self) -> bool;
fn infer_variant_name(&self, index: usize) -> String;
fn infer_union_variant_label(&self, ref_name: Option<&str>, index: usize) -> String;
fn infer_object_variant_name(&self) -> String;
fn infer_name_from_required_fields(&self) -> Option<String>;
fn infer_name_from_ref_properties(&self) -> Option<String>;
fn infer_name_from_single_property(&self) -> Option<String>;
fn infer_name_from_context(&self, path: &str, context: &str) -> String;
fn extract_enum_entries(&self, spec: &Spec) -> Vec<EnumValueEntry>;
fn should_register_as_enum(&self) -> bool;
}
impl SchemaExt for ObjectSchema {
fn is_primitive(&self) -> bool {
self.properties.is_empty()
&& self.one_of.is_empty()
&& self.any_of.is_empty()
&& self.all_of.is_empty()
&& self.enum_values.len() <= 1
&& (self.schema_type.is_some() || self.enum_values.is_empty())
}
fn is_null(&self) -> bool {
self.is_single_type(SchemaType::Null)
}
fn is_nullable_object(&self) -> bool {
if self.is_null() {
return true;
}
if let Some(SchemaTypeSet::Multiple(types)) = &self.schema_type {
types.contains(&SchemaType::Null)
&& types.contains(&SchemaType::Object)
&& self.properties.is_empty()
&& self.additional_properties.is_none()
} else {
false
}
}
fn is_array(&self) -> bool {
self.is_single_type(SchemaType::Array)
}
fn is_string(&self) -> bool {
self.is_single_type(SchemaType::String)
}
fn is_object(&self) -> bool {
self.is_single_type(SchemaType::Object)
}
fn is_numeric(&self) -> bool {
matches!(
&self.schema_type,
Some(SchemaTypeSet::Single(SchemaType::Number | SchemaType::Integer))
)
}
fn is_nullable_array(&self) -> bool {
match &self.schema_type {
Some(SchemaTypeSet::Multiple(types)) => {
types.len() == 2 && types.contains(&SchemaType::Array) && types.contains(&SchemaType::Null)
}
_ => false,
}
}
fn is_single_type(&self, schema_type: SchemaType) -> bool {
matches!(
&self.schema_type,
Some(SchemaTypeSet::Single(t)) if *t == schema_type
)
}
fn single_type(&self) -> Option<SchemaType> {
match &self.schema_type {
Some(SchemaTypeSet::Single(t)) => Some(*t),
_ => None,
}
}
fn non_null_type(&self) -> Option<SchemaType> {
match &self.schema_type {
Some(SchemaTypeSet::Multiple(types)) if types.len() == 2 && types.contains(&SchemaType::Null) => {
types.iter().find(|t| **t != SchemaType::Null).copied()
}
_ => None,
}
}
fn is_inline_object(&self) -> bool {
if !self.enum_values.is_empty() {
return false;
}
if !self.one_of.is_empty() || !self.any_of.is_empty() {
return false;
}
if self.is_array() {
return false;
}
let is_object_type = self.single_type() == Some(SchemaType::Object) || self.schema_type.is_none();
is_object_type && !self.properties.is_empty()
}
fn is_discriminated_base_type(&self) -> bool {
self
.discriminator
.as_ref()
.and_then(|d| d.mapping.as_ref().map(|m| !m.is_empty()))
.unwrap_or(false)
&& !self.properties.is_empty()
}
fn is_empty_object(&self) -> bool {
self.properties.is_empty()
&& self.one_of.is_empty()
&& self.any_of.is_empty()
&& self.all_of.is_empty()
&& self.enum_values.is_empty()
&& self.schema_type.is_none()
}
fn has_union(&self) -> bool {
!self.one_of.is_empty() || !self.any_of.is_empty()
}
fn has_inline_union_array_items(&self, spec: &Spec) -> bool {
if !self.is_array() {
return false;
}
self.inline_array_items(spec).is_some_and(|items| items.has_union())
}
fn inline_array_items<'a>(&'a self, spec: &'a Spec) -> Option<ObjectSchema> {
let items_box = self.items.as_ref()?;
let items_schema_ref = match items_box.as_ref() {
Schema::Object(o) => o,
Schema::Boolean(_) => return None,
};
if matches!(&**items_schema_ref, ObjectOrReference::Ref { .. }) {
return None;
}
items_schema_ref.resolve(spec).ok()
}
fn has_enum_values(&self) -> bool {
!self.enum_values.is_empty()
}
fn has_inline_enum(&self, spec: &Spec) -> bool {
self.enum_values.len() > 1
|| self
.inline_array_items(spec)
.is_some_and(|items| items.enum_values.len() > 1)
}
fn has_intersection(&self) -> bool {
!self.all_of.is_empty()
}
fn requires_type_definition(&self) -> bool {
self.has_enum_values() || self.has_union() || (!self.properties.is_empty() && self.additional_properties.is_none())
}
fn has_relaxed_anyof_enum(&self) -> bool {
!self.any_of.is_empty() && has_mixed_string_variants(self.any_of.iter())
}
fn union_variants(&self) -> impl Iterator<Item = &ObjectOrReference<ObjectSchema>> {
self.any_of.iter().chain(&self.one_of)
}
fn union_variants_with_kind(&self) -> Option<(&[ObjectOrReference<ObjectSchema>], UnionKind)> {
let kind = UnionKind::from_schema(self);
let variants = match kind {
UnionKind::OneOf => &self.one_of,
UnionKind::AnyOf => &self.any_of,
};
if variants.is_empty() {
None
} else {
Some((variants, kind))
}
}
fn has_null_variant(&self, spec: &Spec) -> bool {
self.union_variants().any(|v| variant_is_nullable(v, spec))
}
fn find_non_null_variant<'a>(&'a self, spec: &Spec) -> Option<&'a ObjectOrReference<ObjectSchema>> {
self.union_variants().find(|v| !variant_is_nullable(v, spec))
}
fn single_non_null_variant<'a>(&'a self, spec: &Spec) -> Option<&'a ObjectOrReference<ObjectSchema>> {
let mut non_null_variants = self.union_variants().filter(|v| !variant_is_nullable(v, spec));
let first = non_null_variants.next()?;
non_null_variants.next().is_none().then_some(first)
}
fn has_inline_single_variant(&self, spec: &Spec) -> bool {
self
.single_non_null_variant(spec)
.is_some_and(|v| RefCollector::parse_schema_ref(v).is_none())
}
fn single_type_or_nullable(&self) -> Option<SchemaType> {
self.single_type().or_else(|| self.non_null_type())
}
fn is_string_type(&self) -> bool {
matches!(self.single_type_or_nullable(), Some(SchemaType::String))
}
fn is_freeform_string(&self) -> bool {
self.is_string_type() && self.enum_values.is_empty() && self.const_value.is_none()
}
fn is_constrained(&self) -> bool {
!self.enum_values.is_empty() || self.const_value.is_some()
}
fn is_relaxed_enum_pattern(&self) -> bool {
has_mixed_string_variants(self.union_variants())
}
fn infer_variant_name(&self, index: usize) -> String {
if !self.enum_values.is_empty() {
return "Enum".to_string();
}
if let Some(typ) = self.single_type_or_nullable() {
return match typ {
SchemaType::String => "String".to_string(),
SchemaType::Number => "Number".to_string(),
SchemaType::Integer => "Integer".to_string(),
SchemaType::Boolean => "Boolean".to_string(),
SchemaType::Array => "Array".to_string(),
SchemaType::Object => self.infer_object_variant_name(),
SchemaType::Null => "Null".to_string(),
};
}
if self.schema_type.is_some() {
return "Mixed".to_string();
}
let variants = if self.one_of.is_empty() {
&self.any_of
} else {
&self.one_of
};
extract_common_variant_prefix(variants).map_or_else(|| format!("Variant{index}"), |c| c.name)
}
fn infer_union_variant_label(&self, ref_name: Option<&str>, index: usize) -> String {
if let Some(const_value) = &self.const_value
&& let Ok(normalized) = NormalizedVariant::try_from(const_value)
{
return normalized.name;
}
if let Some(schema_name) = ref_name {
return to_rust_type_name(schema_name);
}
if let Some(title) = &self.title {
return to_rust_type_name(title);
}
self.infer_variant_name(index)
}
fn infer_object_variant_name(&self) -> String {
if self.properties.is_empty() {
return "Object".to_string();
}
if let Some(name) = self.infer_name_from_required_fields() {
return name;
}
if let Some(name) = self.infer_name_from_ref_properties() {
return name;
}
if let Some(name) = self.infer_name_from_single_property() {
return name;
}
"Object".to_string()
}
fn infer_name_from_required_fields(&self) -> Option<String> {
if self.required.len() == 1 {
return Some(self.required[0].to_pascal_case());
}
None
}
fn infer_name_from_ref_properties(&self) -> Option<String> {
let mut ref_names = self.properties.values().filter_map(|prop| {
if let ObjectOrReference::Ref { ref_path, .. } = prop {
SchemaRegistry::parse_ref(ref_path)
} else {
None
}
});
if let Some(first) = ref_names.next()
&& ref_names.next().is_none()
{
return Some(first.to_pascal_case());
}
None
}
fn infer_name_from_single_property(&self) -> Option<String> {
if self.properties.len() == 1 {
return self.properties.keys().next().map(|name| name.to_pascal_case());
}
None
}
fn infer_name_from_context(&self, path: &str, context: &str) -> String {
let is_request = context == REQUEST_BODY_SUFFIX;
let with_suffix = |base: &str| {
let sanitized_base = sanitize(base);
if is_request {
format!("{sanitized_base}{REQUEST_BODY_SUFFIX}")
} else {
format!("{sanitized_base}{RESPONSE_SUFFIX}")
}
};
let with_context_suffix = |base: &str| {
let sanitized_base = sanitize(base);
if is_request {
format!("{sanitized_base}{REQUEST_BODY_SUFFIX}")
} else {
format!("{sanitized_base}{context}{RESPONSE_SUFFIX}")
}
};
if let Some(title) = &self.title {
return with_suffix(title);
}
if self.properties.len() == 1
&& let Some((prop_name, _)) = self.properties.iter().next()
{
let singular = cruet::to_singular(prop_name);
return with_suffix(&singular);
}
let segments = path
.split('/')
.filter(|s| !s.is_empty() && !s.starts_with('{'))
.collect::<Vec<_>>();
segments
.last()
.map(|&s| with_context_suffix(&cruet::to_singular(s)))
.or_else(|| segments.first().map(|&s| with_context_suffix(s)))
.unwrap_or_else(|| {
if is_request {
REQUEST_BODY_SUFFIX.to_string()
} else {
format!("{RESPONSE_PREFIX}{context}")
}
})
}
fn extract_enum_entries(&self, spec: &Spec) -> Vec<EnumValueEntry> {
if !self.enum_values.is_empty() {
return self
.enum_values
.iter()
.map(|value| EnumValueEntry {
value: value.clone(),
docs: Documentation::default(),
deprecated: false,
})
.collect();
}
if let Some(const_val) = &self.const_value {
return vec![EnumValueEntry {
value: const_val.clone(),
docs: Documentation::from_optional(self.description.as_ref()),
deprecated: self.deprecated.unwrap_or(false),
}];
}
self
.union_variants()
.filter_map(|v| v.resolve(spec).ok())
.flat_map(|s| extract_variant_entries(&s))
.unique_by(|e| e.value.clone())
.collect()
}
fn should_register_as_enum(&self) -> bool {
self.has_enum_values() || self.has_relaxed_anyof_enum()
}
}
fn extract_variant_entries(schema: &ObjectSchema) -> Vec<EnumValueEntry> {
if schema.is_freeform_string() {
return vec![];
}
if let Some(const_val) = &schema.const_value {
return vec![EnumValueEntry {
value: const_val.clone(),
docs: Documentation::from_optional(schema.description.as_ref()),
deprecated: schema.deprecated.unwrap_or(false),
}];
}
schema
.enum_values
.iter()
.map(|value| EnumValueEntry {
value: value.clone(),
docs: Documentation::default(),
deprecated: false,
})
.collect()
}
fn variant_is_nullable(variant: &ObjectOrReference<ObjectSchema>, spec: &Spec) -> bool {
variant.resolve(spec).is_ok_and(|schema| schema.is_nullable_object())
}
pub(crate) fn has_mixed_string_variants<'a>(
variants: impl Iterator<Item = &'a ObjectOrReference<ObjectSchema>>,
) -> bool {
let mut has_freeform = false;
let mut has_constrained = false;
for v in variants {
if let ObjectOrReference::Object(s) = v {
if s.is_freeform_string() {
has_freeform = true;
} else if s.is_constrained() {
has_constrained = true;
}
}
if has_freeform && has_constrained {
return true;
}
}
false
}