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