alef 0.67.1

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::codegen::naming::pascal_to_snake;
use crate::core::ir::{ApiSurface, DefaultValue, FieldDef, PrimitiveType, TypeDef, TypeRef};

/// Where the body of a `crate::serde_defaults::*` function comes from, once
/// [`serde_default_source`] has decided the function exists at all.
enum SerdeDefaultSource<'a> {
    /// `#[serde(default = "…::function")]` on a field whose type is a named (struct/enum) type.
    /// The return type depends on whether that type is mirrored into the binding crate, which is
    /// why resolving it needs the whole [`ApiSurface`].
    NamedFunctionPath { type_name: &'a str, function_name: &'a str },
    /// A default recovered from the core type's `Default` impl as a literal value.
    Literal { return_type: &'static str, body: String },
    /// `#[serde(default = "…::function")]` on a scalar field, answered by the fully-qualified
    /// core function the extractor already resolved for this field's `Default`-impl initializer.
    /// [`SerdeDefaultSource::NamedFunctionPath`] cannot serve these: it derives the call from the
    /// field's own `TypeRef::Named`, which a scalar field does not have. ~keep
    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)
}

/// The PHP binding crate's Rust type for a core primitive. One table, shared by every arm that
/// needs to name a `serde_defaults` return type, so the widths cannot drift between them. ~keep
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,
    }
}

/// A scalar field's `#[serde(default = "…")]` answered by the fully-qualified core function the
/// extractor resolved. The core function returns the *core* width (`usize`), while the binding
/// field carries the PHP-facing width (`i64`), so numeric returns need the same `as` cast the
/// generated static-method wrappers use.
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,
    }
}

/// The single decision point for "does `typ.field` get a `crate::serde_defaults::*` function?".
///
/// Both emitters go through it: [`gen_serde_defaults_module`] to write the `pub fn`, and
/// `gen_php_struct`'s field-attribute pass (via [`serde_default_fn_name`]) to write the
/// `#[serde(default = "crate::serde_defaults::…")]` that references it. They used to decide
/// separately, from two hand-mirrored type tables, and a field the reference side accepted but
/// the definition side dropped produced a generated crate that could not compile. Rendering is
/// infallible once this returns `Some`, so the two sides cannot disagree. ~keep
fn serde_default_source<'a>(typ: &TypeDef, field: &'a FieldDef) -> 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 });
    }

    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()),
    }
}

/// The `crate::serde_defaults::…` function name for a field, or `None` when no function will be
/// generated for it. The reference side must not emit an attribute when this returns `None`.
pub(super) fn serde_default_fn_name(typ: &TypeDef, field: &FieldDef) -> Option<String> {
    serde_default_source(typ, field).map(|_| default_fn_ident(&typ.name, &field.name))
}

pub(super) fn gen_serde_defaults_module(api: &ApiSurface) -> 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)?;
            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).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).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}"
        );
    }
}