use crate::codegen::naming::pascal_to_snake;
use crate::core::ir::{ApiSurface, DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};
fn default_fn_ident(type_name: &str, field_name: &str) -> String {
format!("{}_{}", pascal_to_snake(type_name), pascal_to_snake(field_name))
}
fn serde_default_path(default: Option<&str>) -> Option<&str> {
let default = default?;
let marker = "serde(default = \"";
let start = default.find(marker)? + marker.len();
let rest = &default[start..];
let end = rest.find('"')?;
let path = rest[..end].trim();
(!path.is_empty()).then_some(path)
}
fn primitive_return_type(primitive: &PrimitiveType) -> &'static str {
match primitive {
PrimitiveType::U8 => "u8",
PrimitiveType::U16 => "u16",
PrimitiveType::U32 => "u32",
PrimitiveType::U64 => "u64",
PrimitiveType::I8 => "i8",
PrimitiveType::I16 => "i16",
PrimitiveType::I32 => "i32",
PrimitiveType::I64 => "i64",
PrimitiveType::Usize => "usize",
PrimitiveType::Isize => "isize",
PrimitiveType::Bool => "bool",
PrimitiveType::F32 => "f32",
PrimitiveType::F64 => "f64",
}
}
fn typed_default_fn(default: &DefaultValue, ty: &TypeRef) -> Option<(&'static str, String)> {
match (default, ty) {
(DefaultValue::BoolLiteral(value), TypeRef::Primitive(PrimitiveType::Bool)) => {
Some(("bool", value.to_string()))
}
(DefaultValue::StringLiteral(value) | DefaultValue::EnumVariant(value), TypeRef::String) => {
Some(("String", format!("{value:?}.to_string()")))
}
(DefaultValue::IntLiteral(value), TypeRef::Primitive(primitive)) => {
if matches!(primitive, PrimitiveType::Bool | PrimitiveType::F32 | PrimitiveType::F64) {
return None;
}
Some((primitive_return_type(primitive), value.to_string()))
}
(
DefaultValue::FloatLiteral(value),
TypeRef::Primitive(primitive @ (PrimitiveType::F32 | PrimitiveType::F64)),
) => {
let rendered = format!("{value}");
let body = if rendered.contains('.') || rendered.contains('e') {
rendered
} else {
format!("{rendered}.0")
};
Some((primitive_return_type(primitive), body))
}
_ => None,
}
}
fn optional_scalar_return_type(ty: &TypeRef) -> Option<String> {
match ty {
TypeRef::Primitive(primitive) => Some(format!("Option<{}>", primitive_return_type(primitive))),
TypeRef::String => Some("Option<String>".to_string()),
_ => None,
}
}
fn serde_default_body(typ: &TypeDef, field: &FieldDef) -> Option<(String, String)> {
if !typ.has_default || field.binding_excluded {
return None;
}
serde_default_path(field.default.as_deref())?;
let default = field.typed_default.as_ref()?;
if !field.optional
&& let Some((return_type, body)) = typed_default_fn(default, &field.ty)
{
return Some((return_type.to_string(), body));
}
if field.optional
&& let DefaultValue::PublicFunctionCall(resolved) = default
&& let Some(return_type) = optional_scalar_return_type(&field.ty)
{
return Some((return_type, format!("{resolved}()")));
}
None
}
pub(super) fn serde_default_fn_name(typ: &TypeDef, field: &FieldDef) -> Option<String> {
serde_default_body(typ, field).map(|_| default_fn_ident(&typ.name, &field.name))
}
pub(super) fn gen_serde_defaults_module(api: &ApiSurface) -> Option<String> {
let mut body = String::new();
for typ in &api.types {
for field in &typ.fields {
let Some((return_type, expr)) = serde_default_body(typ, field) else {
continue;
};
body.push_str(&format!(
" pub fn {}() -> {return_type} {{ {expr} }}\n",
default_fn_ident(&typ.name, &field.name)
));
}
}
if body.is_empty() {
return None;
}
Some(format!("mod serde_defaults {{\n{body}}}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{ApiSurface, TypeDef};
fn use_cache_field() -> FieldDef {
FieldDef {
name: "use_cache".to_string(),
ty: TypeRef::Primitive(PrimitiveType::Bool),
optional: false,
default: Some("serde(default = \"default_true\")".to_string()),
typed_default: Some(DefaultValue::BoolLiteral(true)),
..Default::default()
}
}
fn extraction_timeout_field() -> FieldDef {
FieldDef {
name: "extraction_timeout_secs".to_string(),
ty: TypeRef::Primitive(PrimitiveType::U64),
optional: true,
default: Some("serde(default = \"ExtractionConfig::default_extraction_timeout\")".to_string()),
typed_default: Some(DefaultValue::PublicFunctionCall(
"xberg::core::config::ExtractionConfig::default_extraction_timeout".to_string(),
)),
..Default::default()
}
}
fn config_with_field(field: FieldDef) -> TypeDef {
TypeDef {
name: "ExtractionConfig".to_string(),
has_default: true,
fields: vec![field],
..Default::default()
}
}
#[test]
fn synthesizes_named_function_for_unresolvable_private_default() {
let typ = config_with_field(use_cache_field());
assert_eq!(
serde_default_fn_name(&typ, &typ.fields[0]),
Some("extraction_config_use_cache".to_string())
);
}
#[test]
fn module_defines_the_function_the_reference_side_names() {
let typ = config_with_field(use_cache_field());
let api = ApiSurface {
types: vec![typ],
..Default::default()
};
let module = gen_serde_defaults_module(&api).expect("module generated");
assert!(
module.contains("pub fn extraction_config_use_cache() -> bool { true }"),
"expected synthesized bool-literal default, got:\n{module}"
);
}
#[test]
fn bare_serde_default_sentinel_is_left_to_the_shared_fallback() {
let mut field = use_cache_field();
field.default = Some("/* serde(default) */".to_string());
let typ = config_with_field(field);
assert_eq!(serde_default_fn_name(&typ, &typ.fields[0]), None);
}
#[test]
fn field_without_a_default_gets_no_function() {
let mut field = use_cache_field();
field.default = None;
field.typed_default = None;
let typ = config_with_field(field);
assert_eq!(serde_default_fn_name(&typ, &typ.fields[0]), None);
}
#[test]
fn optional_field_with_resolved_function_call_gets_a_wrapping_function() {
let typ = config_with_field(extraction_timeout_field());
let api = ApiSurface {
types: vec![typ.clone()],
..Default::default()
};
assert_eq!(
serde_default_fn_name(&typ, &typ.fields[0]),
Some("extraction_config_extraction_timeout_secs".to_string())
);
let module = gen_serde_defaults_module(&api).expect("module generated");
assert!(
module.contains(
"pub fn extraction_config_extraction_timeout_secs() -> Option<u64> { \
xberg::core::config::ExtractionConfig::default_extraction_timeout() }"
),
"expected the RESOLVED (fully-qualified) call target, not the raw attribute text, \
got:\n{module}"
);
}
#[test]
fn optional_field_with_unresolved_function_call_gets_no_function() {
let mut field = extraction_timeout_field();
field.typed_default = Some(DefaultValue::FunctionCall(
"ExtractionConfig::default_extraction_timeout".to_string(),
));
let typ = config_with_field(field);
assert_eq!(serde_default_fn_name(&typ, &typ.fields[0]), None);
}
#[test]
fn optional_named_type_field_gets_no_function() {
let mut field = extraction_timeout_field();
field.ty = TypeRef::Named("OcrConfig".to_string());
let typ = config_with_field(field);
assert_eq!(serde_default_fn_name(&typ, &typ.fields[0]), None);
}
}