use std::{
collections::{BTreeMap, HashMap},
path::{Path, PathBuf},
sync::{Arc, LazyLock, Mutex},
};
use crate::metadata::StructMetadata;
use crate::parser::{extract_default, strip_raw_prefix_owned};
use crate::schema_macro::file_cache::{FileFingerprint, get_file_fingerprint};
type SchemaBucket = Arc<HashMap<String, StructMetadata>>;
type SchemaStorage = HashMap<String, SchemaBucket>;
pub static SCHEMA_STORAGE: LazyLock<Mutex<SchemaStorage>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static DEFAULT_FUNCTION_CACHE: LazyLock<Mutex<HashMap<PathBuf, DefaultFunctionCacheEntry>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[must_use]
pub fn current_crate_key() -> String {
crate::schema_macro::file_cache::get_manifest_dir().unwrap_or_default()
}
fn with_current_crate_bucket<R>(f: impl FnOnce(&mut HashMap<String, StructMetadata>) -> R) -> R {
let mut guard = SCHEMA_STORAGE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
f(Arc::make_mut(
guard
.entry(current_crate_key())
.or_insert_with(|| Arc::new(HashMap::new())),
))
}
pub fn register_schema(name: String, metadata: StructMetadata) -> Result<(), ()> {
with_current_crate_bucket(|bucket| {
if let Some(existing) = bucket.get(&name) {
if existing.definition == metadata.definition
|| (existing.source_identity.is_some()
&& existing.source_identity == metadata.source_identity)
{
bucket.insert(name, metadata);
return Ok(());
}
return Err(());
}
bucket.insert(name, metadata);
Ok(())
})
}
fn derive_source_identity(
input: &syn::DeriveInput,
call_site_file: Option<&Path>,
) -> Option<String> {
call_site_file.map(|path| format!("{}::{}", path.display(), input.ident))
}
fn build_struct_metadata(
input: &syn::DeriveInput,
schema_name: String,
call_site_file: Option<&Path>,
) -> StructMetadata {
let metadata = StructMetadata::new(schema_name, quote::quote!(#input).to_string());
match derive_source_identity(input, call_site_file) {
Some(identity) => metadata.with_source_identity(identity),
None => metadata,
}
}
pub fn insert_schema(name: String, metadata: StructMetadata) {
with_current_crate_bucket(|bucket| {
bucket.insert(name, metadata);
});
}
#[must_use]
pub fn current_crate_schemas() -> Arc<HashMap<String, StructMetadata>> {
SCHEMA_STORAGE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(¤t_crate_key())
.cloned()
.unwrap_or_else(|| Arc::new(HashMap::new()))
}
#[derive(Default)]
struct SchemaAttributeSummary {
name: Option<String>,
has_ref_override: bool,
}
fn collect_schema_attribute_summary(attrs: &[syn::Attribute]) -> SchemaAttributeSummary {
let mut summary = SchemaAttributeSummary::default();
for attr in attrs.iter().filter(|attr| attr.path().is_ident("schema")) {
let mut attr_name = None;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("name") {
let value = meta.value()?;
let lit: syn::LitStr = value.parse()?;
attr_name = Some(lit.value());
} else {
if meta.path.is_ident("ref") {
summary.has_ref_override = true;
}
if let Ok(value) = meta.value() {
let _ = value.parse::<syn::Lit>();
}
}
Ok(())
});
if summary.name.is_none() {
summary.name = attr_name;
}
if summary.name.is_some() && summary.has_ref_override {
break;
}
}
summary
}
#[derive(Clone)]
struct DefaultFunctionCacheEntry {
fingerprint: FileFingerprint,
values: Arc<BTreeMap<String, serde_json::Value>>,
}
pub fn extract_schema_name_attr(attrs: &[syn::Attribute]) -> Option<String> {
collect_schema_attribute_summary(attrs).name
}
pub fn process_derive_schema(
input: &syn::DeriveInput,
) -> (Option<StructMetadata>, proc_macro2::TokenStream) {
let name = &input.ident;
let mut field_constraints: Vec<crate::parser::schema::schema_attrs::SchemaConstraints> =
Vec::new();
if let syn::Data::Struct(data_struct) = &input.data
&& let syn::Fields::Named(fields_named) = &data_struct.fields
{
field_constraints.reserve(fields_named.named.len());
for field in &fields_named.named {
match crate::parser::schema::schema_attrs::try_extract_schema_constraints(&field.attrs)
{
Ok(constraints) => field_constraints.push(constraints),
Err(error) => return (None, error.to_compile_error()),
}
}
}
let schema_attr = collect_schema_attribute_summary(&input.attrs);
let has_explicit_schema_name = schema_attr.name.is_some();
let schema_name = schema_attr.name.unwrap_or_else(|| name.to_string());
let call_site_file = proc_macro2::Span::call_site().local_file();
let field_defaults = call_site_file
.as_deref()
.map(|file_path| extract_field_defaults_from_path(input, file_path))
.unwrap_or_default();
if let Err(error) = validate_serde_default_values(input, &field_defaults) {
return (None, error.to_compile_error());
}
let mut metadata = build_struct_metadata(input, schema_name, call_site_file.as_deref());
if has_explicit_schema_name {
metadata.explicit_schema_name = true;
}
if schema_attr.has_ref_override {
metadata.include_in_openapi = false;
}
metadata.field_defaults = field_defaults;
let garde = crate::garde_emit::emit_garde_validate(input, &field_constraints);
let supplements = crate::schema_assertions::emit_schema_supplements(input, &field_constraints);
let expanded = quote::quote! {
#garde
#supplements
};
(Some(metadata), expanded)
}
pub fn extract_field_defaults_from_path(
input: &syn::DeriveInput,
file_path: &Path,
) -> BTreeMap<String, serde_json::Value> {
let mut defaults = BTreeMap::new();
let fields = match &input.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(named) => &named.named,
_ => return defaults,
},
_ => return defaults,
};
let fn_defaults: Vec<(String, String)> = fields
.iter()
.filter_map(|f| {
let field_name = f.ident.as_ref()?.to_string();
if let Some(Some(fn_name)) = crate::parser::extract_default(&f.attrs) {
if fn_name.contains("::") {
None
} else {
Some((field_name, fn_name))
}
} else {
None
}
})
.collect();
if fn_defaults.is_empty() {
return defaults;
}
defaults.extend(extract_defaults_from_path(&fn_defaults, file_path));
defaults
}
fn validate_serde_default_values(
input: &syn::DeriveInput,
field_defaults: &BTreeMap<String, serde_json::Value>,
) -> Result<(), syn::Error> {
let fields = match &input.data {
syn::Data::Struct(data) => match &data.fields {
syn::Fields::Named(named) => &named.named,
_ => return Ok(()),
},
_ => return Ok(()),
};
let mut errors: Option<syn::Error> = None;
for field in fields {
let Some(default_kind) = extract_default(&field.attrs) else {
continue;
};
if has_schema_default(&field.attrs)
|| serde_default_is_resolvable(field, default_kind.as_ref(), field_defaults)
{
continue;
}
let field_name = field.ident.as_ref().map_or_else(
|| "unknown".to_string(),
|ident| strip_raw_prefix_owned(ident.to_string()),
);
let error = syn::Error::new_spanned(
field,
format!(
"cannot statically determine the OpenAPI default for field `{field_name}` which has `#[serde(default)]`; add an explicit `#[schema(default = \"...\")]`"
),
);
if let Some(existing) = &mut errors {
existing.combine(error);
} else {
errors = Some(error);
}
}
errors.map_or(Ok(()), Err)
}
fn serde_default_is_resolvable(
field: &syn::Field,
default_kind: Option<&String>,
field_defaults: &BTreeMap<String, serde_json::Value>,
) -> bool {
match default_kind {
Some(_) => field
.ident
.as_ref()
.is_some_and(|ident| field_defaults.contains_key(&ident.to_string())),
None => crate::schema_macro::type_utils::get_type_default(&field.ty).is_some(),
}
}
fn has_schema_default(attrs: &[syn::Attribute]) -> bool {
attrs
.iter()
.filter(|attr| attr.path().is_ident("schema"))
.any(|attr| {
let mut found = false;
let _ = attr.parse_nested_meta(|meta| {
if meta.path.is_ident("default") {
found = true;
}
Ok(())
});
found
})
}
fn extract_defaults_from_path(
fn_defaults: &[(String, String)],
file_path: &Path,
) -> BTreeMap<String, serde_json::Value> {
let Some(function_defaults) = cached_default_functions(file_path) else {
return BTreeMap::new();
};
fn_defaults
.iter()
.filter_map(|(field_name, fn_name)| {
function_defaults
.get(fn_name)
.cloned()
.map(|value| (field_name.clone(), value))
})
.collect()
}
fn cached_default_functions(file_path: &Path) -> Option<Arc<BTreeMap<String, serde_json::Value>>> {
let fingerprint = get_file_fingerprint(file_path)?;
if let Some(values) = DEFAULT_FUNCTION_CACHE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(file_path)
.and_then(|entry| (entry.fingerprint == fingerprint).then(|| Arc::clone(&entry.values)))
{
return Some(values);
}
let file_ast = crate::schema_macro::file_cache::get_parsed_file(file_path)?;
let values = Arc::new(extract_default_functions_from_file(&file_ast));
DEFAULT_FUNCTION_CACHE
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(
file_path.to_path_buf(),
DefaultFunctionCacheEntry {
fingerprint,
values: Arc::clone(&values),
},
);
Some(values)
}
fn extract_default_functions_from_file(
file_ast: &syn::File,
) -> BTreeMap<String, serde_json::Value> {
file_ast
.items
.iter()
.filter_map(|item| {
let syn::Item::Fn(func) = item else {
return None;
};
crate::openapi_generator::extract_default_value_from_function(func)
.map(|value| (func.sig.ident.to_string(), value))
})
.collect()
}
#[cfg(test)]
pub fn extract_defaults_from_file(
fn_defaults: &[(String, String)],
file_ast: &syn::File,
) -> BTreeMap<String, serde_json::Value> {
let mut defaults = BTreeMap::new();
for (field_name, fn_name) in fn_defaults {
if let Some(func) = crate::openapi_generator::find_function_in_file(file_ast, fn_name)
&& let Some(value) = crate::openapi_generator::extract_default_value_from_function(func)
{
defaults.insert(field_name.clone(), value);
}
}
defaults
}
#[cfg(test)]
#[path = "schema_impl/tests.rs"]
mod tests;