alef 0.19.6

Opinionated polyglot binding generator for Rust libraries
Documentation
use crate::backends::magnus::type_map::rbs_type;
use crate::codegen::shared::binding_fields;
use crate::core::hash::{self, CommentStyle};
use crate::core::ir::{ApiSurface, EnumDef, FunctionDef, MethodDef, TypeDef};

pub fn gen_stubs(
    api: &ApiSurface,
    gem_name: &str,
    emit_docstrings: bool,
    streaming_method_names: &ahash::AHashSet<String>,
) -> String {
    let header = hash::header(CommentStyle::Hash);
    let mut lines: Vec<String> = header.lines().map(str::to_string).collect();
    lines.push("".to_string());

    let module_name = get_module_name(gem_name);
    lines.push(format!("module {}", module_name));
    lines.push("".to_string());
    lines.push("  VERSION: String".to_string());
    lines.push("".to_string());
    // Type alias for JSON values: any JSON-compatible type
    lines.push(
        "  type json_value = Hash[String, untyped] | Array[untyped] | String | Integer | Float | bool | nil"
            .to_string(),
    );
    lines.push("".to_string());

    // Generate type stubs
    for typ in api.types.iter().filter(|typ| !typ.is_trait) {
        if typ.is_opaque {
            lines.push(gen_opaque_type_stub(typ, emit_docstrings, streaming_method_names));
            lines.push("".to_string());
        } else {
            lines.push(gen_type_stub(typ, emit_docstrings, streaming_method_names));
            lines.push("".to_string());
        }
    }

    // Generate enum stubs
    for enum_def in &api.enums {
        lines.push(gen_enum_stub(enum_def, emit_docstrings));
        lines.push("".to_string());
    }

    // Generate function stubs (module methods)
    for func in &api.functions {
        lines.push(gen_function_stub(func, streaming_method_names));
        lines.push("".to_string());
    }

    // Generate error info class stubs for errors with introspection methods.
    for error in api.errors.iter().filter(|e| !e.methods.is_empty()) {
        let class_name = format!("{}Info", error.name);
        let mut class_lines = vec![format!("  class {class_name}")];
        for method in &error.methods {
            let (rbs_name, rbs_ret): (&str, &str) = match method.name.as_str() {
                "status_code" => ("status_code", "Integer"),
                "is_transient" => ("transient?", "bool"),
                "error_type" => ("error_type", "String"),
                _ => continue,
            };
            class_lines.push(format!("    def {rbs_name}: () -> {rbs_ret}"));
        }
        class_lines.push("  end".to_string());
        lines.push(class_lines.join("\n"));
        lines.push("".to_string());
    }

    lines.push("end".to_string());

    lines.join("\n")
}

/// Convert crate name to PascalCase module name. Handles both kebab- and
/// snake_case (matches `gen_bindings::get_module_name`).
fn get_module_name(crate_name: &str) -> String {
    use heck::ToUpperCamelCase;
    crate_name.to_upper_camel_case()
}

/// Generate a Ruby type stub for an opaque type (no fields, only methods).
fn gen_opaque_type_stub(
    typ: &TypeDef,
    emit_docstrings: bool,
    streaming_method_names: &ahash::AHashSet<String>,
) -> String {
    let mut lines = vec![];

    lines.push(format!("  class {}", typ.name));

    if emit_docstrings && !typ.doc.is_empty() {
        let doc_lines: Vec<String> = typ.doc.lines().map(ToString::to_string).collect();
        lines.push(crate::backends::magnus::template_env::render(
            "rbs_doc_block.jinja",
            minijinja::context! { doc_lines },
        ));
        lines.push("".to_string());
    }

    // Instance methods
    for method in &typ.methods {
        if !method.is_static {
            lines.push(gen_method_stub(method, false, emit_docstrings, streaming_method_names));
        }
    }

    // Static methods
    for method in &typ.methods {
        if method.is_static {
            lines.push(gen_method_stub(method, true, emit_docstrings, streaming_method_names));
        }
    }

    lines.push("  end".to_string());

    lines.join("\n")
}

