alef 0.19.10

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::core::ir::{DefaultValue, EnumDef, FunctionDef, PrimitiveType, TypeDef, TypeRef};
use heck::ToLowerCamelCase;
use std::collections::BTreeSet;

use crate::backends::dart::ident::dart_safe_ident;
use crate::backends::dart::template_env;

use super::render_type::{format_param, render_type};

/// Returns `true` if the parameter is a config type that should be made optional in Dart.
///
/// Parameters named `config` whose named type has a Rust `Default` implementation are
/// made optional in the Dart wrapper so callers can omit the config and get the Rust
/// default represented as a Dart constructor expression.
fn is_optional_config_param(p: &crate::core::ir::ParamDef, type_defs: &[TypeDef]) -> bool {
    let TypeRef::Named(name) = &p.ty else {
        return false;
    };
    p.name == "config" && type_defs.iter().any(|ty| ty.name == *name && ty.has_default)
}

pub(super) fn emit_function(
    f: &FunctionDef,
    type_defs: &[TypeDef],
    enums: &[EnumDef],
    out: &mut String,
    imports: &mut BTreeSet<String>,
) {
    if !f.doc.is_empty() {
        let doc_lines: Vec<String> = f.doc.lines().map(ToString::to_string).collect();
        out.push_str(&template_env::render(
            "doc_comment.jinja",
            minijinja::context! {
                indent => "  ",
                lines => doc_lines,
            },
        ));
    }
    if let Some(ref error_ty) = f.error_type {
        out.push_str(&template_env::render(
            "function_throws_annotation.jinja",
            minijinja::context! {
                error_ty => error_ty.as_str(),
            },
        ));
    }

    let fn_name = dart_safe_ident(&f.name.to_lower_camel_case());

    // Find the optional config param if present, and determine its type.
    let config_param = f.params.iter().find(|p| is_optional_config_param(p, type_defs));
    let config_type = config_param.and_then(|p| match &p.ty {
        TypeRef::Named(n) => Some(n.as_str()),
        _ => None,
    });

    // Build the dart wrapper parameter list. If the function has an optional config param
    // (e.g., ExtractionConfig or PackConfig), split into required params and then
    // `[ConfigType? config]` optional positional.
    //
    // For all other functions, emit required (non-optional) params as positional and
    // optional params inside a `{...}` named-parameter block. This matches the natural
    // Dart calling convention `createClient('key', baseUrl: ...)` and mirrors the
    // underlying FRB binding which is itself named-only.
    let params_str = if let Some(cfg_type) = config_type {
        let required_params: Vec<String> = f
            .params
            .iter()
            .filter(|p| !is_optional_config_param(p, type_defs))
            .map(|p| format_param(p, imports))
            .collect();
        let optional_sig = format!("[{cfg_type}? config]");
        if required_params.is_empty() {
            optional_sig
        } else {
            format!("{}, {optional_sig}", required_params.join(", "))
        }
    } else {
        let required: Vec<String> = f
            .params
            .iter()
            .filter(|p| !p.optional)
            .map(|p| format_param(p, imports))
            .collect();
        let optional: Vec<String> = f
            .params
            .iter()
            .filter(|p| p.optional)
            .map(|p| format_param(p, imports))
            .collect();
        match (required.is_empty(), optional.is_empty()) {
            (true, true) => String::new(),
            (false, true) => required.join(", "),
            (true, false) => format!("{{{}}}", optional.join(", ")),
            (false, false) => format!("{}, {{{}}}", required.join(", "), optional.join(", ")),
        }
    };

    // FRB bridge functions use Dart named parameters (required keyword).
    // Call them with `name: value` named-argument syntax.
    // When config is optional, pass the default when the caller omits it.
    let call_args_str = if let Some(cfg_type) = config_type {
        let non_config: Vec<String> = f
            .params
            .iter()
            .filter(|p| !is_optional_config_param(p, type_defs))
            .map(|p| {
                let ident = dart_safe_ident(&p.name.to_lower_camel_case());
                format!("{ident}: {ident}")
            })
            .collect();
        let default_expr =
            default_expression_for_named_type(cfg_type, type_defs, enums).unwrap_or_else(|| format!("{cfg_type}()"));
        let config_default = format!("config ?? {default_expr}");
        let config_arg = format!("config: {config_default}");
        if non_config.is_empty() {
            config_arg
        } else {
            format!("{}, {config_arg}", non_config.join(", "))
        }
    } else {
        f.params
            .iter()
            .map(|p| {
                let ident = dart_safe_ident(&p.name.to_lower_camel_case());
                format!("{ident}: {ident}")
            })
            .collect::<Vec<_>>()
            .join(", ")
    };

    // FRB v2 wraps ALL Rust functions as `Future<T>` in Dart, including sync ones.
    // Therefore all wrapper methods must be `async` and `await` the bridge call.
    {
        let return_ty = if matches!(f.return_type, TypeRef::Unit) {
            "Future<void>".to_string()
        } else {
            format!("Future<{}>", render_type(&f.return_type, imports))
        };
        out.push_str(&template_env::render(
            "function_signature_async.jinja",
            minijinja::context! {
                return_ty => return_ty,
                fn_name => fn_name.as_str(),
                params => params_str.as_str(),
            },
        ));
        out.push_str(&template_env::render(
            "function_await_return.jinja",
            minijinja::context! {
                fn_name => fn_name.as_str(),
                call_args_str => call_args_str.as_str(),
            },
        ));
        out.push_str("  }\n");
    }
}

