alef_e2e/lib.rs
1//! Fixture-driven e2e test generation for alef.
2//!
3//! This crate generates complete, runnable e2e test projects for all supported
4//! languages from JSON fixture files. Each project is self-contained with
5//! build files, test files, and local package references.
6
7pub mod codegen;
8pub mod config;
9pub mod escape;
10pub mod field_access;
11pub mod fixture;
12pub mod format;
13pub mod scaffold;
14pub mod template_env;
15pub mod validate;
16
17use alef_core::backend::GeneratedFile;
18use alef_core::config::e2e::DependencyMode;
19use alef_core::config::{Language, ResolvedCrateConfig};
20use anyhow::{Context, Result};
21use config::E2eConfig;
22use fixture::{group_fixtures, load_fixtures};
23use std::path::Path;
24use tracing::{info, warn};
25use validate::Severity;
26
27/// Map the top-level `[languages]` list (the scaffolded bindings) to the
28/// e2e generator names registered in [`codegen::all_generators`].
29///
30/// `Language::Ffi` maps to the `c` generator (the FFI binding's e2e harness
31/// is the C test runner). `Language::Rust` is always appended because rust is
32/// the source language and the rust e2e suite exercises the core crate.
33///
34/// Generators that don't have a corresponding `Language` variant (e.g.
35/// `brew`) are intentionally excluded — they require an explicit opt-in via
36/// `[e2e].languages` in alef.toml.
37pub fn default_e2e_languages(scaffolded: &[Language]) -> Vec<String> {
38 let mut names: Vec<String> = scaffolded
39 .iter()
40 .map(|l| match l {
41 Language::Ffi => "c".to_string(),
42 other => other.to_string(),
43 })
44 .collect();
45 if !names.iter().any(|n| n == "rust") {
46 names.push("rust".to_string());
47 }
48 names
49}
50
51/// Generate e2e test projects from fixtures.
52///
53/// Returns the list of generated files. The caller is responsible for writing
54/// them to disk.
55///
56/// `type_defs` is the IR type registry for the source crate. Pass
57/// `&api.types` from the extracted [`alef_core::ir::ApiSurface`]. It is
58/// forwarded to generators that need to introspect struct field types (e.g.
59/// the TypeScript/WASM backend uses it to auto-derive `nested_types` for
60/// wasm-bindgen class wrapping). Pass an empty slice when the registry is not
61/// available; generators will fall back to explicit call-override mappings.
62pub fn generate_e2e(
63 config: &ResolvedCrateConfig,
64 e2e_config: &E2eConfig,
65 languages: Option<&[String]>,
66 type_defs: &[alef_core::ir::TypeDef],
67) -> Result<Vec<GeneratedFile>> {
68 let fixtures_dir = Path::new(&e2e_config.fixtures);
69 let fixtures = load_fixtures(fixtures_dir)
70 .with_context(|| format!("failed to load fixtures from {}", fixtures_dir.display()))?;
71
72 info!("Loaded {} fixture(s) from {}", fixtures.len(), e2e_config.fixtures);
73
74 // Resolution order for which language generators to run:
75 // 1. Explicit `--lang` filter from the CLI (highest priority).
76 // 2. `[e2e].languages` from alef.toml when set.
77 // 3. The top-level `[languages]` list mapped to e2e generator names —
78 // so e2e tests are only generated for actually scaffolded bindings,
79 // never for backends the consumer hasn't opted into.
80 //
81 // The legacy `all_generators()` fallback is removed; emitting tests for
82 // languages without a matching binding produces broken e2e dirs that
83 // cannot compile.
84 let resolved_languages: Vec<String> = if let Some(langs) = languages {
85 langs.to_vec()
86 } else if !e2e_config.languages.is_empty() {
87 e2e_config.languages.clone()
88 } else {
89 default_e2e_languages(&config.languages)
90 };
91
92 // Run semantic validation against the resolved language set so the
93 // empty-category check warns about the same languages we're about to
94 // generate for.
95 let diagnostics = validate::validate_fixtures_semantic(&fixtures, e2e_config, &resolved_languages);
96 for diag in &diagnostics {
97 match diag.severity {
98 Severity::Error => warn!("{}: {}", diag.file, diag.message),
99 Severity::Warning => warn!("{}: {}", diag.file, diag.message),
100 }
101 }
102
103 let all_groups = group_fixtures(&fixtures);
104
105 // Drop categories that are explicitly excluded from cross-language e2e
106 // codegen. These fixtures stay on disk for Rust integration tests but
107 // never reach binding generators.
108 let all_groups: Vec<_> = if e2e_config.exclude_categories.is_empty() {
109 all_groups
110 } else {
111 all_groups
112 .into_iter()
113 .filter(|g| !e2e_config.exclude_categories.contains(&g.category))
114 .collect()
115 };
116
117 // In registry mode with a non-empty category filter, keep only the listed
118 // categories so the generated test apps contain a curated subset.
119 let groups: Vec<_> =
120 if e2e_config.dep_mode == DependencyMode::Registry && !e2e_config.registry.categories.is_empty() {
121 let allowed = &e2e_config.registry.categories;
122 all_groups
123 .into_iter()
124 .filter(|g| allowed.iter().any(|c| c == &g.category))
125 .collect()
126 } else {
127 all_groups
128 };
129
130 let generators = codegen::generators_for(&resolved_languages);
131
132 let mut all_files = Vec::new();
133 for generator in &generators {
134 let files = generator.generate(&groups, e2e_config, config, type_defs)?;
135 info!(" [{}] generated {} file(s)", generator.language_name(), files.len());
136 all_files.extend(files);
137 }
138
139 Ok(all_files)
140}