use heck::ToPascalCase;
use openapiv3::{
AdditionalProperties, IntegerFormat, NumberFormat, ObjectType, ReferenceOr, Schema, SchemaKind,
StringFormat, StringType, Type, VariantOrUnknownOrEmpty,
};
use proc_macro2::TokenStream;
use quote::{format_ident, quote};
#[must_use]
pub fn schema_to_rust_type(ref_or: &ReferenceOr<Schema>, required: bool) -> TokenStream {
let mut sink: Vec<TokenStream> = Vec::new();
schema_to_rust_type_ctx(ref_or, required, None, &mut sink)
}
#[must_use]
pub fn schema_to_rust_type_ctx(
ref_or: &ReferenceOr<Schema>,
required: bool,
parent_name: Option<&str>,
inline_types: &mut Vec<TokenStream>,
) -> TokenStream {
let inner = ref_or_to_inner_type_ctx(ref_or, parent_name, inline_types);
if required {
inner
} else {
quote! { ::core::option::Option<#inner> }
}
}
fn ref_or_to_inner_type_ctx(
ref_or: &ReferenceOr<Schema>,
parent_name: Option<&str>,
inline_types: &mut Vec<TokenStream>,
) -> TokenStream {
match ref_or {
ReferenceOr::Reference { reference } => ref_to_ident(reference),
ReferenceOr::Item(schema) => schema_kind_to_type(schema, parent_name, inline_types),
}
}
#[must_use]
pub fn ref_to_ident(reference: &str) -> TokenStream {
let name = reference.rsplit('/').next().unwrap_or(reference);
let ident = format_ident!("{}", name.to_pascal_case());
quote! { #ident }
}
fn schema_kind_to_type(
schema: &Schema,
parent_name: Option<&str>,
inline_types: &mut Vec<TokenStream>,
) -> TokenStream {
match &schema.schema_kind {
SchemaKind::Type(Type::Object(obj)) => {
object_schema_to_type(schema, obj, parent_name, inline_types)
}
SchemaKind::Type(t) => primitive_type_to_rust(t, parent_name, inline_types),
SchemaKind::OneOf { one_of } => {
synthesize_inline_composition(parent_name, inline_types, |name, sink| {
super::compositions::generate_one_of(
name,
one_of,
schema.schema_data.discriminator.as_ref(),
schema.schema_data.description.as_ref(),
sink,
)
})
}
SchemaKind::AnyOf { any_of } => {
synthesize_inline_composition(parent_name, inline_types, |name, sink| {
super::compositions::generate_any_of(
name,
any_of,
schema.schema_data.description.as_ref(),
sink,
)
})
}
SchemaKind::AllOf { all_of } => {
synthesize_inline_composition(parent_name, inline_types, |name, sink| {
super::compositions::generate_all_of(
name,
all_of,
schema.schema_data.description.as_ref(),
sink,
)
})
}
SchemaKind::Not { .. } | SchemaKind::Any(_) => {
quote! { ::serde_json::Value }
}
}
}
fn synthesize_inline_composition(
parent_name: Option<&str>,
inline_types: &mut Vec<TokenStream>,
generate: impl FnOnce(&str, &mut Vec<TokenStream>) -> TokenStream,
) -> TokenStream {
parent_name.map_or_else(
|| quote! { ::serde_json::Value },
|name| {
let tokens = generate(name, inline_types);
inline_types.push(tokens);
let ident = format_ident!("{}", name.to_pascal_case());
quote! { #ident }
},
)
}
fn primitive_type_to_rust(
t: &Type,
parent_name: Option<&str>,
inline_types: &mut Vec<TokenStream>,
) -> TokenStream {
match t {
Type::Integer(i) => {
if i.format == openapiv3::VariantOrUnknownOrEmpty::Item(IntegerFormat::Int32) {
quote! { i32 }
} else {
quote! { i64 }
}
}
Type::Number(n) => {
if n.format == openapiv3::VariantOrUnknownOrEmpty::Item(NumberFormat::Float) {
quote! { f32 }
} else {
quote! { f64 }
}
}
Type::String(s) => string_type_to_rust(s),
Type::Boolean(_) => quote! { bool },
Type::Array(a) => {
let item_ty = a.items.as_ref().map_or_else(
|| quote! { ::serde_json::Value },
|items| ref_or_to_inner_type_ctx(&items.clone().unbox(), parent_name, inline_types),
);
quote! { ::std::vec::Vec<#item_ty> }
}
Type::Object(_) => quote! { ::serde_json::Value },
}
}
fn string_type_to_rust(s: &StringType) -> TokenStream {
if !s.enumeration.is_empty() {
return quote! { ::std::string::String };
}
match &s.format {
VariantOrUnknownOrEmpty::Item(StringFormat::Binary) => quote! { ::std::vec::Vec<u8> },
VariantOrUnknownOrEmpty::Item(StringFormat::DateTime) => {
quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }
}
VariantOrUnknownOrEmpty::Item(StringFormat::Date) => {
quote! { ::openapi_trait::chrono::NaiveDate }
}
VariantOrUnknownOrEmpty::Unknown(name) if name == "uuid" => {
quote! { ::openapi_trait::uuid::Uuid }
}
_ => quote! { ::std::string::String },
}
}
fn object_schema_to_type(
schema: &Schema,
obj: &ObjectType,
parent_name: Option<&str>,
inline_types: &mut Vec<TokenStream>,
) -> TokenStream {
if !obj.properties.is_empty() {
return synthesize_inline_composition(parent_name, inline_types, |name, sink| {
super::schemas::generate_object_struct(name, schema, obj, sink)
});
}
if let Some(ap) = &obj.additional_properties {
if let Some(value_ty) = additional_properties_value_type(ap, parent_name, inline_types) {
return quote! {
::std::collections::HashMap<::std::string::String, #value_ty>
};
}
}
quote! { ::serde_json::Value }
}
#[must_use]
pub fn additional_properties_value_type(
ap: &AdditionalProperties,
parent_name: Option<&str>,
inline_types: &mut Vec<TokenStream>,
) -> Option<TokenStream> {
match ap {
AdditionalProperties::Any(false) => None,
AdditionalProperties::Any(true) => Some(quote! { ::serde_json::Value }),
AdditionalProperties::Schema(schema) => {
Some(ref_or_to_inner_type_ctx(schema, parent_name, inline_types))
}
}
}
#[must_use]
pub fn is_string_enum(schema: &Schema) -> bool {
if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
!s.enumeration.is_empty()
} else {
false
}
}
#[must_use]
pub fn string_enum_values(schema: &Schema) -> Vec<String> {
if let SchemaKind::Type(Type::String(s)) = &schema.schema_kind {
s.enumeration.iter().filter_map(Clone::clone).collect()
} else {
vec![]
}
}
#[cfg(test)]
mod tests {
use super::*;
fn string_with_format(format: VariantOrUnknownOrEmpty<StringFormat>) -> StringType {
StringType {
format,
..Default::default()
}
}
fn emitted(s: &StringType) -> String {
string_type_to_rust(s).to_string()
}
#[test]
fn date_time_maps_to_chrono_datetime() {
let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::DateTime));
assert_eq!(
emitted(&s),
quote! { ::openapi_trait::chrono::DateTime<::openapi_trait::chrono::Utc> }.to_string()
);
}
#[test]
fn date_maps_to_chrono_naive_date() {
let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Date));
assert_eq!(
emitted(&s),
quote! { ::openapi_trait::chrono::NaiveDate }.to_string()
);
}
#[test]
fn uuid_unknown_format_maps_to_uuid() {
let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("uuid".to_string()));
assert_eq!(
emitted(&s),
quote! { ::openapi_trait::uuid::Uuid }.to_string()
);
}
#[test]
fn binary_still_maps_to_byte_vec() {
let s = string_with_format(VariantOrUnknownOrEmpty::Item(StringFormat::Binary));
assert_eq!(emitted(&s), quote! { ::std::vec::Vec<u8> }.to_string());
}
#[test]
fn email_unknown_format_stays_string() {
let s = string_with_format(VariantOrUnknownOrEmpty::Unknown("email".to_string()));
assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
}
#[test]
fn no_format_stays_string() {
let s = string_with_format(VariantOrUnknownOrEmpty::Empty);
assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
}
#[test]
fn string_enum_stays_string_even_with_format() {
let s = StringType {
format: VariantOrUnknownOrEmpty::Unknown("uuid".to_string()),
enumeration: vec![Some("a".to_string()), Some("b".to_string())],
..Default::default()
};
assert_eq!(emitted(&s), quote! { ::std::string::String }.to_string());
}
}