fn default_expression_for_named_type(name: &str, type_defs: &[TypeDef], enums: &[EnumDef]) -> Option<String> {
    let ty = type_defs.iter().find(|ty| ty.name == name && ty.has_default)?;
    let fields: Vec<String> = ty
        .fields
        .iter()
        .filter(|field| !field.binding_excluded)
        .map(|field| {
            let field_name = dart_safe_ident(&field.name.to_lower_camel_case());
            let value = default_expression_for_field(field, type_defs, enums)?;
            Some(format!("{field_name}: {value}"))
        })
        .collect::<Option<Vec<_>>>()?;

    if fields.is_empty() {
        Some(format!("{name}()"))
    } else {
        Some(format!("{name}({})", fields.join(", ")))
    }
}

fn default_expression_for_field(
    field: &crate::core::ir::FieldDef,
    type_defs: &[TypeDef],
    enums: &[EnumDef],
) -> Option<String> {
    if let Some(default) = &field.typed_default {
        return render_default_value(&field.ty, default, type_defs, enums);
    }
    zero_value_for_type(&field.ty, type_defs, enums)
}

fn render_default_value(
    ty: &TypeRef,
    default: &DefaultValue,
    type_defs: &[TypeDef],
    enums: &[EnumDef],
) -> Option<String> {
    match default {
        DefaultValue::BoolLiteral(value) => Some(value.to_string()),
        DefaultValue::StringLiteral(value) => Some(format!("'{}'", escape_dart_string(value))),
        DefaultValue::IntLiteral(value) => Some(value.to_string()),
        DefaultValue::FloatLiteral(value) => Some(value.to_string()),
        DefaultValue::EnumVariant(variant) => render_enum_variant_default(ty, variant, enums),
        DefaultValue::Empty => zero_value_for_type(ty, type_defs, enums),
        DefaultValue::None => Some("null".to_string()),
    }
}

fn zero_value_for_type(ty: &TypeRef, type_defs: &[TypeDef], enums: &[EnumDef]) -> Option<String> {
    match ty {
        TypeRef::Primitive(PrimitiveType::Bool) => Some("false".to_string()),
        TypeRef::Primitive(PrimitiveType::F32 | PrimitiveType::F64) => Some("0.0".to_string()),
        TypeRef::Primitive(_) => Some("0".to_string()),
        TypeRef::String | TypeRef::Char | TypeRef::Path => Some("''".to_string()),
        TypeRef::Bytes | TypeRef::Vec(_) => Some("[]".to_string()),
        TypeRef::Map(_, _) | TypeRef::Json => Some("{}".to_string()),
        TypeRef::Optional(_) | TypeRef::Unit => Some("null".to_string()),
        TypeRef::Duration => Some("Duration.zero".to_string()),
        TypeRef::Named(name) => {
            if let Some(default) = default_enum_variant(name, enums) {
                render_enum_variant_default(ty, default, enums)
            } else {
                default_expression_for_named_type(name, type_defs, enums)
            }
        }
    }
}

fn render_enum_variant_default(ty: &TypeRef, variant: &str, enums: &[EnumDef]) -> Option<String> {
    let TypeRef::Named(name) = ty else {
        return None;
    };
    let variant_name = dart_safe_ident(&variant.to_lower_camel_case());
    let enum_def = enums.iter().find(|e| e.name == *name)?;
    let enum_variant = enum_def.variants.iter().find(|v| v.name == variant)?;
    if enum_variant.fields.is_empty() {
        Some(format!("{name}.{variant_name}"))
    } else {
        Some(format!("{name}.{variant_name}()"))
    }
}

fn default_enum_variant<'a>(name: &str, enums: &'a [EnumDef]) -> Option<&'a str> {
    enums
        .iter()
        .find(|e| e.name == name)
        .and_then(|e| e.variants.iter().find(|v| v.is_default))
        .map(|v| v.name.as_str())
}

fn escape_dart_string(value: &str) -> String {
    value.replace('\\', "\\\\").replace('\'', "\\'")
}