use std::collections::HashSet;
use proc_macro2::TokenStream;
use quote::quote;
use vespera_core::schema::{Schema, SchemaRef, SchemaType};
use super::type_utils::is_option_type;
use crate::{
metadata::StructMetadata,
parser::{
extract_default, extract_field_rename, extract_rename_all, extract_skip,
extract_skip_serializing_if, parse_type_to_schema_ref, rename_field, strip_raw_prefix,
},
};
pub fn generate_filtered_schema(
struct_item: &syn::ItemStruct,
omit_set: &HashSet<String>,
pick_set: &HashSet<String>,
schema_storage: &[StructMetadata],
) -> Result<TokenStream, syn::Error> {
let rename_all = extract_rename_all(&struct_item.attrs);
let known_schemas: std::collections::HashMap<String, String> = schema_storage
.iter()
.map(|s| (s.name.clone(), s.definition.clone()))
.collect();
let struct_definitions = known_schemas.clone();
let mut property_tokens = Vec::new();
let mut required_fields = Vec::new();
if let syn::Fields::Named(fields_named) = &struct_item.fields {
for field in &fields_named.named {
if extract_skip(&field.attrs) {
continue;
}
let rust_field_name = field
.ident
.as_ref()
.map(|i| strip_raw_prefix(&i.to_string()).to_string())
.unwrap_or_else(|| "unknown".to_string());
let field_name = if let Some(renamed) = extract_field_rename(&field.attrs) {
renamed
} else {
rename_field(&rust_field_name, rename_all.as_deref())
};
if !omit_set.is_empty()
&& (omit_set.contains(&rust_field_name) || omit_set.contains(&field_name))
{
continue;
}
if !pick_set.is_empty()
&& !pick_set.contains(&rust_field_name)
&& !pick_set.contains(&field_name)
{
continue;
}
let field_type = &field.ty;
let schema_ref =
parse_type_to_schema_ref(field_type, &known_schemas, &struct_definitions);
let schema_ref_tokens = schema_ref_to_tokens(&schema_ref);
property_tokens.push(quote! {
properties.insert(#field_name.to_string(), #schema_ref_tokens);
});
let has_default = extract_default(&field.attrs).is_some();
let has_skip_serializing_if = extract_skip_serializing_if(&field.attrs);
let is_optional = is_option_type(field_type);
if !is_optional && !has_default && !has_skip_serializing_if {
required_fields.push(field_name.clone());
}
}
}
let required_tokens = if required_fields.is_empty() {
quote! { None }
} else {
let required_strs: Vec<&str> = required_fields.iter().map(|s| s.as_str()).collect();
quote! { Some(vec![#(#required_strs.to_string()),*]) }
};
Ok(quote! {
{
let mut properties = std::collections::BTreeMap::new();
#(#property_tokens)*
vespera::schema::Schema {
schema_type: Some(vespera::schema::SchemaType::Object),
properties: if properties.is_empty() { None } else { Some(properties) },
required: #required_tokens,
..vespera::schema::Schema::new(vespera::schema::SchemaType::Object)
}
}
})
}
pub fn schema_ref_to_tokens(schema_ref: &SchemaRef) -> TokenStream {
match schema_ref {
SchemaRef::Ref(reference) => {
let ref_path = &reference.ref_path;
quote! {
vespera::schema::SchemaRef::Ref(vespera::schema::Reference::new(#ref_path.to_string()))
}
}
SchemaRef::Inline(schema) => {
let schema_tokens = schema_to_tokens(schema);
quote! {
vespera::schema::SchemaRef::Inline(Box::new(#schema_tokens))
}
}
}
}
pub fn schema_to_tokens(schema: &Schema) -> TokenStream {
let schema_type_tokens = match &schema.schema_type {
Some(SchemaType::String) => quote! { Some(vespera::schema::SchemaType::String) },
Some(SchemaType::Number) => quote! { Some(vespera::schema::SchemaType::Number) },
Some(SchemaType::Integer) => quote! { Some(vespera::schema::SchemaType::Integer) },
Some(SchemaType::Boolean) => quote! { Some(vespera::schema::SchemaType::Boolean) },
Some(SchemaType::Array) => quote! { Some(vespera::schema::SchemaType::Array) },
Some(SchemaType::Object) => quote! { Some(vespera::schema::SchemaType::Object) },
Some(SchemaType::Null) => quote! { Some(vespera::schema::SchemaType::Null) },
None => quote! { None },
};
let format_tokens = match &schema.format {
Some(f) => quote! { Some(#f.to_string()) },
None => quote! { None },
};
let nullable_tokens = match schema.nullable {
Some(true) => quote! { Some(true) },
Some(false) => quote! { Some(false) },
None => quote! { None },
};
let ref_path_tokens = match &schema.ref_path {
Some(rp) => quote! { Some(#rp.to_string()) },
None => quote! { None },
};
let items_tokens = match &schema.items {
Some(items) => {
let inner = schema_ref_to_tokens(items);
quote! { Some(Box::new(#inner)) }
}
None => quote! { None },
};
let properties_tokens = match &schema.properties {
Some(props) => {
let entries: Vec<_> = props
.iter()
.map(|(k, v)| {
let v_tokens = schema_ref_to_tokens(v);
quote! { (#k.to_string(), #v_tokens) }
})
.collect();
quote! {
Some({
let mut map = std::collections::BTreeMap::new();
#(map.insert(#entries.0, #entries.1);)*
map
})
}
}
None => quote! { None },
};
let required_tokens = match &schema.required {
Some(req) => {
let req_strs: Vec<_> = req.iter().map(|s| s.as_str()).collect();
quote! { Some(vec![#(#req_strs.to_string()),*]) }
}
None => quote! { None },
};
quote! {
vespera::schema::Schema {
ref_path: #ref_path_tokens,
schema_type: #schema_type_tokens,
format: #format_tokens,
nullable: #nullable_tokens,
items: #items_tokens,
properties: #properties_tokens,
required: #required_tokens,
..vespera::schema::Schema::new(vespera::schema::SchemaType::Object)
}
}
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use vespera_core::schema::{Reference, Schema, SchemaRef, SchemaType};
use super::*;
#[test]
fn test_generate_filtered_schema_empty_properties() {
let struct_item: syn::ItemStruct = syn::parse_str("pub struct Empty {}").unwrap();
let omit_set = HashSet::new();
let pick_set = HashSet::new();
let result = generate_filtered_schema(&struct_item, &omit_set, &pick_set, &[]);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("properties"));
}
#[test]
fn test_generate_filtered_schema_with_default_field() {
let struct_item: syn::ItemStruct = syn::parse_str(
r#"
pub struct WithDefault {
#[serde(default)]
pub field: String,
}
"#,
)
.unwrap();
let omit_set = HashSet::new();
let pick_set = HashSet::new();
let result = generate_filtered_schema(&struct_item, &omit_set, &pick_set, &[]);
assert!(result.is_ok());
let output = result.unwrap().to_string();
assert!(output.contains("None"));
}
#[test]
fn test_generate_filtered_schema_with_skip_serializing_if() {
let struct_item: syn::ItemStruct = syn::parse_str(
r#"
pub struct WithSkip {
#[serde(skip_serializing_if = "Option::is_none")]
pub field: String,
}
"#,
)
.unwrap();
let omit_set = HashSet::new();
let pick_set = HashSet::new();
let result = generate_filtered_schema(&struct_item, &omit_set, &pick_set, &[]);
assert!(result.is_ok());
}
#[test]
fn test_generate_filtered_schema_tuple_struct() {
let struct_item: syn::ItemStruct =
syn::parse_str("pub struct Tuple(i32, String);").unwrap();
let omit_set = HashSet::new();
let pick_set = HashSet::new();
let result = generate_filtered_schema(&struct_item, &omit_set, &pick_set, &[]);
assert!(result.is_ok());
}
#[test]
fn test_schema_ref_to_tokens_ref_variant() {
let schema_ref = SchemaRef::Ref(Reference::new("#/components/schemas/User".to_string()));
let tokens = schema_ref_to_tokens(&schema_ref);
let output = tokens.to_string();
assert!(output.contains("SchemaRef :: Ref"));
assert!(output.contains("Reference :: new"));
}
#[test]
fn test_schema_ref_to_tokens_inline_variant() {
let schema = Schema::new(SchemaType::String);
let schema_ref = SchemaRef::Inline(Box::new(schema));
let tokens = schema_ref_to_tokens(&schema_ref);
let output = tokens.to_string();
assert!(output.contains("SchemaRef :: Inline"));
assert!(output.contains("Box :: new"));
}
#[test]
fn test_schema_to_tokens_string_type() {
let schema = Schema::new(SchemaType::String);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: String"));
}
#[test]
fn test_schema_to_tokens_integer_type() {
let schema = Schema::new(SchemaType::Integer);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Integer"));
}
#[test]
fn test_schema_to_tokens_number_type() {
let schema = Schema::new(SchemaType::Number);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Number"));
}
#[test]
fn test_schema_to_tokens_boolean_type() {
let schema = Schema::new(SchemaType::Boolean);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Boolean"));
}
#[test]
fn test_schema_to_tokens_array_type() {
let schema = Schema::new(SchemaType::Array);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Array"));
}
#[test]
fn test_schema_to_tokens_object_type() {
let schema = Schema::new(SchemaType::Object);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Object"));
}
#[test]
fn test_schema_to_tokens_null_type() {
let schema = Schema::new(SchemaType::Null);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("SchemaType :: Null"));
}
#[test]
fn test_schema_to_tokens_none_type() {
let schema = Schema {
schema_type: None,
..Default::default()
};
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("schema_type : None"));
}
#[test]
fn test_schema_to_tokens_with_format() {
let mut schema = Schema::new(SchemaType::String);
schema.format = Some("date-time".to_string());
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("date-time"));
}
#[test]
fn test_schema_to_tokens_with_nullable() {
let mut schema = Schema::new(SchemaType::String);
schema.nullable = Some(true);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("Some (true)"));
}
#[test]
fn test_schema_to_tokens_nullable_false() {
let mut schema = Schema::new(SchemaType::String);
schema.nullable = Some(false);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("Some (false)"));
}
#[test]
fn test_schema_to_tokens_with_ref_path() {
let mut schema = Schema::new(SchemaType::Object);
schema.ref_path = Some("#/components/schemas/User".to_string());
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("#/components/schemas/User"));
}
#[test]
fn test_schema_to_tokens_with_items() {
let mut schema = Schema::new(SchemaType::Array);
let item_schema = Schema::new(SchemaType::String);
schema.items = Some(Box::new(SchemaRef::Inline(Box::new(item_schema))));
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("items"));
assert!(output.contains("Some (Box :: new"));
}
#[test]
fn test_schema_to_tokens_with_properties() {
use std::collections::BTreeMap;
let mut schema = Schema::new(SchemaType::Object);
let mut props = BTreeMap::new();
props.insert(
"name".to_string(),
SchemaRef::Inline(Box::new(Schema::new(SchemaType::String))),
);
schema.properties = Some(props);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("properties"));
assert!(output.contains("name"));
}
#[test]
fn test_schema_to_tokens_with_required() {
let mut schema = Schema::new(SchemaType::Object);
schema.required = Some(vec!["id".to_string(), "name".to_string()]);
let tokens = schema_to_tokens(&schema);
let output = tokens.to_string();
assert!(output.contains("required"));
assert!(output.contains("id"));
assert!(output.contains("name"));
}
}