pub mod assertion_recipes;
#[cfg(test)]
mod assertion_type_funnel_pairing;
#[cfg(test)]
mod assertion_type_marker_tests;
pub(crate) mod assertion_type_skip;
pub mod assertion_types;
pub mod brew;
pub mod c;
pub(crate) mod call_ir;
pub mod client;
pub mod client_factory;
pub mod csharp;
pub mod dart;
mod dart_visitors;
pub(crate) mod declared_error_variant;
#[cfg(test)]
mod derived_presentation_binding_tests;
pub mod elixir;
pub(crate) mod error_path_assertions;
mod field_resolution;
pub(crate) mod field_skip;
pub mod gleam;
pub mod go;
pub mod homebrew;
pub(crate) mod inert_example;
pub mod java;
mod java_mvnw;
pub mod kotlin;
pub mod kotlin_android;
pub(crate) mod loop_binding;
pub(crate) mod not_error_presence;
pub mod php;
pub mod php_ext;
mod presentation;
pub mod python;
pub mod r;
pub mod recipe;
pub mod ruby;
pub mod rust;
pub(crate) mod snippet_error_branch;
pub mod streaming_assertions;
#[cfg(test)]
mod streaming_result_binding_tests;
pub mod swift;
mod swift_visitors;
pub mod typescript;
#[cfg(test)]
mod unavailable_field_marker_tests;
pub mod wasm;
#[cfg(test)]
mod working_directory_guard_tests;
pub mod zig;
mod zig_visitors;
use crate::core::backend::GeneratedFile;
use crate::core::config::ResolvedCrateConfig;
use crate::core::ir::{EnumDef, MethodDef, TypeDef};
pub(crate) use field_resolution::{resolve_field, resolve_urls_field, select_best_matching_call};
use crate::e2e::config::E2eConfig;
use crate::e2e::fixture::{Fixture, FixtureGroup};
use anyhow::Result;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum InclusionDecision {
Include,
Exclude(&'static str),
}
impl InclusionDecision {
pub fn is_included(&self) -> bool {
matches!(self, Self::Include)
}
}
pub fn call_skip_reason(fixture: &Fixture, language: &str, e2e_config: &E2eConfig) -> Option<&'static str> {
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
if call_config.skip_languages.iter().any(|l| l == language) {
return Some("call skips language");
}
None
}
pub fn fixture_inclusion(fixture: &Fixture, language: &str, e2e_config: &E2eConfig) -> InclusionDecision {
if !e2e_config.exclude_categories.is_empty() && e2e_config.exclude_categories.contains(&fixture.resolved_category())
{
return InclusionDecision::Exclude("excluded category");
}
if let Some(skip) = &fixture.skip
&& skip.should_skip(language)
{
return InclusionDecision::Exclude("fixture skip directive");
}
if let Some(reason) = call_skip_reason(fixture, language, e2e_config) {
return InclusionDecision::Exclude(reason);
}
let call_config = e2e_config.resolve_call_for_fixture(
fixture.call.as_deref(),
&fixture.id,
&fixture.resolved_category(),
&fixture.tags,
&fixture.input,
);
let is_http_fixture = fixture.mock_response.is_some() || fixture.http.is_some();
if !is_http_fixture && call_config.function.is_empty() && !call_config.overrides.contains_key(language) {
return InclusionDecision::Exclude("no callable function");
}
InclusionDecision::Include
}
pub(crate) fn should_include_fixture(fixture: &Fixture, language: &str, e2e_config: &E2eConfig) -> bool {
fixture_inclusion(fixture, language, e2e_config).is_included()
}
pub(crate) fn percent_encode_query(value: &str) -> String {
let mut out = String::with_capacity(value.len());
for &byte in value.as_bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => out.push(byte as char),
_ => out.push_str(&format!("%{byte:02X}")),
}
}
out
}
pub(crate) fn transform_json_keys_for_language(value: &serde_json::Value, wire_case: &str) -> serde_json::Value {
use heck::{ToKebabCase, ToLowerCamelCase, ToPascalCase, ToShoutyKebabCase, ToShoutySnakeCase, ToSnakeCase};
let rewrite_key: fn(&str) -> String = match wire_case {
"camelCase" => |k| k.to_lower_camel_case(),
"PascalCase" => |k| k.to_pascal_case(),
"SCREAMING_SNAKE_CASE" => |k| k.to_shouty_snake_case(),
"kebab-case" => |k| k.to_kebab_case(),
"SCREAMING-KEBAB-CASE" => |k| k.to_shouty_kebab_case(),
_ => |k| k.to_snake_case(),
};
fn walk(value: &serde_json::Value, rewrite_key: fn(&str) -> String) -> serde_json::Value {
match value {
serde_json::Value::Object(obj) => {
let new_obj: serde_json::Map<String, serde_json::Value> = obj
.iter()
.map(|(k, v)| (rewrite_key(k), walk(v, rewrite_key)))
.collect();
serde_json::Value::Object(new_obj)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(|v| walk(v, rewrite_key)).collect())
}
other => other.clone(),
}
}
walk(value, rewrite_key)
}
pub(crate) const MOCK_URL_PLACEHOLDER: &str = "$mock_url";
pub(crate) fn value_contains_mock_url_placeholder(value: &serde_json::Value) -> bool {
match value {
serde_json::Value::String(value) => value.contains(MOCK_URL_PLACEHOLDER),
serde_json::Value::Array(values) => values.iter().any(value_contains_mock_url_placeholder),
serde_json::Value::Object(values) => values.values().any(value_contains_mock_url_placeholder),
serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => false,
}
}
pub(crate) fn mock_url_env_key(fixture_id: &str) -> String {
format!("MOCK_SERVER_{}", fixture_id.to_uppercase())
}
pub(crate) fn declared_error_value(fixture: &crate::e2e::fixture::Fixture) -> Option<&str> {
fixture
.assertions
.iter()
.filter(|assertion| assertion.assertion_type == "error")
.find_map(|assertion| assertion.value.as_ref())
.and_then(serde_json::Value::as_str)
}
pub(crate) fn preserved_url_literal(preserve: bool, value: &serde_json::Value) -> Option<&str> {
if !preserve {
return None;
}
value.as_str()
}
pub(crate) fn preserved_url_list(preserve: bool, value: &serde_json::Value) -> Option<Vec<&str>> {
if !preserve {
return None;
}
value.as_array()?.iter().map(serde_json::Value::as_str).collect()
}
pub(crate) const STRICT_ASSERTIONS_ENV: &str = "ALEF_E2E_STRICT_ASSERTIONS";
fn strict_assertions_default() -> bool {
true
}
pub(crate) fn strict_assertions_enabled() -> bool {
std::env::var(STRICT_ASSERTIONS_ENV)
.ok()
.map_or_else(strict_assertions_default, |raw| !is_falsy_flag(&raw))
}
fn is_falsy_flag(raw: &str) -> bool {
raw == "0" || raw.eq_ignore_ascii_case("false")
}
const COARSE_FIELD_ORACLE_LANGUAGES: &[&str] = &["gleam", "brew"];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SkipVerdict {
UnacknowledgedGap,
Limitation,
AwaitingGeneratorSupport,
Acknowledged(crate::e2e::fixture::AssertionSkipKind),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum SkipOrigin {
Field,
AssertionType,
}
#[derive(Debug, Clone)]
pub(crate) struct SkipRecord {
pub(crate) language: String,
pub(crate) fixture_id: String,
pub(crate) field: String,
pub(crate) verdict: SkipVerdict,
pub(crate) origin: SkipOrigin,
}
thread_local! {
static SKIP_LEDGER: std::cell::RefCell<Vec<SkipRecord>> = const { std::cell::RefCell::new(Vec::new()) };
}
pub(crate) fn take_skip_records() -> Vec<SkipRecord> {
SKIP_LEDGER.with(|ledger| std::mem::take(&mut *ledger.borrow_mut()))
}
pub(crate) fn peek_skip_records(language: &str, fixture_id: &str) -> Vec<SkipRecord> {
SKIP_LEDGER.with(|ledger| {
ledger
.borrow()
.iter()
.filter(|record| record.language == language && record.fixture_id == fixture_id)
.cloned()
.collect()
})
}
pub(crate) fn skip_summary(records: &[SkipRecord]) -> Option<String> {
if records.is_empty() {
return None;
}
use crate::e2e::fixture::AssertionSkipKind;
let fixtures: std::collections::BTreeSet<&str> = records.iter().map(|r| r.fixture_id.as_str()).collect();
let count = |predicate: fn(&SkipVerdict) -> bool| records.iter().filter(|r| predicate(&r.verdict)).count();
let awaiting = count(|v| {
matches!(
v,
SkipVerdict::AwaitingGeneratorSupport | SkipVerdict::Acknowledged(AssertionSkipKind::NotRepresentable)
)
});
let limitations = count(|v| {
matches!(
v,
SkipVerdict::Limitation | SkipVerdict::Acknowledged(AssertionSkipKind::LanguageLimitation)
)
});
let gaps = count(|v| matches!(v, SkipVerdict::UnacknowledgedGap));
let type_skips = records.iter().filter(|r| r.origin == SkipOrigin::AssertionType).count();
let type_skip_suffix = if type_skips > 0 {
format!(", {type_skips} from an assertion type this backend cannot render rather than an unavailable field")
} else {
String::new()
};
Some(format!(
"{} assertion(s) skipped across {} fixture(s): {awaiting} awaiting alef support, \
{limitations} language/ABI limitation(s), {gaps} unresolved field path(s){type_skip_suffix}",
records.len(),
fixtures.len(),
))
}
pub(crate) fn fail_on_unavailable_field_markers(
body: &str,
language: &str,
fixture_id: &str,
assertions: &[crate::e2e::fixture::Assertion],
) {
let coarse_oracle = COARSE_FIELD_ORACLE_LANGUAGES.contains(&language);
for line in body.lines() {
let Some((field, skip)) = field_skip::FieldSkip::extract_classified(line) else {
continue;
};
let declared = assertions
.iter()
.filter(|assertion| assertion.field.as_deref() == Some(field))
.find_map(|assertion| assertion.skip.as_ref())
.filter(|skip| skip.should_skip(language));
let verdict = match (declared, skip.class()) {
(Some(declaration), _) => SkipVerdict::Acknowledged(declaration.kind()),
(None, field_skip::SkipClass::GeneratorGap) => SkipVerdict::AwaitingGeneratorSupport,
(None, field_skip::SkipClass::AuthoringGap) if !coarse_oracle => SkipVerdict::UnacknowledgedGap,
(None, _) => SkipVerdict::Limitation,
};
SKIP_LEDGER.with(|ledger| {
ledger.borrow_mut().push(SkipRecord {
language: language.to_string(),
fixture_id: fixture_id.to_string(),
field: field.to_string(),
verdict,
origin: SkipOrigin::Field,
});
});
}
}
pub(crate) fn fail_on_unsupported_assertion_type_markers(body: &str, language: &str, fixture_id: &str) {
for line in body.lines() {
let Some((token, skip)) = assertion_type_skip::AssertionTypeSkip::extract_classified(line) else {
continue;
};
let verdict = match skip.class() {
field_skip::SkipClass::GeneratorGap => SkipVerdict::AwaitingGeneratorSupport,
field_skip::SkipClass::LanguageLimitation | field_skip::SkipClass::AuthoringGap => SkipVerdict::Limitation,
};
SKIP_LEDGER.with(|ledger| {
ledger.borrow_mut().push(SkipRecord {
language: language.to_string(),
fixture_id: fixture_id.to_string(),
field: token.to_string(),
verdict,
origin: SkipOrigin::AssertionType,
});
});
}
}
pub(crate) fn strict_assertion_failure(records: &[SkipRecord], strict: bool) -> Option<anyhow::Error> {
if !strict {
return None;
}
let gaps: Vec<&SkipRecord> = records
.iter()
.filter(|r| r.verdict == SkipVerdict::UnacknowledgedGap)
.collect();
if gaps.is_empty() {
return None;
}
let detail = gaps
.iter()
.map(|r| format!(" [{}] fixture `{}`: field `{}`", r.language, r.fixture_id, r.field))
.collect::<std::collections::BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
.join("\n");
Some(anyhow::anyhow!(
"{} e2e assertion(s) reference a field the availability oracle cannot resolve, so they \
would have been silently dropped and the generated tests would have passed while \
asserting nothing:\n{detail}\n\nEither fix the field path (or the field-availability \
config) so the assertion runs, or declare on the assertion why it cannot:\n \
\"skip\": {{ \"kind\": \"not_representable\", \"reason\": \"...\" }} — alef cannot \
express this shape yet (an assertion *kind* such as \"the call errored\", a property of \
the call rather than the result, or an assertion over a stream's events)\n \
\"skip\": {{ \"kind\": \"language_limitation\", \"languages\": [\"<lang>\"], \
\"reason\": \"...\" }} — this binding genuinely cannot reach the field\nEither way the \
skip stays counted in the end-of-run summary, in the bucket that names who owns it. Set \
{}=0 to downgrade this to a warning for one run.",
gaps.len(),
STRICT_ASSERTIONS_ENV,
))
}
pub trait E2eCodegen: Send + Sync {
#[allow(clippy::too_many_arguments)]
fn generate(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>>;
fn supported_assertion_types(&self) -> Vec<&'static str> {
assertion_types::supported_assertion_types(self.language_name())
}
#[allow(clippy::too_many_arguments)]
fn generate_gated(
&self,
groups: &[FixtureGroup],
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[crate::core::ir::EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) -> Result<Vec<GeneratedFile>> {
assertion_types::ensure_supported_assertion_types(
groups,
e2e_config,
self.language_name(),
&self.supported_assertion_types(),
)?;
self.generate(groups, e2e_config, config, type_defs, enums, functions, errors)
}
fn render_snippet_body(
&self,
_fixture: &Fixture,
_e2e_config: &E2eConfig,
_config: &ResolvedCrateConfig,
_type_defs: &[TypeDef],
_enums: &[EnumDef],
) -> Result<String> {
anyhow::bail!(
"e2e code generator `{}` does not support documentation snippets",
self.language_name()
)
}
#[allow(clippy::too_many_arguments)]
fn render_snippet_body_with_functions(
&self,
fixture: &Fixture,
e2e_config: &E2eConfig,
config: &ResolvedCrateConfig,
type_defs: &[TypeDef],
enums: &[EnumDef],
functions: &[crate::core::ir::FunctionDef],
errors: &[crate::core::ir::ErrorDef],
) -> Result<String>;
fn language_name(&self) -> &'static str;
}
pub fn all_generators() -> Vec<Box<dyn E2eCodegen>> {
vec![
Box::new(rust::RustE2eCodegen),
Box::new(python::PythonE2eCodegen),
Box::new(typescript::TypeScriptCodegen),
Box::new(go::GoCodegen),
Box::new(java::JavaCodegen),
Box::new(kotlin::KotlinE2eCodegen),
Box::new(kotlin_android::KotlinAndroidE2eCodegen),
Box::new(csharp::CSharpCodegen),
Box::new(php::PhpCodegen),
Box::new(php_ext::PhpExtCodegen),
Box::new(ruby::RubyCodegen),
Box::new(elixir::ElixirCodegen),
Box::new(gleam::GleamE2eCodegen),
Box::new(r::RCodegen),
Box::new(wasm::WasmCodegen),
Box::new(c::CCodegen),
Box::new(zig::ZigE2eCodegen),
Box::new(dart::DartE2eCodegen),
Box::new(swift::SwiftE2eCodegen),
Box::new(brew::BrewCodegen),
Box::new(homebrew::HomebrewCodegen),
]
}
pub fn generators_for(languages: &[String]) -> Vec<Box<dyn E2eCodegen>> {
all_generators()
.into_iter()
.filter(|g| languages.iter().any(|l| l == g.language_name()))
.collect()
}
#[derive(Debug, Clone)]
pub struct TestBackendEmission {
pub setup_block: String,
pub arg_expr: String,
pub type_imports: Vec<String>,
pub teardown_block: String,
}
pub fn emit_test_backend(
language: &str,
trait_bridge: &crate::core::config::TraitBridgeConfig,
methods: &[&MethodDef],
fixture: &Fixture,
enums: &[crate::core::ir::EnumDef],
wasm_type_prefix: &str,
) -> TestBackendEmission {
match language {
"rust" => rust::emit_test_backend(trait_bridge, methods, fixture),
"python" => python::emit_test_backend(trait_bridge, methods, fixture),
"typescript" => typescript::emit_test_backend(trait_bridge, methods, fixture, enums, ""),
"wasm" => typescript::emit_test_backend(trait_bridge, methods, fixture, enums, wasm_type_prefix),
"node" => typescript::emit_test_backend(trait_bridge, methods, fixture, enums, ""),
"go" => go::emit_test_backend(trait_bridge, methods, fixture),
"java" => java::emit_test_backend(trait_bridge, methods, fixture, ""),
"kotlin" => kotlin::emit_test_backend(trait_bridge, methods, fixture),
"kotlin_android" => kotlin_android::emit_test_backend(trait_bridge, methods, fixture, enums),
"csharp" => csharp::emit_test_backend(trait_bridge, methods, fixture),
"php" => php::emit_test_backend(trait_bridge, methods, fixture),
"ruby" => ruby::emit_test_backend(trait_bridge, methods, fixture),
"elixir" => elixir::emit_test_backend(trait_bridge, methods, fixture, "", ""),
"gleam" => gleam::emit_test_backend(trait_bridge, methods, fixture),
"r" => r::emit_test_backend(trait_bridge, methods, fixture),
"c" => c::emit_test_backend(trait_bridge, methods, fixture),
"zig" => zig::emit_test_backend(trait_bridge, methods, fixture),
"dart" => dart::emit_test_backend(trait_bridge, methods, fixture, enums),
"swift" => swift::emit_test_backend(trait_bridge, methods, fixture, enums),
"brew" => brew::emit_test_backend(trait_bridge, methods, fixture),
"php_ext" => php_ext::emit_test_backend(trait_bridge, methods, fixture),
"homebrew" => homebrew::emit_test_backend(trait_bridge, methods, fixture),
_ => panic!(
"e2e codegen: no test_backend emitter registered for language `{language}`; \
cannot generate a test_backend stub for this target"
),
}
}
#[cfg(test)]
mod preserved_url_tests {
use super::{preserved_url_list, preserved_url_literal};
#[test]
fn scalar_url_is_preserved_only_when_requested() {
let value = serde_json::json!("http://127.0.0.1/private");
assert_eq!(preserved_url_literal(true, &value), Some("http://127.0.0.1/private"));
assert_eq!(preserved_url_literal(false, &value), None);
}
#[test]
fn url_list_is_preserved_atomically() {
let value = serde_json::json!(["http://host-a.test/", "file:///tmp/example"]);
assert_eq!(
preserved_url_list(true, &value),
Some(vec!["http://host-a.test/", "file:///tmp/example"])
);
assert_eq!(
preserved_url_list(true, &serde_json::json!(["https://host.test", 7])),
None
);
}
}
#[cfg(test)]
mod unimplemented_test_backend_tests {
use super::emit_test_backend;
use crate::core::config::TraitBridgeConfig;
use crate::e2e::fixture::Fixture;
fn registered_bridge() -> TraitBridgeConfig {
TraitBridgeConfig {
trait_name: "SampleBackend".into(),
..TraitBridgeConfig::default()
}
}
fn sample_fixture() -> Fixture {
Fixture {
id: "register_sample_backend".into(),
..Fixture::default()
}
}
#[test]
fn languages_without_a_real_emitter_panic_through_the_dispatcher() {
for language in ["gleam", "brew", "php_ext", "homebrew", "kotlin", "not-a-real-language"] {
let bridge = registered_bridge();
let fixture = sample_fixture();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
emit_test_backend(language, &bridge, &[], &fixture, &[], "")
}));
assert!(
result.is_err(),
"expected `{language}` to panic instead of returning a TestBackendEmission, but it returned a value"
);
}
}
}