use inflections::Inflect;
use itertools::Itertools;
use oas3::{
Spec,
spec::{FromRef, ObjectOrReference, ObjectSchema, RefError, Schema, SchemaType, SchemaTypeSet},
};
use crate::{
generator::{
ast::{VariantContent, VariantDef},
naming::{
constants::{REQUEST_BODY_SUFFIX, RESPONSE_PREFIX, RESPONSE_SUFFIX},
identifiers::{sanitize, to_rust_type_name},
inference::{NormalizedVariant, extract_common_variant_prefix},
},
},
utils::refs::{SchemaInspect, SchemaRefName},
};
pub(crate) trait SchemaResolveExt {
fn resolve_object(&self, spec: &Spec) -> Result<ObjectSchema, RefError>;
}
impl SchemaResolveExt for ObjectOrReference<ObjectSchema> {
fn resolve_object(&self, spec: &Spec) -> Result<ObjectSchema, RefError> {
match self {
ObjectOrReference::Object(schema) => Ok(schema.clone()),
ObjectOrReference::Ref { ref_path, .. } => match Schema::from_ref(spec, ref_path)? {
Schema::Object(resolved) => match *resolved {
ObjectOrReference::Object(schema) => Ok(schema),
ObjectOrReference::Ref { ref_path, .. } => Err(RefError::Unresolvable(ref_path)),
},
Schema::Boolean(_) => Ok(ObjectSchema::default()),
},
}
}
}
impl SchemaResolveExt for Schema {
fn resolve_object(&self, spec: &Spec) -> Result<ObjectSchema, RefError> {
match self {
Schema::Object(obj_ref) => obj_ref.resolve_object(spec),
Schema::Boolean(_) => Ok(ObjectSchema::default()),
}
}
}
pub trait SchemaIters {
fn variants(self) -> impl Iterator<Item = VariantDef>;
}
pub trait Resolvable {
type Output;
fn try_resolve(self, spec: &Spec) -> Option<Self::Output>;
}
impl<T: FromRef> Resolvable for &ObjectOrReference<T> {
type Output = T;
fn try_resolve(self, spec: &Spec) -> Option<T> {
self.resolve(spec).ok()
}
}
impl Resolvable for &Schema {
type Output = ObjectSchema;
fn try_resolve(self, spec: &Spec) -> Option<ObjectSchema> {
self.resolve_object(spec).ok()
}
}
impl<'a, K, T: FromRef> Resolvable for (&'a K, &'a ObjectOrReference<T>) {
type Output = (&'a K, T);
fn try_resolve(self, spec: &Spec) -> Option<(&'a K, T)> {
let value = self.1.resolve(spec).ok()?;
Some((self.0, value))
}
}
impl<'a, K> Resolvable for (&'a K, &'a Schema) {
type Output = (&'a K, ObjectSchema);
fn try_resolve(self, spec: &Spec) -> Option<(&'a K, ObjectSchema)> {
let value = self.1.resolve_object(spec).ok()?;
Some((self.0, value))
}
}
pub trait SchemaExtIters: Iterator {
fn resolve_all(self, spec: &Spec) -> impl Iterator<Item = <Self::Item as Resolvable>::Output>
where
Self::Item: Resolvable;
}
impl<I: Iterator> SchemaExtIters for I {
fn resolve_all(self, spec: &Spec) -> impl Iterator<Item = <Self::Item as Resolvable>::Output>
where
Self::Item: Resolvable,
{
self.filter_map(|item| item.try_resolve(spec))
}
}
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_selectable_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 = &Schema>;
fn union_variants_with_kind(&self) -> Option<&[Schema]>;
fn has_null_variant(&self, spec: &Spec) -> bool;
fn find_non_null_variant<'a>(&'a self, spec: &Spec) -> Option<&'a Schema>;
fn single_non_null_variant<'a>(&'a self, spec: &Spec) -> Option<&'a Schema>;
fn single_type_or_nullable(&self) -> Option<SchemaType>;
fn is_string_type(&self) -> bool;
fn is_unconstrained_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<VariantDef>;
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 = self.items.as_deref()?;
if items.ref_path().is_some() {
return None;
}
items.resolve_object(spec).ok()
}
fn has_enum_values(&self) -> bool {
!self.enum_values.is_empty()
}
fn has_selectable_values(&self) -> bool {
self.enum_values.len() > 1
}
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 = &Schema> {
self.any_of.iter().chain(&self.one_of)
}
fn union_variants_with_kind(&self) -> Option<&[Schema]> {
match (!self.one_of.is_empty(), !self.any_of.is_empty()) {
(true, false) => Some(&self.one_of),
(false, true) => Some(&self.any_of),
_ => None,
}
}
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 Schema> {
self.union_variants().find(|v| !variant_is_nullable(v, spec))
}
fn single_non_null_variant<'a>(&'a self, spec: &Spec) -> Option<&'a Schema> {
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 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_unconstrained_string(&self) -> bool {
self.is_string_type() && !self.is_constrained()
}
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(SchemaRefName::schema_ref_name);
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<VariantDef> {
if !self.enum_values.is_empty() {
return self.enum_values.iter().variants().collect();
}
if let Some(const_val) = &self.const_value {
let Ok(normalized) = NormalizedVariant::try_from(const_val) else {
return vec![];
};
return vec![
VariantDef::builder()
.normalized(normalized)
.content(VariantContent::Unit)
.schema(self)
.build(),
];
}
self
.union_variants()
.resolve_all(spec)
.flat_map(|s| extract_variant_entries(&s))
.unique_by(VariantDef::serde_name)
.collect()
}
fn should_register_as_enum(&self) -> bool {
self.has_enum_values() || self.has_relaxed_anyof_enum()
}
}
fn extract_variant_entries(schema: &ObjectSchema) -> Vec<VariantDef> {
if schema.is_unconstrained_string() {
return vec![];
}
if let Some(const_val) = &schema.const_value {
let Ok(normalized) = NormalizedVariant::try_from(const_val) else {
return vec![];
};
return vec![
VariantDef::builder()
.normalized(normalized)
.content(VariantContent::Unit)
.schema(schema)
.build(),
];
}
schema.enum_values.iter().variants().collect()
}
pub(crate) fn variant_is_nullable(variant: &Schema, spec: &Spec) -> bool {
variant
.resolve_object(spec)
.is_ok_and(|schema| schema.is_nullable_object())
}
pub(crate) fn has_mixed_string_variants<'a>(variants: impl Iterator<Item = &'a Schema>) -> bool {
let objects = variants.filter_map(Schema::as_inline);
let mut has_freeform = false;
let mut has_constrained = false;
for v in objects {
if v.is_unconstrained_string() {
if has_constrained {
return true;
}
has_freeform = true;
} else if v.is_constrained() {
if has_freeform {
return true;
}
has_constrained = true;
}
}
false
}