1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
use super::defaults::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
/// Route registration call form for language-specific idioms in e2e harness code generation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RouteCallForm {
/// Direct 2-arg: app.route(builder, handler) — PHP reference, C#, Go, Node, TypeScript, etc.
Direct,
/// Decorator 1-arg returning callable: app.route(builder)(handler) — Python verb-decorator
Decorator,
/// Block form: app.route(builder) { |req| handler.call(req) } — Ruby blocks
Block,
}
/// Per-language harness config overrides.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HarnessOverride {
/// Method to register handlers (overrides HarnessConfig.register_method)
#[serde(default)]
pub register_method: Option<String>,
/// SUT app class name (overrides HarnessConfig.app_class)
#[serde(default)]
pub app_class: Option<String>,
/// Method/field on RouteBuilder to set request body schema
#[serde(default)]
pub body_schema_setter: Option<String>,
/// HTTP method enum/type name
#[serde(default)]
pub method_enum: Option<String>,
/// Serve entrypoint method name (overrides HarnessConfig.run_method)
#[serde(default)]
pub run_method: Option<String>,
/// Modules/packages to import the SUT app from (overrides HarnessConfig.imports)
#[serde(default)]
pub imports: Option<Vec<String>>,
/// Expression to instantiate the default ServerConfig (overrides HarnessConfig.server_config_factory)
#[serde(default)]
pub server_config_factory: Option<String>,
}
/// Server-shaped e2e harness configuration for HTTP fixtures.
///
/// When HTTP fixtures are present (server-pattern testing), alef generates
/// a harness script that starts the SUT app, registers handlers per fixture,
/// and serves requests. This config provides the language-agnostic knobs that
/// control harness code generation.
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct HarnessConfig {
/// Module/package to import the SUT app from (e.g., "my_app")
#[serde(default)]
pub imports: Vec<String>,
/// SUT app class name (e.g., "App")
#[serde(default)]
pub app_class: Option<String>,
/// Method to register handlers on the app (e.g., "route" or "get"/"post"/etc.)
#[serde(default)]
pub register_method: Option<String>,
/// Method/field on RouteBuilder to set request body schema (e.g., "request_schema_json")
#[serde(default)]
pub body_schema_setter: Option<String>,
/// RouteBuilder class name (if distinct from return type of register_method)
#[serde(default)]
pub route_builder: Option<String>,
/// HTTP method enum/type name (e.g., "Method")
#[serde(default)]
pub method_enum: Option<String>,
/// Serve entrypoint method name (e.g., "run")
#[serde(default)]
pub run_method: Option<String>,
/// Field name in the handler's returned response object that carries the
/// response body payload (e.g., "body" or "content"). Defaults to "body".
/// Configure to match the deserialization shape of the SUT's Response type.
#[serde(default = "default_response_body_field")]
pub response_body_field: String,
/// Default host for SUT binding (e.g., "127.0.0.1")
#[serde(default = "default_harness_host")]
pub host: String,
/// Default port for SUT binding (e.g., 8000)
#[serde(default = "default_harness_port")]
pub port: u16,
/// Per-language harness overrides
#[serde(default)]
pub overrides: HashMap<String, HarnessOverride>,
}
impl Default for HarnessConfig {
fn default() -> Self {
Self {
imports: Vec::new(),
app_class: None,
register_method: None,
body_schema_setter: None,
route_builder: None,
method_enum: None,
run_method: None,
response_body_field: default_response_body_field(),
host: default_harness_host(),
port: default_harness_port(),
overrides: HashMap::new(),
}
}
}
impl HarnessConfig {
/// Get the effective register_method for a language, applying language-specific overrides.
pub fn register_method_for_lang(&self, lang: &str) -> Option<String> {
self.overrides
.get(lang)
.and_then(|o| o.register_method.clone())
.or_else(|| self.register_method.clone())
}
/// Get the effective run_method for a language, applying language-specific overrides.
pub fn run_method_for_lang(&self, lang: &str) -> Option<String> {
self.overrides
.get(lang)
.and_then(|o| o.run_method.clone())
.or_else(|| self.run_method.clone())
}
/// Get the effective app_class for a language, applying language-specific overrides.
pub fn app_class_for_lang(&self, lang: &str) -> Option<String> {
self.overrides
.get(lang)
.and_then(|o| o.app_class.clone())
.or_else(|| self.app_class.clone())
}
/// Get the effective imports for a language, applying language-specific overrides.
pub fn imports_for_lang(&self, lang: &str) -> Vec<String> {
self.overrides
.get(lang)
.and_then(|o| o.imports.clone())
.unwrap_or_else(|| self.imports.clone())
}
/// Get the ServerConfig factory expression for a language, applying language-specific overrides.
/// Returns a code expression that instantiates a default ServerConfig.
/// Backend-specific defaults: "node" → "serverConfigDefault()", "wasm" → "new WasmServerConfig()".
/// Falls back to "new ServerConfig()" for other languages.
pub fn server_config_factory_for_lang(&self, lang: &str) -> String {
self.overrides
.get(lang)
.and_then(|o| o.server_config_factory.clone())
.unwrap_or_else(|| match lang {
"node" => "serverConfigDefault()".to_string(),
"wasm" => "new WasmServerConfig()".to_string(),
_ => "new ServerConfig()".to_string(),
})
}
/// Get the import name for the ServerConfig factory, if the factory is a bare identifier.
/// Returns the identifier to include in the import destructure when the factory
/// expression is a function/class that must be imported.
/// - "node" → Some("serverConfigDefault")
/// - "wasm" → Some("WasmServerConfig")
/// - others → None (assumes available in scope or uses `new ClassName()`)
pub fn server_config_factory_import_for_lang(&self, lang: &str) -> Option<String> {
match lang {
"node" => Some("serverConfigDefault".to_string()),
"wasm" => Some("WasmServerConfig".to_string()),
_ => None,
}
}
/// Get the route registration call form for a language (Direct 2-arg, Decorator 1-arg, or Block form).
/// Determines how app.route(builder, handler) is emitted for language-specific idioms:
/// - Direct: app.route(builder, handler) [PHP reference, most others]
/// - Decorator: app.route(builder)(handler) [Python verb-decorator]
/// - Block: app.route(builder) { |req| handler.call(req) } [Ruby block]
pub fn harness_route_call_form_for_lang(&self, lang: &str) -> RouteCallForm {
match lang {
"python" => RouteCallForm::Decorator,
"ruby" => RouteCallForm::Block,
_ => RouteCallForm::Direct,
}
}
/// Get the import style for a language's TypeScript harness.
/// - "named" for wasm (wasm-bindgen emits only named exports)
/// - "default" for node/typescript (Node.js and TypeScript expect default imports)
pub fn import_style_for_lang(&self, lang: &str) -> &'static str {
match lang {
"wasm" => "named",
_ => "default",
}
}
/// Get the register_method for `lang` rendered in the language's idiomatic
/// identifier case.
///
/// The canonical name in `[crates.e2e.harness] register_method` (and any
/// per-language override under `[crates.e2e.harness.overrides.<lang>]`) is
/// stored verbatim. Each language has its own identifier convention:
///
/// - **snake_case** (python, ruby, elixir, rust, php) — leave verbatim;
/// PHP's PSR-1 prefers camelCase but historic snake_case method names
/// remain idiomatic for binding wrappers.
/// - **camelCase** (typescript / node, javascript, dart, swift, kotlin,
/// java) — convert `register_route` → `registerRoute`.
/// - **PascalCase** (csharp, go) — convert `register_route` →
/// `RegisterRoute`. Go exports require an upper-case leading character;
/// C# methods are PascalCase by convention.
///
/// Returns `None` when neither the per-language override nor the top-level
/// `register_method` is set.
pub fn register_method_idiomatic(&self, lang: &str) -> Option<String> {
self.register_method_for_lang(lang)
.map(|name| idiomatic_identifier(&name, lang))
}
}
/// Convert `name` (typically snake_case) into the identifier case idiomatic
/// for `lang`. Single-word names round-trip unchanged (e.g. `route` stays
/// `route` in every language).
fn idiomatic_identifier(name: &str, lang: &str) -> String {
use heck::{ToLowerCamelCase, ToUpperCamelCase};
match lang {
// Snake-case-native languages: leave as-is. PHP's PSR-1 prefers
// camelCase for method names, but binding consumers (and the PHP
// service-API codegen itself) historically emit snake_case methods,
// so retain the canonical form here.
"python" | "ruby" | "elixir" | "rust" | "php" => name.to_string(),
// camelCase languages.
"typescript" | "node" | "wasm" | "javascript" | "dart" | "swift" | "kotlin" | "kotlin-android" | "java" => {
name.to_lower_camel_case()
}
// PascalCase languages.
"csharp" | "go" => name.to_upper_camel_case(),
// Unknown language: be conservative and pass through.
_ => name.to_string(),
}
}