Skip to main content

alef_e2e/codegen/
mod.rs

1//! E2e test code generation trait and language dispatch.
2
3pub mod brew;
4pub mod c;
5pub mod csharp;
6pub mod dart;
7pub mod elixir;
8pub mod gleam;
9pub mod go;
10pub mod java;
11pub mod kotlin;
12pub mod php;
13pub mod python;
14pub mod r;
15pub mod ruby;
16pub mod rust;
17pub mod swift;
18pub mod typescript;
19pub mod wasm;
20pub mod zig;
21
22use crate::config::E2eConfig;
23use crate::fixture::FixtureGroup;
24use alef_core::backend::GeneratedFile;
25use alef_core::config::AlefConfig;
26use anyhow::Result;
27
28/// Convert a JSON value's object keys from camelCase to snake_case recursively.
29///
30/// Used when serializing fixture options for FFI-based languages (Rust, C, Java)
31/// where the receiving Rust type uses default serde (snake_case) without `rename_all`.
32pub(crate) fn normalize_json_keys_to_snake_case(value: &serde_json::Value) -> serde_json::Value {
33    use heck::ToSnakeCase;
34    match value {
35        serde_json::Value::Object(obj) => {
36            let new_obj: serde_json::Map<String, serde_json::Value> = obj
37                .iter()
38                .map(|(k, v)| (k.to_snake_case(), normalize_json_keys_to_snake_case(v)))
39                .collect();
40            serde_json::Value::Object(new_obj)
41        }
42        serde_json::Value::Array(arr) => {
43            serde_json::Value::Array(arr.iter().map(normalize_json_keys_to_snake_case).collect())
44        }
45        other => other.clone(),
46    }
47}
48
49/// Trait for per-language e2e test code generation.
50pub trait E2eCodegen: Send + Sync {
51    /// Generate all e2e test project files for this language.
52    fn generate(
53        &self,
54        groups: &[FixtureGroup],
55        e2e_config: &E2eConfig,
56        alef_config: &AlefConfig,
57    ) -> Result<Vec<GeneratedFile>>;
58
59    /// Language name for display and directory naming.
60    fn language_name(&self) -> &'static str;
61}
62
63/// Get all available e2e code generators.
64pub fn all_generators() -> Vec<Box<dyn E2eCodegen>> {
65    vec![
66        Box::new(rust::RustE2eCodegen),
67        Box::new(python::PythonE2eCodegen),
68        Box::new(typescript::TypeScriptCodegen),
69        Box::new(go::GoCodegen),
70        Box::new(java::JavaCodegen),
71        Box::new(kotlin::KotlinE2eCodegen),
72        Box::new(csharp::CSharpCodegen),
73        Box::new(php::PhpCodegen),
74        Box::new(ruby::RubyCodegen),
75        Box::new(elixir::ElixirCodegen),
76        Box::new(gleam::GleamE2eCodegen),
77        Box::new(r::RCodegen),
78        Box::new(wasm::WasmCodegen),
79        Box::new(c::CCodegen),
80        Box::new(zig::ZigE2eCodegen),
81        Box::new(dart::DartE2eCodegen),
82        Box::new(swift::SwiftE2eCodegen),
83        Box::new(brew::BrewCodegen),
84    ]
85}
86
87/// Get e2e code generators for specific language names.
88pub fn generators_for(languages: &[String]) -> Vec<Box<dyn E2eCodegen>> {
89    all_generators()
90        .into_iter()
91        .filter(|g| languages.iter().any(|l| l == g.language_name()))
92        .collect()
93}
94
95/// Resolve a JSON field from a fixture input by path.
96///
97/// Field paths in call config are "input.path", "input.config", etc.
98/// Since we already receive `fixture.input`, strip the leading "input." prefix.
99pub(crate) fn resolve_field<'a>(input: &'a serde_json::Value, field_path: &str) -> &'a serde_json::Value {
100    let path = field_path.strip_prefix("input.").unwrap_or(field_path);
101    let mut current = input;
102    for part in path.split('.') {
103        current = current.get(part).unwrap_or(&serde_json::Value::Null);
104    }
105    current
106}