use std::collections::BTreeSet;
use openapiv3::{
AdditionalProperties, ArrayType, Components, IntegerType, NumberType, OpenAPI, ReferenceOr,
Schema, SchemaKind, StringType, Type,
};
use proc_macro2::{Literal, TokenStream};
use quote::quote;
use super::types::is_string_enum;
#[must_use]
pub fn validatable_model_names(openapi: &OpenAPI) -> BTreeSet<String> {
let Some(components) = &openapi.components else {
return BTreeSet::new();
};
components
.schemas
.iter()
.filter(|(_, ref_or)| is_validatable(ref_or, components, &mut Vec::new()))
.map(|(name, _)| name.clone())
.collect()
}
fn is_validatable(
ref_or: &ReferenceOr<Schema>,
components: &Components,
visited: &mut Vec<String>,
) -> bool {
match ref_or {
ReferenceOr::Reference { reference } => {
let target = reference.rsplit('/').next().unwrap_or(reference);
if visited.iter().any(|v| v == target) {
return false;
}
visited.push(target.to_string());
components
.schemas
.get(target)
.is_some_and(|t| is_validatable(t, components, visited))
}
ReferenceOr::Item(schema) => schema_is_validatable(schema),
}
}
fn schema_is_validatable(schema: &Schema) -> bool {
if is_string_enum(schema) {
return true;
}
match &schema.schema_kind {
SchemaKind::OneOf { .. } | SchemaKind::AnyOf { .. } | SchemaKind::AllOf { .. } => true,
SchemaKind::Type(Type::Object(obj)) => {
!obj.properties.is_empty()
|| !matches!(
&obj.additional_properties,
Some(AdditionalProperties::Any(true) | AdditionalProperties::Schema(_))
)
}
_ => false,
}
}
#[must_use]
pub fn validation_attrs(prop: &ReferenceOr<Schema>, models: &BTreeSet<String>) -> TokenStream {
match prop {
ReferenceOr::Reference { reference } => {
let target = reference.rsplit('/').next().unwrap_or(reference);
if models.contains(target) {
quote! { #[validate] }
} else {
quote! {}
}
}
ReferenceOr::Item(schema) => schema_attrs(schema),
}
}
fn schema_attrs(schema: &Schema) -> TokenStream {
match &schema.schema_kind {
SchemaKind::Type(Type::String(s)) => string_attrs(s),
SchemaKind::Type(Type::Integer(i)) => integer_attrs(i),
SchemaKind::Type(Type::Number(n)) => number_attrs(n),
SchemaKind::Type(Type::Array(a)) => array_attrs(a),
SchemaKind::Type(Type::Object(obj)) if !obj.properties.is_empty() => quote! { #[validate] },
SchemaKind::OneOf { .. } | SchemaKind::AnyOf { .. } | SchemaKind::AllOf { .. } => {
quote! { #[validate] }
}
_ => quote! {},
}
}
fn string_attrs(s: &StringType) -> TokenStream {
let mut attrs = Vec::new();
if let Some(min) = s.min_length {
let lit = Literal::usize_unsuffixed(min);
attrs.push(quote! { #[validate(min_length = #lit)] });
}
if let Some(max) = s.max_length {
let lit = Literal::usize_unsuffixed(max);
attrs.push(quote! { #[validate(max_length = #lit)] });
}
if let Some(pattern) = &s.pattern {
attrs.push(quote! { #[validate(pattern = #pattern)] });
}
quote! { #(#attrs)* }
}
fn integer_attrs(i: &IntegerType) -> TokenStream {
let min = i.minimum.map(Literal::i64_unsuffixed);
let max = i.maximum.map(Literal::i64_unsuffixed);
let mult = i.multiple_of.map(Literal::i64_unsuffixed);
bound_attrs(min, i.exclusive_minimum, max, i.exclusive_maximum, mult)
}
fn number_attrs(n: &NumberType) -> TokenStream {
let min = n.minimum.map(Literal::f64_unsuffixed);
let max = n.maximum.map(Literal::f64_unsuffixed);
let mult = n.multiple_of.map(Literal::f64_unsuffixed);
bound_attrs(min, n.exclusive_minimum, max, n.exclusive_maximum, mult)
}
fn bound_attrs(
min: Option<Literal>,
exclusive_min: bool,
max: Option<Literal>,
exclusive_max: bool,
multiple_of: Option<Literal>,
) -> TokenStream {
let mut attrs = Vec::new();
if let Some(min) = min {
if exclusive_min {
attrs.push(quote! { #[validate(exclusive_minimum = #min)] });
} else {
attrs.push(quote! { #[validate(minimum = #min)] });
}
}
if let Some(max) = max {
if exclusive_max {
attrs.push(quote! { #[validate(exclusive_maximum = #max)] });
} else {
attrs.push(quote! { #[validate(maximum = #max)] });
}
}
if let Some(mult) = multiple_of {
attrs.push(quote! { #[validate(multiple_of = #mult)] });
}
quote! { #(#attrs)* }
}
fn array_attrs(a: &ArrayType) -> TokenStream {
let mut attrs = Vec::new();
if let Some(min) = a.min_items {
let lit = Literal::usize_unsuffixed(min);
attrs.push(quote! { #[validate(min_items = #lit)] });
}
if let Some(max) = a.max_items {
let lit = Literal::usize_unsuffixed(max);
attrs.push(quote! { #[validate(max_items = #lit)] });
}
if a.unique_items {
attrs.push(quote! { #[validate(unique_items)] });
}
quote! { #(#attrs)* }
}