use std::fmt::Write as FmtWrite;
use crate::core::ir::{ErrorDef, MethodDef};
use crate::e2e::fixture::{Assertion, Fixture};
pub(crate) const STATUS_CODE: &[&str] = &["status_code"];
pub(crate) fn reachable_errors(errors: &[ErrorDef], supported: &[&str]) -> Vec<ErrorDef> {
crate::codegen::binding_projection::visible_errors(errors)
.cloned()
.map(|mut error| {
error
.methods
.retain(|method| !method.sanitized && supported.contains(&method.name.as_str()));
error
})
.collect()
}
pub(crate) fn checks<'a>(
fixture: &'a Fixture,
reachable: &'a [ErrorDef],
) -> Vec<(&'a ErrorDef, &'a MethodDef, Option<i64>, &'a Assertion)> {
fixture
.assertions
.iter()
.filter_map(|assertion| {
let (error, method) =
crate::e2e::codegen::error_field_reachability::resolvable_equals_error_field(assertion, reachable)?;
let expected = assertion.value.as_ref().and_then(serde_json::Value::as_i64);
Some((error, method, expected, assertion))
})
.collect()
}
pub(crate) fn unrenderable_marker(
marker_prefix: &str,
language: &str,
fixture: &Fixture,
assertion: &Assertion,
) -> String {
let field = assertion.field.as_deref().unwrap_or("<none>");
let line = format!(
"{marker_prefix}skipped: assertion type 'equals' has no accessor for error field {field} in this backend"
);
crate::e2e::codegen::fail_on_unsupported_assertion_type_markers(&line, language, &fixture.id);
line
}
pub(crate) fn render(
fixture: &Fixture,
reachable: &[ErrorDef],
marker_prefix: &str,
language: &str,
statement: impl Fn(&ErrorDef, &str, i64) -> String,
) -> String {
let mut out = String::new();
for (error, method, expected, assertion) in checks(fixture, reachable) {
match expected {
Some(expected) => out.push_str(&statement(error, &method.name, expected)),
None => {
let _ = writeln!(
out,
"{}",
unrenderable_marker(marker_prefix, language, fixture, assertion)
);
}
}
}
out
}
#[cfg(test)]
pub(crate) mod test_fixtures {
use crate::core::ir::{ErrorDef, ErrorVariant, MethodDef, PrimitiveType, TypeRef};
use crate::e2e::fixture::{Assertion, Fixture};
pub(crate) fn error_with_methods(name: &str, methods: &[&str]) -> Vec<ErrorDef> {
vec![ErrorDef {
exception_base: None,
name: name.to_string(),
rust_path: format!("lib::{name}"),
original_rust_path: String::new(),
variants: vec![ErrorVariant {
name: "Authentication".to_string(),
error_code: Some(100),
is_unit: true,
..ErrorVariant::default()
}],
doc: String::new(),
methods: methods
.iter()
.map(|method| MethodDef {
name: (*method).to_string(),
return_type: TypeRef::Primitive(PrimitiveType::U16),
..MethodDef::default()
})
.collect(),
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}]
}
pub(crate) fn status_code_errors() -> Vec<ErrorDef> {
error_with_methods("ApiError", &["status_code"])
}
pub(crate) fn status_code_fixture(status: i64) -> Fixture {
Fixture {
id: "error_auth_401".to_string(),
assertions: vec![
Assertion {
assertion_type: "error".to_string(),
..Assertion::default()
},
Assertion {
assertion_type: "equals".to_string(),
field: Some("error.status_code".to_string()),
value: Some(serde_json::json!(status)),
..Assertion::default()
},
],
..Fixture::default()
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::ir::{ErrorVariant, MethodDef, TypeRef};
use crate::e2e::fixture::Assertion;
fn error_with(methods: &[&str]) -> Vec<ErrorDef> {
vec![ErrorDef {
exception_base: None,
name: "ApiError".to_string(),
rust_path: "lib::ApiError".to_string(),
original_rust_path: String::new(),
variants: vec![ErrorVariant::default()],
doc: String::new(),
methods: methods
.iter()
.map(|name| MethodDef {
name: (*name).to_string(),
return_type: TypeRef::Primitive(crate::core::ir::PrimitiveType::U16),
..MethodDef::default()
})
.collect(),
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}]
}
fn equals(field: &str, value: serde_json::Value) -> Assertion {
Assertion {
assertion_type: "equals".to_string(),
field: Some(field.to_string()),
value: Some(value),
..Assertion::default()
}
}
fn fixture(assertions: Vec<Assertion>) -> Fixture {
Fixture {
id: "rate_limited".to_string(),
assertions,
..Fixture::default()
}
}
#[test]
fn reachable_errors_keeps_only_the_supported_methods() {
let narrowed = reachable_errors(&error_with(&["status_code", "is_transient"]), &["status_code"]);
let names: Vec<_> = narrowed[0].methods.iter().map(|m| m.name.as_str()).collect();
assert_eq!(names, ["status_code"]);
}
#[test]
fn a_whole_number_value_is_rendered_by_the_backend_statement() {
let reachable = reachable_errors(&error_with(&["status_code"]), &["status_code"]);
let fixture = fixture(vec![equals("error.status_code", serde_json::json!(429))]);
let out = render(&fixture, &reachable, "// ", "go", |_, method, value| {
format!("check {method} == {value}\n")
});
assert_eq!(out, "check status_code == 429\n");
}
#[test]
fn a_method_the_backend_does_not_support_renders_nothing() {
let reachable = reachable_errors(&error_with(&["status_code", "is_transient"]), &["status_code"]);
let fixture = fixture(vec![equals("error.is_transient", serde_json::json!(true))]);
let out = render(&fixture, &reachable, "// ", "go", |_, _, _| "unexpected\n".to_string());
assert_eq!(out, "");
}
#[test]
fn a_value_that_is_not_a_whole_number_keeps_the_skip_marker() {
let reachable = reachable_errors(&error_with(&["status_code"]), &["status_code"]);
let fixture = fixture(vec![equals("error.status_code", serde_json::json!("429"))]);
let _ = crate::e2e::codegen::take_skip_records();
let out = render(&fixture, &reachable, "// ", "go", |_, _, _| "unexpected\n".to_string());
assert_eq!(
out,
"// skipped: assertion type 'equals' has no accessor for error field error.status_code in this backend\n"
);
assert_eq!(crate::e2e::codegen::take_skip_records().len(), 1);
}
}