use std::collections::{BTreeMap, BTreeSet, HashMap};
use crate::{
parser::{
extract_default, extract_field_rename, extract_rename_all, rename_field,
strip_raw_prefix_owned,
},
schema_macro::type_utils::get_type_default as utils_get_type_default,
};
pub(super) fn set_property_default(
properties: &mut BTreeMap<String, vespera_core::schema::SchemaRef>,
field_name: &str,
value: serde_json::Value,
) {
use vespera_core::schema::{SchemaRef, SchemaType};
if let Some(SchemaRef::Inline(prop_schema)) = properties.get_mut(field_name)
&& prop_schema.default.is_none()
{
let value = if prop_schema.schema_type == Some(SchemaType::String) {
match value {
serde_json::Value::Number(n) => serde_json::Value::String(n.to_string()),
serde_json::Value::Bool(b) => serde_json::Value::String(b.to_string()),
other => other,
}
} else {
value
};
prop_schema.default = Some(value);
}
}
pub(super) fn process_default_functions(
struct_item: &syn::ItemStruct,
file_ast: Option<&syn::File>,
schema: &mut vespera_core::schema::Schema,
stored_defaults: &BTreeMap<String, serde_json::Value>,
) {
use syn::Fields;
if stored_defaults.is_empty() && !any_field_carries_default_relevant_attr(struct_item) {
return;
}
let struct_rename_all = extract_rename_all(&struct_item.attrs);
let Some(target) = field_bearing_schema_mut(schema) else {
return;
};
let mut serde_default_fields: BTreeSet<String> = BTreeSet::new();
let function_index = file_ast.map(function_index_by_name);
if let Some(properties) = target.properties.as_mut()
&& let Fields::Named(fields_named) = &struct_item.fields
{
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, struct_rename_all.as_deref()));
if let Some(value) = stored_defaults.get(&rust_field_name) {
set_property_default(properties, &field_name, value.clone());
continue;
}
if let Some(default_str) = extract_schema_default_attr(&field.attrs) {
let is_string_field = matches!(
properties.get(&field_name),
Some(vespera_core::schema::SchemaRef::Inline(s))
if s.schema_type == Some(vespera_core::schema::SchemaType::String)
);
let value = if is_string_field {
serde_json::Value::String(default_str)
} else {
parse_default_string_to_json_value(&default_str)
};
set_property_default(properties, &field_name, value);
continue;
}
let default_info = match extract_default(&field.attrs) {
Some(Some(func_name)) => {
serde_default_fields.insert(field_name.clone());
func_name
}
Some(None) => {
serde_default_fields.insert(field_name.clone());
if let Some(default_value) = utils_get_type_default(&field.ty) {
set_property_default(properties, &field_name, default_value);
}
continue;
}
None => continue, };
let resolved = function_index
.as_ref()
.and_then(|index| find_function_in_index(index, &default_info))
.and_then(extract_default_value_from_function);
if let Some(default_value) = resolved {
set_property_default(properties, &field_name, default_value);
}
}
}
if !serde_default_fields.is_empty() {
if let Some(required) = target.required.as_mut() {
required.retain(|name| !serde_default_fields.contains(name));
}
if target.required.as_ref().is_some_and(Vec::is_empty) {
target.required = None;
}
}
}
fn any_field_carries_default_relevant_attr(struct_item: &syn::ItemStruct) -> bool {
let syn::Fields::Named(fields) = &struct_item.fields else {
return false;
};
fields.named.iter().any(|field| {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("serde") || attr.path().is_ident("schema"))
})
}
fn field_bearing_schema_mut(
schema: &mut vespera_core::schema::Schema,
) -> Option<&mut vespera_core::schema::Schema> {
if schema.properties.is_some() {
return Some(schema);
}
schema
.all_of
.as_mut()?
.iter_mut()
.find_map(|member| match member {
vespera_core::schema::SchemaRef::Inline(inline) => Some(inline.as_mut()),
vespera_core::schema::SchemaRef::Ref(_) => None,
})
}
pub(super) fn extract_schema_default_attr(attrs: &[syn::Attribute]) -> Option<String> {
attrs
.iter()
.filter(|attr| attr.path().is_ident("schema"))
.find_map(|attr| {
let mut default_value = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
default_value = Some(lit.value());
}
Ok(())
});
default_value
})
}
pub(super) fn parse_default_string_to_json_value(value: &str) -> serde_json::Value {
if let Ok(n) = value.parse::<i64>() {
return serde_json::Value::Number(n.into());
}
if let Ok(f) = value.parse::<f64>()
&& let Some(n) = serde_json::Number::from_f64(f)
{
return serde_json::Value::Number(n);
}
if let Ok(b) = value.parse::<bool>() {
return serde_json::Value::Bool(b);
}
serde_json::Value::String(value.to_string())
}
#[cfg(test)]
pub fn find_function_in_file<'a>(
file_ast: &'a syn::File,
function_name: &str,
) -> Option<&'a syn::ItemFn> {
let index = function_index_by_name(file_ast);
find_function_in_index(&index, function_name)
}
fn function_index_by_name(file_ast: &syn::File) -> HashMap<String, &syn::ItemFn> {
let mut functions = HashMap::new();
for item in &file_ast.items {
if let syn::Item::Fn(fn_item) = item {
functions
.entry(fn_item.sig.ident.to_string())
.or_insert(fn_item);
}
}
functions
}
fn find_function_in_index<'a>(
function_index: &HashMap<String, &'a syn::ItemFn>,
function_name: &str,
) -> Option<&'a syn::ItemFn> {
let local_name = function_name.rsplit("::").next().unwrap_or(function_name);
function_index.get(local_name).copied()
}
pub fn extract_default_value_from_function(func: &syn::ItemFn) -> Option<serde_json::Value> {
for stmt in &func.block.stmts {
if let syn::Stmt::Expr(expr, _) = stmt {
if let Some(value) = extract_value_from_expr(expr) {
return Some(value);
}
if let syn::Expr::Return(ret) = expr
&& let Some(expr) = &ret.expr
&& let Some(value) = extract_value_from_expr(expr)
{
return Some(value);
}
}
}
None
}
pub(super) fn extract_value_from_expr(expr: &syn::Expr) -> Option<serde_json::Value> {
use syn::{Expr, ExprLit, ExprMacro, Lit};
match expr {
Expr::Lit(ExprLit { lit, .. }) => match lit {
Lit::Str(s) => Some(serde_json::Value::String(s.value())),
Lit::Int(i) => i
.base10_parse::<i64>()
.ok()
.map(|v| serde_json::Value::Number(v.into())),
Lit::Float(f) => f
.base10_parse::<f64>()
.ok()
.and_then(serde_json::Number::from_f64)
.map(serde_json::Value::Number),
Lit::Bool(b) => Some(serde_json::Value::Bool(b.value)),
_ => None,
},
Expr::MethodCall(method_call) => {
if method_call.method == "to_string" {
if let Expr::Lit(ExprLit {
lit: Lit::Str(s), ..
}) = method_call.receiver.as_ref()
{
return Some(serde_json::Value::String(s.value()));
}
if let Some(value) = extract_value_from_expr(method_call.receiver.as_ref()) {
return Some(value);
}
}
None
}
Expr::Call(call) => {
let Expr::Path(path) = call.func.as_ref() else {
return None;
};
let mut segments = path.path.segments.iter().rev();
let last = segments.next()?;
let prev = segments.next()?;
if last.ident == "from"
&& prev.ident == "String"
&& call.args.len() == 1
&& let Some(first_arg) = call.args.first()
{
return extract_value_from_expr(first_arg).and_then(|value| match value {
serde_json::Value::String(_) => Some(value),
_ => None,
});
}
None
}
Expr::Macro(ExprMacro { mac, .. }) => {
if mac.path.is_ident("vec") {
if mac.tokens.is_empty() {
return Some(serde_json::Value::Array(Vec::new()));
}
return mac
.parse_body_with(
syn::punctuated::Punctuated::<Expr, syn::Token![,]>::parse_terminated,
)
.ok()
.and_then(|elems| {
elems
.iter()
.map(extract_value_from_expr)
.collect::<Option<Vec<_>>>()
})
.map(serde_json::Value::Array);
}
None
}
_ => None,
}
}
#[cfg(test)]
mod tests;