pub(crate) mod cache;
pub(crate) mod common;
pub(crate) mod discriminator;
pub(crate) mod fields;
pub(crate) mod hashing;
pub(crate) mod inline_resolver;
pub(crate) mod methods;
pub(crate) mod operations;
pub(crate) mod parameters;
pub(crate) mod relaxed_enum;
pub(crate) mod requests;
pub(crate) mod responses;
pub(crate) mod structs;
pub(crate) mod type_resolver;
pub(crate) mod type_usage_recorder;
pub(crate) mod union_types;
pub(crate) mod unions;
pub(crate) mod value_enums;
pub(crate) mod variants;
use std::{
cell::{Ref, RefCell, RefMut},
collections::HashMap,
rc::Rc,
sync::Arc,
};
use anyhow::Result;
pub(crate) use common::ConversionOutput;
use oas3::spec::ObjectSchema;
pub(crate) use operations::{OperationsProcessor, build_server_trait};
pub(crate) use type_resolver::TypeResolver;
pub(crate) use type_usage_recorder::SerdeUsageRecorder;
use crate::{
generator::{
ast::{Documentation, EnumToken, RustType, TypeAliasDef, TypeAliasToken, TypeRef},
converter::{
cache::SharedSchemaCache,
discriminator::DiscriminatorConverter,
structs::StructConverter,
unions::{EnumConverter, UnionConverter},
},
metrics::{GenerationStats, GenerationWarning},
naming::{constants::DISCRIMINATED_BASE_SUFFIX, identifiers::to_rust_type_name},
schema_registry::SchemaRegistry,
},
utils::{SchemaExt, SchemaSet},
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumCasePolicy {
Preserve,
#[default]
Deduplicate,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumHelperPolicy {
#[default]
Generate,
Disable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumDeserializePolicy {
#[default]
CaseSensitive,
CaseInsensitive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ODataPolicy {
#[default]
Disabled,
Enabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum GenerationTarget {
#[default]
Client,
Server,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SchemaScope {
#[default]
ReferencedOnly,
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum HeaderScope {
#[default]
ReferencedOnly,
All,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum EnumLayoutPolicy {
#[default]
Spec,
Sorted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CollectionTypePolicy {
#[default]
Ordered,
Hashed,
}
#[derive(Debug, Clone, Default, bon::Builder)]
pub struct CodegenConfig {
#[builder(default)]
pub enum_case: EnumCasePolicy,
#[builder(default)]
pub enum_helpers: EnumHelperPolicy,
#[builder(default)]
pub enum_deserialize: EnumDeserializePolicy,
#[builder(default)]
pub odata: ODataPolicy,
#[builder(default)]
pub target: GenerationTarget,
#[builder(default)]
pub schema_scope: SchemaScope,
#[builder(default)]
pub header_scope: HeaderScope,
#[builder(default)]
pub collection_types: CollectionTypePolicy,
#[builder(default)]
pub enum_layout: EnumLayoutPolicy,
#[builder(default)]
pub enable_builders: bool,
#[builder(default)]
pub customizations: HashMap<String, String>,
}
impl CodegenConfig {
#[must_use]
pub fn preserve_case_variants(&self) -> bool {
self.enum_case == EnumCasePolicy::Preserve
}
#[must_use]
pub fn case_insensitive_enums(&self) -> bool {
self.enum_deserialize == EnumDeserializePolicy::CaseInsensitive
}
#[must_use]
pub fn no_helpers(&self) -> bool {
self.enum_helpers == EnumHelperPolicy::Disable
}
#[must_use]
pub fn odata_support(&self) -> bool {
self.odata == ODataPolicy::Enabled
}
#[must_use]
pub fn include_all_schemas(&self) -> bool {
self.schema_scope == SchemaScope::All
}
#[must_use]
pub fn include_all_headers(&self) -> bool {
self.header_scope == HeaderScope::All
}
#[must_use]
pub fn enable_builders(&self) -> bool {
self.enable_builders
}
#[must_use]
pub fn ordered_collections(&self) -> bool {
self.collection_types == CollectionTypePolicy::Ordered
}
#[must_use]
pub fn sort_enum_variants(&self) -> bool {
self.enum_layout == EnumLayoutPolicy::Sorted
}
#[must_use]
pub fn map_type_path(&self) -> &'static str {
match self.collection_types {
CollectionTypePolicy::Ordered => "indexmap::IndexMap",
CollectionTypePolicy::Hashed => "std::collections::HashMap",
}
}
}
#[derive(Debug, Clone)]
pub(crate) struct ConverterContext {
pub(crate) graph: Arc<SchemaRegistry>,
pub(crate) config: CodegenConfig,
pub(crate) cache: RefCell<SharedSchemaCache>,
pub(crate) type_usage: RefCell<SerdeUsageRecorder>,
pub(crate) reachable_schemas: Option<Arc<SchemaSet>>,
}
impl ConverterContext {
pub(crate) fn new(
graph: Arc<SchemaRegistry>,
config: CodegenConfig,
cache: SharedSchemaCache,
reachable_schemas: Option<Arc<SchemaSet>>,
) -> Self {
Self {
graph,
config,
cache: RefCell::new(cache),
type_usage: RefCell::new(SerdeUsageRecorder::new()),
reachable_schemas,
}
}
pub(crate) fn graph(&self) -> &SchemaRegistry {
&self.graph
}
pub(crate) fn config(&self) -> &CodegenConfig {
&self.config
}
pub(crate) fn take_type_usage(&self) -> SerdeUsageRecorder {
self.type_usage.take()
}
pub(crate) fn mark_request(&self, type_name: impl Into<EnumToken>) {
self.type_usage.borrow_mut().mark_request(type_name);
}
pub(crate) fn mark_response(&self, type_name: impl Into<EnumToken>) {
self.type_usage.borrow_mut().mark_response(type_name);
}
pub(crate) fn mark_request_iter<I, T>(&self, types: I)
where
I: IntoIterator<Item = T>,
T: Into<super::ast::EnumToken>,
{
self.type_usage.borrow_mut().mark_request_iter(types);
}
pub(crate) fn mark_response_type_ref(&self, type_ref: &TypeRef) {
self.type_usage.borrow_mut().mark_response_type_ref(type_ref);
}
pub(crate) fn merge_usage(&self, other: SerdeUsageRecorder) {
self.type_usage.borrow_mut().merge(other);
}
pub(crate) fn cache(&self) -> Ref<'_, SharedSchemaCache> {
self.cache.borrow()
}
pub(crate) fn cache_mut(&self) -> RefMut<'_, SharedSchemaCache> {
self.cache.borrow_mut()
}
}
#[derive(Debug, Clone)]
pub(crate) struct SchemaConverter {
context: Rc<ConverterContext>,
type_resolver: TypeResolver,
struct_converter: StructConverter,
enum_converter: EnumConverter,
union_converter: UnionConverter,
discriminator_converter: DiscriminatorConverter,
}
impl SchemaConverter {
pub(crate) fn new(context: &Rc<ConverterContext>) -> Self {
Self {
type_resolver: TypeResolver::new(context.clone()),
struct_converter: StructConverter::new(context.clone()),
enum_converter: EnumConverter::new(context.clone()),
union_converter: UnionConverter::new(context.clone()),
discriminator_converter: DiscriminatorConverter::new(context.clone()),
context: context.clone(),
}
}
pub(crate) fn context(&self) -> &Rc<ConverterContext> {
&self.context
}
pub(crate) fn contains(&self, name: &str) -> bool {
self.context.cache().contains_schema_name(name)
}
pub(crate) fn convert_schema(&self, name: &str, schema: &ObjectSchema) -> Result<Vec<RustType>> {
if schema.has_intersection() {
return self.struct_converter.convert_all_of_schema(name);
}
if schema.union_variants_with_kind().is_some() {
if schema.discriminator.is_none() && self.type_resolver.is_wrapper_union(schema)? {
return self.convert_nullable_enum(name, schema);
}
if let Some(flattened) = self.type_resolver.try_flatten_nested_union(schema)? {
return self
.union_converter
.convert_union(name, &flattened.into())
.map(ConversionOutput::into_vec);
}
return self
.union_converter
.convert_union(name, schema)
.map(ConversionOutput::into_vec);
}
if !schema.enum_values.is_empty() {
return Ok(vec![self.enum_converter.convert_value_enum(name, schema)]);
}
if !schema.properties.is_empty() || schema.additional_properties.is_some() {
let result = self.struct_converter.convert_struct(name, schema, None)?;
return self.finalize_struct_types(name, schema, result.result, result.inline_types);
}
if let Some(output) = self.try_array_alias(name, schema)? {
let alias = RustType::TypeAlias(TypeAliasDef {
name: TypeAliasToken::from_raw(name),
docs: Documentation::from_optional(schema.description.as_ref()),
target: output.result,
});
let mut result = vec![alias];
result.extend(output.inline_types);
return Ok(result);
}
let type_ref = self.type_resolver.resolve_type(schema)?;
Ok(vec![RustType::TypeAlias(TypeAliasDef {
name: TypeAliasToken::from_raw(name),
docs: Documentation::from_optional(schema.description.as_ref()),
target: type_ref,
})])
}
fn discriminated_enum(&self, name: &str, schema: &ObjectSchema, fallback_type: &str) -> Result<RustType> {
self
.discriminator_converter
.build_base_discriminated_enum(name, schema, fallback_type)
}
pub(crate) fn finalize_struct_types(
&self,
name: &str,
schema: &ObjectSchema,
main_type: RustType,
inline_types: Vec<RustType>,
) -> Result<Vec<RustType>> {
let discriminated_enum = schema
.is_discriminated_base_type()
.then(|| {
let base_struct_name = match &main_type {
RustType::Struct(def) => def.name.as_str().to_string(),
_ => format!("{}{DISCRIMINATED_BASE_SUFFIX}", to_rust_type_name(name)),
};
self.discriminated_enum(name, schema, &base_struct_name)
})
.transpose()?;
Ok(
discriminated_enum
.into_iter()
.chain(std::iter::once(main_type))
.chain(inline_types)
.collect(),
)
}
fn convert_nullable_enum(&self, name: &str, schema: &ObjectSchema) -> Result<Vec<RustType>> {
let spec = self.context.graph().spec();
let Some(non_null_variant) = schema.find_non_null_variant(spec) else {
return Ok(vec![]);
};
let resolved = self.type_resolver.resolve(non_null_variant)?;
if resolved.has_enum_values() && resolved.enum_values.len() > 1 {
return Ok(vec![self.enum_converter.convert_value_enum(name, &resolved)]);
}
Ok(vec![])
}
fn try_array_alias(&self, name: &str, schema: &ObjectSchema) -> Result<Option<ConversionOutput<TypeRef>>> {
if !schema.is_array() && !schema.is_nullable_array() {
return Ok(None);
}
if let Some(output) = self.type_resolver.try_inline_array(name, name, schema)? {
let type_ref = if schema.is_nullable_array() {
output.result.with_option()
} else {
output.result
};
return Ok(Some(ConversionOutput::with_inline_types(type_ref, output.inline_types)));
}
Ok(None)
}
pub(crate) fn convert_all_schemas(
&self,
operation_reachable: Option<&SchemaSet>,
stats: &mut GenerationStats,
) -> Vec<RustType> {
let schemas = self.context.graph().schemas();
let filtered = schemas
.iter()
.filter(|(name, _)| operation_reachable.is_none_or(|filter| filter.contains(name.as_str())))
.collect::<Vec<_>>();
let ordered = filtered;
{
let mut cache = self.context().cache_mut();
for (name, schema) in &ordered {
let _ = cache.register_top_level_schema(schema, name);
}
}
ordered.into_iter().fold(vec![], |mut acc, (name, schema)| {
match self.convert_schema(name, schema) {
Ok(types) => acc.extend(types),
Err(e) => stats.record_warning(GenerationWarning::SchemaConversionFailed {
schema_name: name.clone(),
error: e.to_string(),
}),
}
acc
})
}
}
#[cfg(test)]
mod tests;