use std::collections::BTreeSet;
use oas3::{
Spec,
spec::{ObjectOrReference, ObjectSchema, Schema, SchemaType, SchemaTypeSet},
};
use crate::generator::{
ast::{RustType, TypeRef},
converter::{ConverterContext, cache::SharedSchemaCache},
naming::inference::has_mixed_string_variants,
schema_registry::RefCollector,
};
pub(crate) struct ConversionOutput<T> {
pub result: T,
pub inline_types: Vec<RustType>,
}
impl<T> ConversionOutput<T> {
pub(crate) fn new(result: T) -> Self {
Self {
result,
inline_types: vec![],
}
}
pub(crate) fn with_inline_types(result: T, inline_types: Vec<RustType>) -> Self {
Self { result, inline_types }
}
}
impl ConversionOutput<RustType> {
pub(crate) fn into_vec(self) -> Vec<RustType> {
let mut types = self.inline_types;
types.push(self.result);
types
}
}
pub(crate) struct InlineSchemaOutput {
pub type_name: String,
pub generated_types: Vec<RustType>,
}
pub(crate) fn handle_inline_creation<F, C>(
schema: &ObjectSchema,
base_name: &str,
forced_name: Option<String>,
context: &ConverterContext,
cached_name_check: C,
generator: F,
) -> anyhow::Result<ConversionOutput<TypeRef>>
where
F: FnOnce(&str) -> anyhow::Result<ConversionOutput<RustType>>,
C: FnOnce(&SharedSchemaCache) -> Option<String>,
{
{
let cache = context.cache.borrow();
if let Some(existing_name) = cache.get_type_name(schema)? {
return Ok(ConversionOutput::new(TypeRef::new(existing_name)));
}
if let Some(name) = cached_name_check(&cache) {
return Ok(ConversionOutput::new(TypeRef::new(name)));
}
}
let name = if let Some(forced) = forced_name {
forced
} else {
context.cache.borrow().get_preferred_name(schema, base_name)?
};
let result = generator(&name)?;
let type_name =
context
.cache
.borrow_mut()
.register_type(schema, &name, result.inline_types, result.result.clone())?;
Ok(ConversionOutput::new(TypeRef::new(type_name)))
}
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_intersection(&self) -> bool;
fn has_const_value(&self) -> bool;
fn requires_type_definition(&self) -> bool;
fn has_relaxed_anyof_enum(&self) -> bool;
fn is_freeform_string(&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_intersection(&self) -> bool {
!self.all_of.is_empty()
}
fn has_const_value(&self) -> bool {
self.const_value.is_some()
}
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 is_freeform_string(&self) -> bool {
!self.has_const_value() && !self.has_enum_values() && self.is_string()
}
}
pub(crate) fn extract_variant_references(variants: &[ObjectOrReference<ObjectSchema>]) -> BTreeSet<String> {
variants.iter().filter_map(RefCollector::parse_schema_ref).collect()
}