use crate::core::ir::{ErrorDef, MethodDef, PrimitiveType, TypeRef};
#[derive(Debug, Clone, PartialEq)]
pub enum LastErrorFieldKind {
Scalar(PrimitiveType),
Text,
DurationMillis,
}
#[derive(Debug, Clone)]
pub struct LastErrorField {
pub name: String,
pub kind: LastErrorFieldKind,
}
const RESERVED_NAMES: &[&str] = &["code", "context", "variant"];
pub fn last_error_field_kind(method: &MethodDef) -> Option<LastErrorFieldKind> {
if method.is_static
|| method.is_async
|| !method.params.is_empty()
|| RESERVED_NAMES.contains(&method.name.as_str())
{
return None;
}
match &method.return_type {
TypeRef::Primitive(PrimitiveType::Usize | PrimitiveType::Isize) => None,
TypeRef::Primitive(primitive) => Some(LastErrorFieldKind::Scalar(primitive.clone())),
TypeRef::String => Some(LastErrorFieldKind::Text),
TypeRef::Duration => Some(LastErrorFieldKind::DurationMillis),
TypeRef::Optional(inner) if matches!(inner.as_ref(), TypeRef::Duration) => {
Some(LastErrorFieldKind::DurationMillis)
}
_ => None,
}
}
pub fn last_error_fields(errors: &[ErrorDef]) -> Vec<LastErrorField> {
let mut fields: Vec<LastErrorField> = Vec::new();
for method in errors.iter().flat_map(|error| &error.methods) {
let Some(kind) = last_error_field_kind(method) else {
continue;
};
if !fields.iter().any(|field| field.name == method.name) {
fields.push(LastErrorField {
name: method.name.clone(),
kind,
});
}
}
fields
}
#[cfg(test)]
mod tests {
use super::*;
fn method(name: &str, return_type: TypeRef) -> MethodDef {
MethodDef {
name: name.to_string(),
return_type,
..MethodDef::default()
}
}
fn error(name: &str, methods: Vec<MethodDef>) -> ErrorDef {
ErrorDef {
name: name.to_string(),
rust_path: format!("sample::{name}"),
original_rust_path: String::new(),
variants: vec![],
doc: String::new(),
methods,
binding_excluded: false,
binding_exclusion_reason: None,
version: Default::default(),
}
}
#[test]
fn unions_capturable_methods_and_skips_the_rest() {
let mut static_method = method("with_arg", TypeRef::String);
static_method.is_static = true;
let mut async_method = method("later", TypeRef::String);
async_method.is_async = true;
let sample = error(
"SampleError",
vec![
method("status_code", TypeRef::Primitive(PrimitiveType::U16)),
method("is_transient", TypeRef::Primitive(PrimitiveType::Bool)),
method("error_type", TypeRef::String),
method("retry_after", TypeRef::Optional(Box::new(TypeRef::Duration))),
method("payload_len", TypeRef::Primitive(PrimitiveType::Usize)),
method("code", TypeRef::Primitive(PrimitiveType::U32)),
method("details", TypeRef::Vec(Box::new(TypeRef::String))),
static_method,
async_method,
],
);
let names: Vec<String> = last_error_fields(&[sample])
.into_iter()
.map(|field| field.name)
.collect();
assert_eq!(names, ["status_code", "is_transient", "error_type", "retry_after"]);
}
#[test]
fn duration_methods_travel_as_milliseconds() {
let optional = method("retry_after", TypeRef::Optional(Box::new(TypeRef::Duration)));
let bare = method("timeout", TypeRef::Duration);
assert_eq!(
last_error_field_kind(&optional),
Some(LastErrorFieldKind::DurationMillis)
);
assert_eq!(last_error_field_kind(&bare), Some(LastErrorFieldKind::DurationMillis));
}
#[test]
fn first_declaration_wins_when_two_errors_disagree_on_a_kind() {
let first = error(
"FirstError",
vec![method("status_code", TypeRef::Primitive(PrimitiveType::U16))],
);
let second = error("SecondError", vec![method("status_code", TypeRef::String)]);
let fields = last_error_fields(&[first, second]);
assert_eq!(fields.len(), 1);
assert_eq!(fields[0].kind, LastErrorFieldKind::Scalar(PrimitiveType::U16));
}
}