alef 0.23.35

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Dart e2e test generator using package:test and package:http.
//!
//! Generates `e2e/dart/test/<category>_test.dart` files from JSON fixtures.
//! HTTP fixtures hit the mock server at `MOCK_SERVER_URL/fixtures/<id>`.
//! Non-HTTP fixtures without a dart-specific call override emit a skip stub.

use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
use std::path::PathBuf;

use super::E2eCodegen;

/// Dart e2e code generator.
pub struct DartE2eCodegen;

impl E2eCodegen for DartE2eCodegen {
    fn generate(
        &self,
        groups: &[FixtureGroup],
        e2e_config: &E2eConfig,
        config: &ResolvedCrateConfig,
        type_defs: &[crate::core::ir::TypeDef],
        enums: &[crate::core::ir::EnumDef],
    ) -> Result<Vec<GeneratedFile>> {
        let lang = self.language_name();
        let output_base = PathBuf::from(e2e_config.effective_output()).join(lang);

        let mut files = Vec::new();

        // Resolve package config.
        let dart_pkg = e2e_config.resolve_package("dart");
        let pkg_name = dart_pkg
            .as_ref()
            .and_then(|p| p.name.as_ref())
            .cloned()
            .unwrap_or_else(|| config.dart_pubspec_name());
        let pkg_path = dart_pkg
            .as_ref()
            .and_then(|p| p.path.as_ref())
            .cloned()
            .unwrap_or_else(|| "../../packages/dart".to_string());
        let pkg_version = dart_pkg
            .as_ref()
            .and_then(|p| p.version.as_ref())
            .cloned()
            .or_else(|| config.resolved_version())
            .unwrap_or_else(|| "0.1.0".to_string());

        // Generate pubspec.yaml with http dependency for HTTP client tests.
        files.push(GeneratedFile {
            path: output_base.join("pubspec.yaml"),
            content: project::render_pubspec(&pkg_name, &pkg_path, &pkg_version, e2e_config.dep_mode),
            generated_header: false,
        });

        // Generate dart_test.yaml to limit parallelism — the SUT uses keep-alive
        // connections and gets overwhelmed when test files run in parallel.
        files.push(GeneratedFile {
            path: output_base.join("dart_test.yaml"),
            content: concat!(
                "# Generated by alef — DO NOT EDIT.\n",
                "# Run test files sequentially to avoid overwhelming the SUT with\n",
                "# concurrent keep-alive connections.\n",
                "concurrency: 1\n",
            )
            .to_string(),
            generated_header: false,
        });

        // Check if any fixture is an HTTP test (needs app harness).
        let has_http_fixtures = groups.iter().any(|g| g.fixtures.iter().any(|f| f.is_http_test()));

        // Generate app_harness.dart when using server-pattern (HTTP fixtures).
        if has_http_fixtures {
            files.push(GeneratedFile {
                path: output_base.join("app_harness.dart"),
                content: project::render_app_harness(groups, e2e_config, &pkg_name),
                generated_header: true,
            });
        }

        let test_base = output_base.join("test");

        // One test file per fixture group.
        let bridge_class = config.dart_bridge_class_name();

        // FRB places its generated dart code under `lib/src/{module_name}_bridge_generated/`,
        // where `module_name` is the snake_cased crate name.
        // This is independent of the pubspec `name` (which may be a short alias).
        let frb_module_name = config.name.replace('-', "_");

        // Methods declared as `stub_methods` in `[crates.dart]` cannot be bridged through
        // FRB and have `unimplemented!()` bodies on the Rust side. Emitting e2e tests for
        // these fixtures would result in `PanicException` at run-time. Filter them out
        // here so the dart e2e suite mirrors the actual runtime surface of the binding.
        let dart_stub_methods: std::collections::HashSet<String> = config
            .dart
            .as_ref()
            .map(|d| d.stub_methods.iter().cloned().collect())
            .unwrap_or_default();

        // Build the Dart stringy field classification map for aggregating text
        // accessors in Vec<T> contains assertions.
        let dart_first_class_map = values::build_dart_first_class_map(type_defs, enums, e2e_config);

        for group in groups {
            let active: Vec<&Fixture> = group
                .fixtures
                .iter()
                .filter(|f| super::should_include_fixture(f, lang, e2e_config))
                .filter(|f| {
                    let call_config = e2e_config.resolve_call_for_fixture(
                        f.call.as_deref(),
                        &f.id,
                        &f.resolved_category(),
                        &f.tags,
                        &f.input,
                    );
                    let resolved_function = call_config
                        .overrides
                        .get(lang)
                        .and_then(|o| o.function.as_ref())
                        .cloned()
                        .unwrap_or_else(|| call_config.function.clone());
                    !dart_stub_methods.contains(&resolved_function)
                })
                .collect();

            if active.is_empty() {
                continue;
            }

            let filename = format!("{}_test.dart", sanitize_filename(&group.category));
            let content = test_file::render_test_file(
                &group.category,
                &active,
                e2e_config,
                lang,
                &pkg_name,
                &frb_module_name,
                &bridge_class,
                &dart_first_class_map,
                &config.adapters,
                config,
                type_defs,
            );
            files.push(GeneratedFile {
                path: test_base.join(filename),
                content,
                generated_header: true,
            });
        }

        Ok(files)
    }

    fn language_name(&self) -> &'static str {
        "dart"
    }
}

mod assertions;
mod http;
mod project;
mod stubs;
mod test_case;
mod test_file;
mod values;

pub use stubs::emit_test_backend;
pub(super) use values::escape_dart;

#[cfg(test)]
mod tests;

/// Returns `None` when no override sets `result_type`; the renderer then falls
/// back to the workspace-default root-type heuristic in `DartFirstClassMap`.
pub(super) fn dart_call_result_type(call_config: &crate::core::config::e2e::CallConfig) -> Option<String> {
    const LOOKUP_LANGS: &[&str] = &["c", "csharp", "java", "kotlin", "go", "php"];
    for lang in LOOKUP_LANGS {
        if let Some(o) = call_config.overrides.get(*lang)
            && let Some(rt) = o.result_type.as_deref()
            && !rt.is_empty()
        {
            return Some(rt.to_string());
        }
    }
    None
}