/// Generate a Ruby type stub for a struct.
fn gen_type_stub(typ: &TypeDef, emit_docstrings: bool, streaming_method_names: &ahash::AHashSet<String>) -> String {
    let mut lines = vec![];

    lines.push(format!("  class {}", typ.name));

    // Add docstring if present
    if emit_docstrings && !typ.doc.is_empty() {
        let doc_lines: Vec<String> = typ.doc.lines().map(ToString::to_string).collect();
        lines.push(crate::backends::magnus::template_env::render(
            "rbs_doc_block.jinja",
            minijinja::context! { doc_lines },
        ));
        lines.push("".to_string());
    }

    // Add field attr declarations — use attr_accessor for config types (has_default),
    // attr_reader for immutable result types.
    // For config types, all fields are optional (builder pattern).
    let accessor = if typ.has_default {
        "attr_accessor"
    } else {
        "attr_reader"
    };
    for f in binding_fields(&typ.fields) {
        let mut field_type = rbs_type(&f.ty);
        // Builder types have optional fields (attr_accessor allows setting/getting nil)
        if typ.has_default && !field_type.ends_with('?') {
            field_type.push('?');
        }
        // Field-level doc comment from the Rust source. Gated behind emit_docstrings.
        if emit_docstrings && !f.doc.is_empty() {
            for line in f.doc.lines() {
                let line = line.trim();
                if line.is_empty() {
                    lines.push("    #".to_string());
                } else {
                    lines.push(format!("    # {line}"));
                }
            }
        }
        lines.push(format!(r#"    {accessor} {}: {field_type}"#, f.name));
    }

    if binding_fields(&typ.fields).next().is_some() {
        lines.push("".to_string());
    }

    // Add initialize method
    // For has_default types (config/builder), all fields are optional kwargs.
    // For result types, required fields are required kwargs, optional fields are optional.
    let init_params: Vec<String> = typ
        .fields
        .iter()
        .filter(|f| !f.binding_excluded)
        .map(|f| {
            let field_type = rbs_type(&f.ty);
            if typ.has_default {
                // Config types: all fields are optional kwargs in Ruby (defaults applied in Rust)
                format!("?{}: {}", f.name, field_type)
            } else if f.optional {
                // Result types: optional fields are optional kwargs
                format!("?{}: {}", f.name, field_type)
            } else {
                // Result types: required fields are required kwargs
                format!("{}: {}", f.name, field_type)
            }
        })
        .collect();

    lines.push(format!("    def initialize: ({}) -> void", init_params.join(", ")));

    // Add instance methods
    for method in &typ.methods {
        if !method.is_static {
            lines.push(gen_method_stub(method, false, emit_docstrings, streaming_method_names));
        }
    }

    // Add static methods
    for method in &typ.methods {
        if method.is_static {
            lines.push(gen_method_stub(method, true, emit_docstrings, streaming_method_names));
        }
    }

    lines.push("  end".to_string());

    lines.join("\n")
}

/// Generate a method stub using RBS declaration syntax.
/// Streaming methods return Enumerator[ItemType] instead of String.
fn gen_method_stub(
    method: &MethodDef,
    is_static: bool,
    emit_docstrings: bool,
    streaming_method_names: &ahash::AHashSet<String>,
) -> String {
    let params: Vec<String> = method
        .params
        .iter()
        .map(|p| {
            let param_type = rbs_type(&p.ty);
            if p.optional {
                format!("?{} {}", param_type, p.name)
            } else {
                format!("{} {}", param_type, p.name)
            }
        })
        .collect();

    let return_type = if streaming_method_names.contains(&method.name) {
        // For streaming methods like crawl_stream, derive the iterator type name
        // from the method name (e.g., crawl_stream → CrawlStreamIterator)
        let pascal_name = method
            .name
            .split('_')
            .map(|part| {
                let mut chars = part.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                }
            })
            .collect::<String>();
        format!("Enumerator[{}Iterator]", pascal_name)
    } else {
        rbs_type(&method.return_type)
    };

    let param_list = format!("({})", params.join(", "));

    let sig_line = if is_static {
        format!("    def self.{}: {} -> {}", method.name, param_list, return_type)
    } else {
        format!("    def {}: {} -> {}", method.name, param_list, return_type)
    };

    // Prefix with the method's Rust doc comment, line by line. RBS allows free-form
    // comments preceding method declarations. Gated behind emit_docstrings.
    if !emit_docstrings || method.doc.is_empty() {
        return sig_line;
    }
    let mut out = String::new();
    for line in method.doc.lines() {
        let line = line.trim();
        if line.is_empty() {
            out.push_str("    #\n");
        } else {
            out.push_str(&format!("    # {line}\n"));
        }
    }
    out.push_str(&sig_line);
    out
}

/// Generate a Ruby enum stub.
/// Unit-variant enums are represented as Ruby Symbols (e.g., :left_to_right).
/// RBS stubs are minimal — actual return types use symbol unions in method signatures.
fn gen_enum_stub(enum_def: &EnumDef, emit_docstrings: bool) -> String {
    let mut lines = vec![];

    // Always emit class stub (even for unit enums, for Ruby introspection)
    lines.push(format!("  class {}", enum_def.name));

    // Add docstring if present — gated behind emit_docstrings.
    if emit_docstrings && !enum_def.doc.is_empty() {
        let doc_lines: Vec<String> = enum_def.doc.lines().map(ToString::to_string).collect();
        lines.push(crate::backends::magnus::template_env::render(
            "rbs_doc_block.jinja",
            minijinja::context! { doc_lines },
        ));
    }

    // Check if enum has data (non-unit variants)
    let has_data = enum_def.variants.iter().any(|v| !v.fields.is_empty());

    if !has_data {
        // Unit enum: also emit as type alias with symbol union inside the class
        let symbol_variants: Vec<String> = enum_def
            .variants
            .iter()
            .map(|v| format!(":{}", to_snake_case(&v.name)))
            .collect();
        lines.push(format!("    type value = {}", symbol_variants.join(" | ")));
    }

    lines.push("  end".to_string());

    lines.join("\n")
}

fn to_snake_case(s: &str) -> String {
    let mut result = String::new();
    for (i, ch) in s.chars().enumerate() {
        if i > 0 && ch.is_uppercase() {
            result.push('_');
        }
        result.push(ch.to_ascii_lowercase());
    }
    result
}

/// Generate a function stub (module method) using RBS declaration syntax.
fn gen_function_stub(func: &FunctionDef, streaming_method_names: &ahash::AHashSet<String>) -> String {
    let params: Vec<String> = func
        .params
        .iter()
        .map(|p| {
            let param_type = rbs_type(&p.ty);
            if p.optional {
                format!("?{} {}", param_type, p.name)
            } else {
                format!("{} {}", param_type, p.name)
            }
        })
        .collect();

    let return_type = if streaming_method_names.contains(&func.name) {
        // For streaming methods like batch_crawl_stream
        let pascal_name = func
            .name
            .split('_')
            .map(|part| {
                let mut chars = part.chars();
                match chars.next() {
                    None => String::new(),
                    Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
                }
            })
            .collect::<String>();
        format!("Enumerator[{}Iterator]", pascal_name)
    } else {
        rbs_type(&func.return_type)
    };

    let param_list = format!("({})", params.join(", "));

    format!("  def self.{}: {} -> {}", func.name, param_list, return_type)
}