alef 0.60.0

Opinionated polyglot binding generator for Rust libraries
Documentation
use heck::{ToLowerCamelCase, ToUpperCamelCase};

use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::TypeDef;
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::Fixture;

use super::args::{JavaArgsContext, build_args_and_setup};

pub(super) fn render_snippet_body(
    fixture: &Fixture,
    e2e_config: &E2eConfig,
    config: &ResolvedCrateConfig,
    type_defs: &[TypeDef],
) -> String {
    let mut call = e2e_config.resolve_call_for_fixture(
        fixture.call.as_deref(),
        &fixture.id,
        &fixture.resolved_category(),
        &fixture.tags,
        &fixture.input,
    );
    call = crate::e2e::codegen::select_best_matching_call(call, e2e_config, fixture);
    let recipe = crate::e2e::codegen::recipe::ResolvedE2eCallRecipe::resolve("java", fixture, call, type_defs);
    let overrides = recipe.override_config;
    let class_name = overrides
        .and_then(|value| value.class.clone())
        .unwrap_or_else(|| config.name.to_upper_camel_case());
    let function_name = overrides
        .and_then(|value| value.function.as_deref())
        .unwrap_or(&call.function)
        .to_lower_camel_case();
    let options_type = recipe
        .options_type
        .or_else(|| recipe.compatible_options_type(&["kotlin", "csharp", "c", "go", "python"]));
    let mut teardown = String::new();
    let (mut setup_lines, mut args) = build_args_and_setup(
        &fixture.input,
        recipe.args,
        JavaArgsContext {
            class_name: &class_name,
            options_type,
            fixture,
            adapter_request_type: None,
            owner_handle_is_receiver: false,
            config,
            type_defs,
            teardown_block: &mut teardown,
        },
    );
    setup_lines.splice(0..0, render_json_object_setup(fixture, recipe.args, options_type));
    if !recipe.extra_args.is_empty() {
        args = if args.is_empty() {
            recipe.extra_args.join(", ")
        } else {
            format!("{args}, {}", recipe.extra_args.join(", "))
        };
    }
    let client_factory = overrides
        .and_then(|value| value.client_factory.as_deref())
        .or_else(|| {
            e2e_config
                .call
                .overrides
                .get("java")
                .and_then(|value| value.client_factory.as_deref())
        })
        .map(ToLowerCamelCase::to_lower_camel_case);
    let package_name = overrides
        .and_then(|value| value.module.clone())
        .unwrap_or_else(|| config.java_package());
    let needs_mapper = setup_lines.iter().any(|line| line.contains("MAPPER"));

    crate::e2e::template_env::render(
        "java/snippet_body.jinja",
        minijinja::context! {
            package_name => package_name,
            class_name => class_name,
            setup_lines => setup_lines,
            client_factory => client_factory,
            function_name => function_name,
            args => args,
            result_var => call.result_var,
            returns_void => call.returns_void,
            needs_mapper => needs_mapper,
            fixture_id => fixture.id,
        },
    )
}

