pub mod assertions;
pub mod cargo_config;
pub mod cargo_toml;
pub mod http;
pub mod mock_server;
pub mod test_file;
mod args;
#[cfg(test)]
mod assertion_containment_tests;
mod assertion_helpers;
mod assertion_streaming;
#[cfg(test)]
mod assertion_streaming_option_tests;
mod assertion_synthetic;
#[cfg(test)]
mod assertion_wildcard_element_tests;
mod assertion_wire;
#[cfg(test)]
mod collection_field_classification_tests;
#[cfg(test)]
mod common_module_generation_tests;
#[cfg(test)]
mod containment_enum_wire_tests;
mod fieldless_result;
#[cfg(test)]
mod fieldless_result_tests;
#[cfg(test)]
mod iterate_display_safety_tests;
#[cfg(test)]
mod manifest_dependency_tests;
#[cfg(test)]
mod renamed_enum_wire_assertion_tests;
#[cfg(test)]
mod snippet_result_binding_tests;
#[cfg(test)]
mod test_backend_tests;
#[cfg(test)]
mod tests;
pub use cargo_config::render_cargo_config;
pub use cargo_toml::{CargoTomlInputs, render_cargo_toml};
pub use mock_server::{render_common_module, render_mock_server_binary, render_mock_server_module};
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use anyhow::Result;
use std::path::PathBuf;
use crate::e2e::config::E2eConfig;
use crate::e2e::escape::sanitize_filename;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use super::E2eCodegen;
use test_file::{is_skipped, render_test_file};
const RUST_JSON_CRATE_PATH: &str = "serde_json::";
pub struct RustE2eCodegen;
impl E2eCodegen for RustE2eCodegen {
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
_errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>> {
let mut files = Vec::new();
let output_base = PathBuf::from(e2e_config.effective_output()).join("rust");
let crate_name = resolve_crate_name(e2e_config, config);
let crate_path = resolve_crate_path(e2e_config, &crate_name);
let dep_name = crate_name.replace('-', "_");
let all_call_configs = std::iter::once(&e2e_config.call).chain(e2e_config.calls.values());
let needs_serde_json = all_call_configs
.flat_map(|c| c.args.iter())
.any(|a| a.arg_type == "json_object" || a.arg_type == "handle");
let needs_mock_server = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| !is_skipped(f, "rust") && f.needs_mock_server());
let needs_http_tests = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.any(|f| !is_skipped(f, "rust") && f.http.is_some());
let needs_tower_http = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.filter(|f| !is_skipped(f, "rust"))
.filter_map(|f| f.http.as_ref())
.filter_map(|h| h.handler.middleware.as_ref())
.any(|m| m.cors.is_some() || m.static_files.is_some());
let any_async_call = std::iter::once(&e2e_config.call)
.chain(e2e_config.calls.values())
.any(|c| c.r#async);
let needs_tokio = needs_mock_server || needs_http_tests || any_async_call;
let all_call_args_for_anyhow = std::iter::once(&e2e_config.call)
.chain(e2e_config.calls.values())
.flat_map(|c| c.args.iter())
.any(|a| a.arg_type == "test_backend");
let any_fixture_test_backend = groups
.iter()
.flat_map(|g| g.fixtures.iter())
.filter(|f| !is_skipped(f, "rust"))
.any(|f| f.args.iter().any(|a| a.arg_type == "test_backend"));
let needs_anyhow = all_call_args_for_anyhow || any_fixture_test_backend;
if let Some(content) = render_cargo_config(&e2e_config.env) {
files.push(GeneratedFile {
path: output_base.join(".cargo").join("config.toml"),
content,
generated_header: false,
});
}
let mut test_files = Vec::new();
for group in groups {
let fixtures: Vec<&Fixture> = group.fixtures.iter().filter(|f| !is_skipped(f, "rust")).collect();
if fixtures.is_empty() {
continue;
}
let filename = format!("{}_test.rs", sanitize_filename(&group.category));
let content = render_test_file(
&group.category,
&fixtures,
e2e_config,
config,
type_defs,
enums,
functions,
&dep_name,
needs_mock_server,
None,
false,
);
test_files.push(GeneratedFile {
path: output_base.join("tests").join(filename),
content,
generated_header: true,
});
}
let body_names = |path: &str| test_files.iter().any(|file| file.content.contains(path));
let needs_tokio_stream = body_names(crate::e2e::codegen::streaming_assertions::RUST_STREAM_CRATE_PATH);
let needs_serde_json = needs_serde_json || body_names(RUST_JSON_CRATE_PATH);
let crate_version = resolve_crate_version(e2e_config).or_else(|| config.resolved_version());
files.push(GeneratedFile {
path: output_base.join("Cargo.toml"),
content: render_cargo_toml(&CargoTomlInputs {
crate_name: &crate_name,
dep_name: &dep_name,
crate_path: &crate_path,
needs_serde_json,
needs_mock_server,
needs_http_tests,
needs_tokio,
needs_tokio_stream,
needs_tower_http,
needs_anyhow,
dep_mode: e2e_config.dep_mode,
version: crate_version.as_deref(),
features: &config.features,
}),
generated_header: true,
});
if needs_mock_server {
files.push(GeneratedFile {
path: output_base.join("tests").join("mock_server.rs"),
content: render_mock_server_module(),
generated_header: true,
});
}
if needs_tokio {
files.push(GeneratedFile {
path: output_base.join("tests").join("common.rs"),
content: render_common_module(),
generated_header: true,
});
}
if needs_mock_server || needs_http_tests {
files.push(GeneratedFile {
path: output_base.join("src").join("main.rs"),
content: render_mock_server_binary(),
generated_header: true,
});
}
files.extend(test_files);
Ok(files)
}
fn render_snippet_body(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
) -> Result<String> {
render_docs_snippet(fixture, e2e_config, config, type_defs, enums, &[])
}
fn render_snippet_body_with_functions(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
_errors: &[crate::core::ir::ErrorDef],
) -> Result<String> {
render_docs_snippet(fixture, e2e_config, config, type_defs, enums, functions)
}
fn language_name(&self) -> &'static str {
"rust"
}
}
fn render_docs_snippet(
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[crate::core::ir::TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
) -> Result<String> {
let dep_name = resolve_crate_name(e2e_config, config).replace('-', "_");
let mut call_fixture = fixture.docs_call_fixture();
let expects_error = fixture
.assertions
.iter()
.any(|assertion| assertion.assertion_type == "error");
super::presentation::apply_derived_shows(&mut call_fixture, e2e_config, "rust", type_defs, enums, functions);
let presentation = super::presentation::resolve(&call_fixture, e2e_config, "rust", type_defs, enums, functions);
let call = e2e_config.resolve_call_for_fixture(
call_fixture.call.as_deref(),
&call_fixture.id,
&call_fixture.resolved_category(),
&call_fixture.tags,
&call_fixture.input,
);
call_fixture.assertions.clear();
call_fixture.mock_response = None;
let is_streaming =
crate::e2e::codegen::streaming_assertions::resolve_is_streaming(&call_fixture, call.streaming_enabled());
let presentation = if is_streaming { Vec::new() } else { presentation };
let expects_error = expects_error && !is_streaming;
let result_var = if is_streaming {
test_file::STREAMING_COLLECTED_VAR
} else {
call.effective_result_var()
};
let test_file = render_test_file(
&fixture.resolved_category(),
&[&call_fixture],
e2e_config,
config,
type_defs,
enums,
&[],
&dep_name,
call_fixture.needs_mock_server(),
fixture.docs_client(),
expects_error,
);
let (imports, body, is_async) = extract_rust_snippet(&test_file)?;
let api_key_var = crate::e2e::fixture::FixtureEnv::api_key_var_or_default(fixture.env.as_ref());
let body = body
.into_iter()
.map(|line| {
line.replace(
"\"test-key\".to_string()",
&format!("std::env::var(\"{api_key_var}\").expect(\"{api_key_var} must be set\")"),
)
})
.collect::<Vec<_>>();
let display_result = !expects_error && presentation.is_empty() && !call.returns_void;
let body = body
.into_iter()
.map(|line| {
if display_result && !is_streaming {
line.replacen("let _ =", &format!("let {result_var} ="), 1)
} else {
line.to_string()
}
})
.collect::<Vec<_>>();
Ok(crate::e2e::template_env::render(
"rust/snippet_body.rs.jinja",
minijinja::context! {
imports => imports, body => body, is_async => is_async, presentation => presentation,
display_result => display_result, result_var => result_var,
expects_error => expects_error, returns_void => call.returns_void,
},
))
}
const BLOCK_ON_OPEN_LINE: &str = "common::runtime().block_on(async {";
fn extract_rust_snippet(rendered: &str) -> Result<(Vec<&str>, Vec<&str>, bool)> {
let lines = rendered.lines().collect::<Vec<_>>();
let signature = lines
.iter()
.position(|line| line.starts_with("fn test_"))
.ok_or_else(|| anyhow::anyhow!("generated Rust test did not contain a fixture function"))?;
let imports = lines[..signature]
.iter()
.copied()
.filter(|line| line.starts_with("use ") && !line.contains("common::"))
.collect();
let function_end = find_function_end(&lines, signature + 1)
.ok_or_else(|| anyhow::anyhow!("generated Rust fixture function was not closed"))?;
let is_async = lines
.get(signature + 1)
.is_some_and(|line| line.trim_start() == BLOCK_ON_OPEN_LINE);
let (body_start, body_end) = if is_async {
(signature + 2, function_end - 1)
} else {
(signature + 1, function_end)
};
let body = lines[body_start..body_end]
.iter()
.copied()
.filter(|line| !line.trim_start().starts_with("//"))
.collect();
Ok((imports, body, is_async))
}
fn find_function_end(lines: &[&str], start: usize) -> Option<usize> {
#[derive(Clone, Copy)]
enum State {
Code,
Str,
RawStr(usize),
}
let mut depth: i32 = 1;
let mut state = State::Code;
for (offset, line) in lines[start..].iter().enumerate() {
let chars: Vec<char> = line.chars().collect();
let mut i = 0;
while i < chars.len() {
match state {
State::Code => {
if chars[i] == '/' && chars.get(i + 1) == Some(&'/') {
break; } else if chars[i] == '"' {
state = State::Str;
} else if let Some(hashes) = raw_string_open(&chars, i) {
state = State::RawStr(hashes);
i += 1 + hashes; } else if chars[i] == '{' {
depth += 1;
} else if chars[i] == '}' {
depth -= 1;
if depth == 0 {
return Some(start + offset);
}
}
}
State::Str => {
if chars[i] == '\\' {
i += 1; } else if chars[i] == '"' {
state = State::Code;
}
}
State::RawStr(hashes) => {
if chars[i] == '"' && raw_string_close_matches(&chars, i + 1, hashes) {
i += hashes; state = State::Code;
}
}
}
i += 1;
}
}
None
}
fn raw_string_open(chars: &[char], i: usize) -> Option<usize> {
if chars.get(i) != Some(&'r') {
return None;
}
let mut hashes = 0;
while chars.get(i + 1 + hashes) == Some(&'#') {
hashes += 1;
}
if chars.get(i + 1 + hashes) == Some(&'"') {
Some(hashes)
} else {
None
}
}
fn raw_string_close_matches(chars: &[char], from: usize, hashes: usize) -> bool {
(0..hashes).all(|offset| chars.get(from + offset) == Some(&'#'))
}
fn resolve_crate_name(_e2e_config: &E2eConfig, config: &ResolvedCrateConfig) -> String {
config.name.clone()
}
fn resolve_crate_path(e2e_config: &E2eConfig, crate_name: &str) -> String {
e2e_config
.resolve_package("rust")
.and_then(|p| p.path.clone())
.unwrap_or_else(|| format!("../../crates/{crate_name}"))
}
fn resolve_crate_version(e2e_config: &E2eConfig) -> Option<String> {
e2e_config.resolve_package("rust").and_then(|p| p.version.clone())
}
pub fn emit_test_backend(
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&crate::core::ir::MethodDef],
fixture: &Fixture,
) -> super::TestBackendEmission {
use crate::codegen::defaults::language_defaults;
use std::fmt::Write as FmtWrite;
let stub_name = format!("TestStub{}", fixture_id_to_pascal_case(&fixture.id));
let trait_name = &trait_bridge.trait_name;
let backend_name = extract_backend_name_from_input(&fixture.input, &fixture.id);
let defaults = language_defaults("rust");
let mut type_imports: Vec<String> = Vec::new();
type_imports.push(trait_name.clone());
let mut setup = String::new();
let crate_module: Option<&str> = trait_bridge
.super_trait
.as_deref()
.and_then(|s| s.split("::").next())
.filter(|s| !s.is_empty());
let _ = writeln!(setup, "struct {stub_name} {{ _name: &'static str }}");
if let Some(super_trait) = &trait_bridge.super_trait {
let super_short = super_trait.split("::").last().unwrap_or(super_trait.as_str());
let _ = writeln!(setup, "impl {super_trait} for {stub_name} {{");
let _ = writeln!(setup, " fn name(&self) -> &str {{ self._name }}");
let _ = writeln!(setup, "}}");
if !super_short.is_empty() && !super_trait.contains("::") {
type_imports.push(super_short.to_string());
}
}
let has_async_methods = methods
.iter()
.any(|m| !(m.has_default_impl || trait_bridge.super_trait.is_some() && m.name == "name") && m.is_async);
if has_async_methods {
let _ = writeln!(setup, "#[async_trait::async_trait]");
}
let _ = writeln!(setup, "impl {trait_name} for {stub_name} {{");
for method in methods {
if method.has_default_impl {
continue;
}
if trait_bridge.super_trait.is_some() && method.name == "name" {
continue;
}
emit_rust_stub_method(&mut setup, method, &*defaults, &mut type_imports, crate_module);
}
let _ = writeln!(setup, "}}");
type_imports.sort();
type_imports.dedup();
let arg_expr = format!("std::sync::Arc::new({stub_name} {{ _name: \"{backend_name}\" }})");
let type_imports = type_imports
.into_iter()
.filter(|s| {
!matches!(
s.as_str(),
"bool"
| "u8"
| "u16"
| "u32"
| "u64"
| "i8"
| "i16"
| "i32"
| "i64"
| "f32"
| "f64"
| "usize"
| "isize"
| "String"
| "str"
| "Vec"
| "Option"
| "Result"
| "()"
)
})
.collect();
super::TestBackendEmission {
setup_block: setup,
arg_expr,
type_imports,
teardown_block: String::new(),
}
}
fn collect_named_types(ty: &crate::core::ir::TypeRef, out: &mut Vec<String>) {
use crate::core::ir::TypeRef;
match ty {
TypeRef::Named(name) => out.push(name.clone()),
TypeRef::Optional(inner) | TypeRef::Vec(inner) => collect_named_types(inner, out),
TypeRef::Map(k, v) => {
collect_named_types(k, out);
collect_named_types(v, out);
}
_ => {}
}
}
fn rust_type_name(ty: &crate::core::ir::TypeRef) -> String {
use crate::codegen::type_mapper::{IdentityMapper, TypeMapper};
IdentityMapper.map_type(ty)
}
fn emit_rust_stub_method(
out: &mut String,
method: &crate::core::ir::MethodDef,
defaults: &dyn crate::codegen::defaults::LanguageDefaults,
type_imports: &mut Vec<String>,
crate_module: Option<&str>,
) {
use crate::core::ir::TypeRef;
use std::fmt::Write as FmtWrite;
let params_typed: Vec<String> = method
.params
.iter()
.enumerate()
.map(|(i, param)| {
collect_named_types(¶m.ty, type_imports);
if param.is_ref {
use crate::core::ir::TypeRef;
let mut_kw = if param.is_mut { "mut " } else { "" };
let ref_str = match ¶m.ty {
TypeRef::Bytes => format!("&{mut_kw}[u8]"),
TypeRef::String => format!("&{mut_kw}str"),
other => format!("&{}{}", mut_kw, rust_type_name(other)),
};
format!("_p{i}: {ref_str}")
} else {
let ty_str = rust_type_name(¶m.ty);
format!("_p{i}: {ty_str}")
}
})
.collect();
let params_str = if params_typed.is_empty() {
String::new()
} else {
format!(", {}", params_typed.join(", "))
};
let return_type_str = if method.returns_ref {
use crate::core::ir::TypeRef;
let ref_type = match &method.return_type {
TypeRef::String => "&str".to_string(),
TypeRef::Bytes => "&[u8]".to_string(),
TypeRef::Vec(inner) => match inner.as_ref() {
TypeRef::String => "&[&str]".to_string(),
TypeRef::Bytes => "&[u8]".to_string(),
other => format!("&[{}]", rust_type_name(other)),
},
other => format!("&{}", rust_type_name(other)),
};
Some(ref_type)
} else {
match &method.return_type {
TypeRef::Unit if method.error_type.is_none() => None,
_ => {
let base = rust_type_name(&method.return_type);
collect_named_types(&method.return_type, type_imports);
let full = if let Some(err) = &method.error_type {
if err == "anyhow::Error" {
if let Some(module) = crate_module {
format!("{module}::Result<{base}>")
} else {
format!("Result<{base}, {err}>")
}
} else {
if !err.contains("::") {
type_imports.push(err.clone());
}
format!("Result<{base}, {err}>")
}
} else {
base
};
Some(full)
}
}
};
let body = if method.returns_ref {
use crate::core::ir::TypeRef;
match &method.return_type {
TypeRef::String => "\"\"".to_string(),
TypeRef::Bytes | TypeRef::Vec(_) => "&[]".to_string(),
_ => format!(
"compile_error!(\"alef cannot generate Rust e2e test_backend method `{}` because it returns an \
unsupported reference type\")",
method.name
),
}
} else {
use crate::core::ir::{PrimitiveType, TypeRef};
let raw = match &method.return_type {
TypeRef::Unit => "()".to_string(),
TypeRef::Primitive(
PrimitiveType::I8
| PrimitiveType::I16
| PrimitiveType::I32
| PrimitiveType::I64
| PrimitiveType::U8
| PrimitiveType::U16
| PrimitiveType::U32
| PrimitiveType::U64
| PrimitiveType::Isize
| PrimitiveType::Usize,
) => "1".to_string(),
_ => defaults.emit_default(&method.return_type),
};
if method.error_type.is_some() {
format!("Ok({raw})")
} else {
raw
}
};
let async_kw = if method.is_async { "async " } else { "" };
let return_annotation = match &return_type_str {
Some(rt) => format!(" -> {rt}"),
None => String::new(),
};
let _ = writeln!(
out,
" {async_kw}fn {name}(&self{params_str}){return_annotation} {{ {body} }}",
name = method.name
);
}
fn fixture_id_to_pascal_case(id: &str) -> String {
id.split('_')
.map(|word| {
let mut chars = word.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().to_string() + chars.as_str(),
}
})
.collect()
}
fn extract_backend_name_from_input(input: &serde_json::Value, fallback: &str) -> String {
if let Some(obj) = input.as_object() {
if let Some(s) = obj.get("name").and_then(|v| v.as_str()) {
return s.to_string();
}
for v in obj.values() {
if let Some(inner) = v.as_object()
&& let Some(s) = inner.get("name").and_then(|v| v.as_str())
{
return s.to_string();
}
}
for v in obj.values() {
if let Some(s) = v.as_str() {
return s.to_string();
}
}
}
fallback.to_string()
}
#[cfg(test)]
fn make_fixture(id: &str, input: serde_json::Value) -> crate::e2e::fixture::Fixture {
serde_json::from_value(serde_json::json!({
"id": id,
"description": "test fixture",
"input": input,
"assertions": []
}))
.expect("minimal fixture JSON must parse")
}
#[cfg(test)]
fn test_method(
name: &str,
return_type: crate::core::ir::TypeRef,
is_async: bool,
error_type: Option<&str>,
) -> crate::core::ir::MethodDef {
crate::core::ir::MethodDef {
name: name.to_string(),
params: Vec::new(),
return_type,
is_async,
is_static: false,
error_type: error_type.map(str::to_string),
doc: String::new(),
receiver: Some(crate::core::ir::ReceiverKind::Ref),
cfg: None,
sanitized: false,
trait_source: None,
returns_ref: false,
returns_cow: false,
return_newtype_wrapper: None,
has_default_impl: false,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}