use std::{
borrow::Borrow,
collections::{BTreeMap, HashMap, HashSet},
hash::Hash,
};
use vespera_core::schema::{Schema, SchemaRef, SchemaType};
use super::super::{
serde_attrs::{
extract_doc_comment, extract_field_rename, extract_rename_all, rename_field,
strip_raw_prefix_owned,
},
type_schema::parse_type_to_schema_ref,
};
use crate::schema_macro::type_utils::is_option_type;
pub(super) fn build_struct_variant_properties(
fields_named: &syn::FieldsNamed,
enum_rename_all: Option<&str>,
variant_attrs: &[syn::Attribute],
known_schemas: &HashSet<impl Borrow<str> + Eq + Hash>,
struct_definitions: &HashMap<impl Borrow<str> + Eq + Hash, impl AsRef<str>>,
) -> (BTreeMap<String, SchemaRef>, Vec<String>) {
let mut variant_properties = BTreeMap::new();
let mut variant_required = Vec::with_capacity(fields_named.named.len());
let variant_rename_all = extract_rename_all(variant_attrs);
for field in &fields_named.named {
let rust_field_name = field.ident.as_ref().map_or_else(
|| "unknown".to_string(),
|i| strip_raw_prefix_owned(i.to_string()),
);
let field_name = extract_field_rename(&field.attrs).unwrap_or_else(|| {
rename_field(
&rust_field_name,
variant_rename_all.as_deref().or(enum_rename_all),
)
});
let field_type = &field.ty;
let mut schema_ref =
parse_type_to_schema_ref(field_type, known_schemas, struct_definitions);
if let Some(doc) = extract_doc_comment(&field.attrs) {
match &mut schema_ref {
SchemaRef::Inline(schema) => {
schema.description = Some(doc);
}
SchemaRef::Ref(_) => {
let ref_schema = std::mem::replace(
&mut schema_ref,
SchemaRef::Inline(Box::new(Schema::object())),
);
if let SchemaRef::Ref(reference) = ref_schema {
schema_ref = SchemaRef::Inline(Box::new(Schema {
description: Some(doc),
all_of: Some(vec![SchemaRef::Ref(reference)]),
..Default::default()
}));
}
}
}
}
variant_properties.insert(field_name.clone(), schema_ref);
let is_optional = is_option_type(field_type);
if !is_optional {
variant_required.push(field_name);
}
}
(variant_properties, variant_required)
}
pub(super) fn build_variant_data_schema(
variant: &syn::Variant,
enum_rename_all: Option<&str>,
known_schemas: &HashSet<impl Borrow<str> + Eq + Hash>,
struct_definitions: &HashMap<impl Borrow<str> + Eq + Hash, impl AsRef<str>>,
) -> Option<SchemaRef> {
match &variant.fields {
syn::Fields::Unit => None,
syn::Fields::Unnamed(fields_unnamed) => {
if fields_unnamed.unnamed.len() == 1 {
let inner_type = &fields_unnamed.unnamed[0].ty;
Some(parse_type_to_schema_ref(
inner_type,
known_schemas,
struct_definitions,
))
} else {
let mut tuple_item_schemas = Vec::with_capacity(fields_unnamed.unnamed.len());
for field in &fields_unnamed.unnamed {
let field_schema =
parse_type_to_schema_ref(&field.ty, known_schemas, struct_definitions);
tuple_item_schemas.push(field_schema);
}
let tuple_len = tuple_item_schemas.len();
Some(SchemaRef::Inline(Box::new(Schema {
prefix_items: Some(tuple_item_schemas),
min_items: Some(tuple_len),
max_items: Some(tuple_len),
items: None,
..Schema::new(SchemaType::Array)
})))
}
}
syn::Fields::Named(fields_named) => {
let (properties, required) = build_struct_variant_properties(
fields_named,
enum_rename_all,
&variant.attrs,
known_schemas,
struct_definitions,
);
Some(SchemaRef::Inline(Box::new(Schema {
properties: if properties.is_empty() {
None
} else {
Some(properties)
},
required: if required.is_empty() {
None
} else {
Some(required)
},
..Schema::object()
})))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn malformed_named_field_uses_unknown_name() {
let mut variant: syn::Variant = syn::parse_quote!(Broken { value: String });
let syn::Fields::Named(fields) = &mut variant.fields else {
panic!("named fields");
};
fields.named.first_mut().expect("field").ident = None;
let (properties, required) = build_struct_variant_properties(
fields,
None,
&[],
&HashSet::<String>::new(),
&HashMap::<String, String>::new(),
);
assert!(properties.contains_key("unknown"));
assert_eq!(required, ["unknown"]);
}
#[test]
fn empty_struct_variant_omits_properties_and_required() {
let variant: syn::Variant = syn::parse_quote!(Empty {});
let schema = build_variant_data_schema(
&variant,
None,
&HashSet::<String>::new(),
&HashMap::<String, String>::new(),
)
.expect("struct variant schema");
let SchemaRef::Inline(schema) = schema else {
panic!("inline schema");
};
assert!(schema.properties.is_none());
assert!(schema.required.is_none());
}
}