fn render_json_object_setup(
    fixture: &Fixture,
    args: &[crate::e2e::config::ArgMapping],
    options_type: Option<&str>,
) -> Vec<String> {
    args.iter()
        .filter_map(|arg| {
            if arg.arg_type != "json_object" {
                return None;
            }
            let value = crate::e2e::codegen::resolve_field(&fixture.input, &arg.field);
            let type_name = crate::e2e::codegen::recipe::json_object_constructor_type(arg, options_type, value)?;
            if value.is_null() || value.is_array() {
                return None;
            }
            let mut normalized = crate::e2e::codegen::transform_json_keys_for_language(value, "snake_case");
            let files = fixture.docs_files_for_arg(&arg.field);
            let file_reads = files
                .iter()
                .enumerate()
                .filter_map(|(index, file)| {
                    let marker = format!("__ALEF_DOC_FILE_{index}__");
                    let target = if file.field.is_empty() {
                        Some(&mut normalized)
                    } else {
                        normalized.pointer_mut(&file.field)
                    }?;
                    *target = serde_json::Value::String(marker.clone());
                    Some((index, marker, file.path.clone()))
                })
                .collect::<Vec<_>>();
            let json = serde_json::to_string(&normalized).unwrap_or_default();
            Some(
                crate::e2e::template_env::render(
                    "java/snippet_json_object_setup.jinja",
                    minijinja::context! {
                        variable => arg.name,
                        json => crate::e2e::escape::escape_java(&json),
                        type_name => type_name,
                        file_reads => file_reads,
                    },
                )
                .trim_end()
                .to_string(),
            )
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::e2e::config::{CallConfig, CallOverride};

    #[test]
    fn snippet_keeps_native_call_without_junit_harness() {
        let fixture = Fixture {
            id: "quick_start".into(),
            description: "Quick start".into(),
            input: serde_json::Value::Null,
            ..Fixture::default()
        };
        let mut call = CallConfig {
            function: "load_document".into(),
            result_var: "document".into(),
            ..CallConfig::default()
        };
        call.overrides.insert(
            "java".into(),
            CallOverride {
                class: Some("DocumentApi".into()),
                ..CallOverride::default()
            },
        );
        let body = render_snippet_body(
            &fixture,
            &E2eConfig {
                call,
                ..E2eConfig::default()
            },
            &ResolvedCrateConfig::default(),
            &[],
        );

        assert!(body.contains("DocumentApi.loadDocument()"));
        assert!(body.contains("public static void main(String[] args) throws Exception"));
        assert!(!body.contains("@Test"));
        assert!(!body.contains("assert"));
    }

    #[test]
    fn snippet_declares_typed_json_arguments_and_preserves_qualified_service_name() {
        let fixture = Fixture {
            id: "configured_call".into(),
            description: "Configured call".into(),
            input: serde_json::json!({"source": "example", "options": {"mode": "fast"}}),
            ..Fixture::default()
        };
        let mut call = CallConfig {
            function: "process".into(),
            args: vec![
                crate::e2e::config::ArgMapping {
                    name: "source".into(),
                    field: "source".into(),
                    arg_type: "string".into(),
                    optional: false,
                    owned: false,
                    element_type: None,
                    go_type: None,
                    vec_inner_is_ref: false,
                    trait_name: None,
                },
                crate::e2e::config::ArgMapping {
                    name: "options".into(),
                    field: "options".into(),
                    arg_type: "json_object".into(),
                    optional: false,
                    owned: false,
                    element_type: None,
                    go_type: None,
                    vec_inner_is_ref: false,
                    trait_name: None,
                },
            ],
            ..CallConfig::default()
        };
        call.overrides.insert(
            "java".into(),
            CallOverride {
                class: Some("dev.example.SampleService".into()),
                options_type: Some("ProcessOptions".into()),
                ..CallOverride::default()
            },
        );

        let body = render_snippet_body(
            &fixture,
            &E2eConfig {
                call,
                ..E2eConfig::default()
            },
            &ResolvedCrateConfig::default(),
            &[],
        );

        assert!(body.contains("var optionsJson = \"{\\\"mode\\\":\\\"fast\\\"}\";"));
        assert!(body.contains("var options = JsonUtil.fromJson(optionsJson, ProcessOptions.class);"));
        assert!(body.contains("dev.example.SampleService.process(\"example\", options)"));
        assert!(!body.contains("DevExampleSampleService"));
    }

    #[test]
    fn snippet_reads_nested_typed_dto_files() {
        let fixture: Fixture = serde_json::from_value(serde_json::json!({
            "id": "document_input",
            "description": "Read a document",
            "input": {"request": {"content": "ignored"}},
            "assertions": [],
            "docs": {
                "topic": "documents",
                "presentation": {"files": [{"field": "/request/content", "path": "document.pdf"}]}
            }
        }))
        .expect("fixture");
        let mut call = CallConfig {
            function: "process".into(),
            args: vec![crate::e2e::config::ArgMapping {
                name: "request".into(),
                field: "request".into(),
                arg_type: "json_object".into(),
                optional: false,
                owned: false,
                element_type: None,
                go_type: None,
                vec_inner_is_ref: false,
                trait_name: None,
            }],
            ..CallConfig::default()
        };
        call.overrides.insert(
            "java".into(),
            CallOverride {
                options_type: Some("DocumentRequest".into()),
                ..CallOverride::default()
            },
        );

        let body = render_snippet_body(
            &fixture.docs_call_fixture(),
            &E2eConfig {
                call,
                ..E2eConfig::default()
            },
            &ResolvedCrateConfig::default(),
            &[],
        );

        assert!(
            body.contains("Files.readAllBytes(java.nio.file.Path.of(\"document.pdf\"))"),
            "{body}"
        );
        assert!(body.contains("Base64.getEncoder().encodeToString"), "{body}");
        assert!(body.contains("DocumentRequest.class"), "{body}");
    }
}