Skip to main content

alef_e2e/codegen/
mod.rs

1//! E2e test code generation trait and language dispatch.
2//!
3//! ## DRY layer ([`client`])
4//!
5//! Per-language e2e codegen historically duplicated the structural shape of every
6//! test (function header, request build, response assert) and only differed in
7//! syntax. The [`client`] submodule pulls that shape into trait + driver pairs
8//! ([`client::TestClientRenderer`] + [`client::http_call::render_http_test`])
9//! so each language can be migrated to TestClient-driven tests by:
10//!
11//! 1. Implementing `TestClientRenderer` once per language (small, mechanical).
12//! 2. Replacing the language's monolithic `render_http_test_function` with a
13//!    call to `client::http_call::render_http_test(out, &MyRenderer, fixture)`.
14//! 3. Optionally splitting the per-language file into a directory
15//!    `<lang>/{mod.rs,client.rs,ws.rs,helpers.rs}` when the file gets unwieldy.
16//!
17//! Until a language migrates, it continues using the legacy monolithic renderer —
18//! both can coexist behind the per-language [`E2eCodegen::generate`] entry.
19
20pub mod brew;
21pub mod c;
22pub mod client;
23pub mod csharp;
24pub mod dart;
25pub mod elixir;
26pub mod gleam;
27pub mod go;
28pub mod java;
29pub mod kotlin;
30pub mod php;
31pub mod python;
32pub mod r;
33pub mod ruby;
34pub mod rust;
35pub mod swift;
36pub mod typescript;
37pub mod wasm;
38pub mod zig;
39
40use crate::config::E2eConfig;
41use crate::fixture::FixtureGroup;
42use alef_core::backend::GeneratedFile;
43use alef_core::config::AlefConfig;
44use anyhow::Result;
45
46/// Convert a JSON value's object keys from camelCase to snake_case recursively.
47///
48/// Used when serializing fixture options for FFI-based languages (Rust, C, Java)
49/// where the receiving Rust type uses default serde (snake_case) without `rename_all`.
50pub(crate) fn normalize_json_keys_to_snake_case(value: &serde_json::Value) -> serde_json::Value {
51    use heck::ToSnakeCase;
52    match value {
53        serde_json::Value::Object(obj) => {
54            let new_obj: serde_json::Map<String, serde_json::Value> = obj
55                .iter()
56                .map(|(k, v)| (k.to_snake_case(), normalize_json_keys_to_snake_case(v)))
57                .collect();
58            serde_json::Value::Object(new_obj)
59        }
60        serde_json::Value::Array(arr) => {
61            serde_json::Value::Array(arr.iter().map(normalize_json_keys_to_snake_case).collect())
62        }
63        other => other.clone(),
64    }
65}
66
67/// Trait for per-language e2e test code generation.
68pub trait E2eCodegen: Send + Sync {
69    /// Generate all e2e test project files for this language.
70    fn generate(
71        &self,
72        groups: &[FixtureGroup],
73        e2e_config: &E2eConfig,
74        alef_config: &AlefConfig,
75    ) -> Result<Vec<GeneratedFile>>;
76
77    /// Language name for display and directory naming.
78    fn language_name(&self) -> &'static str;
79}
80
81/// Get all available e2e code generators.
82pub fn all_generators() -> Vec<Box<dyn E2eCodegen>> {
83    vec![
84        Box::new(rust::RustE2eCodegen),
85        Box::new(python::PythonE2eCodegen),
86        Box::new(typescript::TypeScriptCodegen),
87        Box::new(go::GoCodegen),
88        Box::new(java::JavaCodegen),
89        Box::new(kotlin::KotlinE2eCodegen),
90        Box::new(csharp::CSharpCodegen),
91        Box::new(php::PhpCodegen),
92        Box::new(ruby::RubyCodegen),
93        Box::new(elixir::ElixirCodegen),
94        Box::new(gleam::GleamE2eCodegen),
95        Box::new(r::RCodegen),
96        Box::new(wasm::WasmCodegen),
97        Box::new(c::CCodegen),
98        Box::new(zig::ZigE2eCodegen),
99        Box::new(dart::DartE2eCodegen),
100        Box::new(swift::SwiftE2eCodegen),
101        Box::new(brew::BrewCodegen),
102    ]
103}
104
105/// Get e2e code generators for specific language names.
106pub fn generators_for(languages: &[String]) -> Vec<Box<dyn E2eCodegen>> {
107    all_generators()
108        .into_iter()
109        .filter(|g| languages.iter().any(|l| l == g.language_name()))
110        .collect()
111}
112
113/// Resolve a JSON field from a fixture input by path.
114///
115/// Field paths in call config are "input.path", "input.config", etc.
116/// Since we already receive `fixture.input`, strip the leading "input." prefix.
117pub(crate) fn resolve_field<'a>(input: &'a serde_json::Value, field_path: &str) -> &'a serde_json::Value {
118    let path = field_path.strip_prefix("input.").unwrap_or(field_path);
119    let mut current = input;
120    for part in path.split('.') {
121        current = current.get(part).unwrap_or(&serde_json::Value::Null);
122    }
123    current
124}