use crate::codegen::naming::{pascal_to_snake, wire_variant_value};
use crate::core::ir::{ApiSurface, DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};
use ahash::AHashSet;
enum SerdeDefaultSource<'a> {
NamedFunctionPath { type_name: &'a str, function_name: &'a str },
Literal { return_type: &'static str, body: String },
ResolvedFunctionPath { return_type: &'static str, call: String },
}
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::I8 => "i8",
PrimitiveType::I16 => "i16",
PrimitiveType::I32 => "i32",
PrimitiveType::U64 | PrimitiveType::I64 | PrimitiveType::Usize | PrimitiveType::Isize => "i64",
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 resolved_function_call(path: &str, ty: &TypeRef) -> Option<(&'static str, String)> {
match ty {
TypeRef::String => Some(("String", format!("{path}().into()"))),
TypeRef::Primitive(PrimitiveType::Bool) => Some(("bool", format!("{path}()"))),
TypeRef::Primitive(primitive) => {
let return_type = primitive_return_type(primitive);
Some((return_type, format!("{path}() as {return_type}")))
}
_ => None,
}
}
fn enum_variant_wire_value(api: &ApiSurface, enum_name: &str, variant_ref: &str) -> Option<String> {
let variant_name = variant_ref.rsplit_once("::").map_or(variant_ref, |(_, v)| v);
let enum_def = api.enums.iter().find(|e| e.name == enum_name)?;
let variant = enum_def.variants.iter().find(|v| v.name == variant_name)?;
Some(wire_variant_value(
&variant.name,
variant.serde_rename.as_deref(),
enum_def.serde_rename_all.as_deref(),
))
}
fn enum_default_wire_source(field: &FieldDef, enum_names: &AHashSet<String>, api: &ApiSurface) -> Option<String> {
if field.default.as_deref() != Some("/* serde(default) */") {
return None;
}
let TypeRef::Named(enum_name) = &field.ty else {
return None;
};
if !enum_names.contains(enum_name) {
return None;
}
let DefaultValue::EnumVariant(variant_ref) = field.typed_default.as_ref()? else {
return None;
};
enum_variant_wire_value(api, enum_name, variant_ref)
}
fn serde_default_source<'a>(
typ: &TypeDef,
field: &'a FieldDef,
enum_names: &AHashSet<String>,
api: &ApiSurface,
) -> Option<SerdeDefaultSource<'a>> {
if !typ.has_default || field.optional || field.binding_excluded {
return None;
}
let serde_path = serde_default_path(field.default.as_deref());
if let Some(path) = serde_path
&& let TypeRef::Named(type_name) = &field.ty
&& let Some((_, function_name)) = path.rsplit_once("::")
{
return Some(SerdeDefaultSource::NamedFunctionPath {
type_name,
function_name,
});
}
if let Some(default) = &field.typed_default
&& let Some((return_type, body)) = typed_default_fn(default, &field.ty)
{
return Some(SerdeDefaultSource::Literal { return_type, body });
}
if serde_path.is_some()
&& let Some(DefaultValue::PublicFunctionCall(path)) = &field.typed_default
&& let Some((return_type, call)) = resolved_function_call(path, &field.ty)
{
return Some(SerdeDefaultSource::ResolvedFunctionPath { return_type, call });
}
if let Some(wire_value) = enum_default_wire_source(field, enum_names, api) {
return Some(SerdeDefaultSource::Literal {
return_type: "String",
body: format!("{wire_value:?}.to_string()"),
});
}
None
}
fn default_fn_signature(source: &SerdeDefaultSource<'_>, field: &FieldDef, api: &ApiSurface) -> (String, String) {
match source {
SerdeDefaultSource::NamedFunctionPath {
type_name,
function_name,
} => match field.type_rust_path.as_deref() {
Some(core_path) if api.types.iter().any(|typ| typ.name == **type_name) => (
format!("crate::{type_name}"),
format!("{core_path}::{function_name}().into()"),
),
Some(core_path) => (core_path.to_string(), format!("{core_path}::{function_name}()")),
None => (
format!("crate::{type_name}"),
format!("crate::{type_name}::{function_name}()"),
),
},
SerdeDefaultSource::Literal { return_type, body } => ((*return_type).to_string(), body.clone()),
SerdeDefaultSource::ResolvedFunctionPath { return_type, call } => ((*return_type).to_string(), call.clone()),
}
}
pub(super) fn serde_default_fn_name(
typ: &TypeDef,
field: &FieldDef,
enum_names: &AHashSet<String>,
api: &ApiSurface,
) -> Option<String> {
serde_default_source(typ, field, enum_names, api).map(|_| default_fn_ident(&typ.name, &field.name))
}
pub(super) fn gen_serde_defaults_module(api: &ApiSurface, enum_names: &AHashSet<String>) -> Option<String> {
let functions: Vec<minijinja::Value> = api
.types
.iter()
.flat_map(|typ| typ.fields.iter().map(move |field| (typ, field)))
.filter_map(|(typ, field)| {
let source = serde_default_source(typ, field, enum_names, api)?;
let (return_type, body) = default_fn_signature(&source, field, api);
Some(minijinja::context! {
name => default_fn_ident(&typ.name, &field.name),
return_type,
body,
})
})
.collect();
if functions.is_empty() {
return None;
}
Some(
crate::backends::php::template_env::render("serde_defaults_module.jinja", minijinja::context! { functions })
.trim_end()
.to_string(),
)
}
#[cfg(test)]
#[path = "serde_defaults/pairing_tests.rs"]
mod pairing_tests;
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::TypeDef;
fn foreign_default_field(name: &str, type_name: &str, core_path: &str, default_fn: &str) -> FieldDef {
FieldDef {
name: name.to_string(),
ty: TypeRef::Named(type_name.to_string()),
optional: false,
default: Some(format!("serde(default = \"{default_fn}\")")),
type_rust_path: Some(core_path.to_string()),
..Default::default()
}
}
fn config_with_field(field: FieldDef) -> TypeDef {
TypeDef {
name: "FetchConfig".to_string(),
has_default: true,
fields: vec![field],
..Default::default()
}
}
#[test]
fn mirrored_core_type_default_returns_mirror_and_converts() {
let config = config_with_field(foreign_default_field(
"ssrf",
"SsrfPolicy",
"mylib::SsrfPolicy",
"mylib::SsrfPolicy::from_env",
));
let mirror = TypeDef {
name: "SsrfPolicy".to_string(),
..Default::default()
};
let api = ApiSurface {
types: vec![config, mirror],
..Default::default()
};
let module = gen_serde_defaults_module(&api, &AHashSet::new()).expect("module generated");
assert!(
module.contains("pub fn fetch_config_ssrf() -> crate::SsrfPolicy { mylib::SsrfPolicy::from_env().into() }"),
"expected mirror return type with `.into()` conversion, got:\n{module}"
);
}
#[test]
fn unmirrored_core_type_default_returns_core_type() {
let config = config_with_field(foreign_default_field(
"ssrf",
"SsrfPolicy",
"mylib::SsrfPolicy",
"mylib::SsrfPolicy::from_env",
));
let api = ApiSurface {
types: vec![config],
..Default::default()
};
let module = gen_serde_defaults_module(&api, &AHashSet::new()).expect("module generated");
assert!(
module.contains("pub fn fetch_config_ssrf() -> mylib::SsrfPolicy { mylib::SsrfPolicy::from_env() }"),
"expected core return type without conversion, got:\n{module}"
);
}
}