use super::field::FieldDefinition;
pub(crate) fn validate_no_optional_fields_in_message(
error_message: &str,
fields: &[FieldDefinition],
) -> syn::Result<()> {
let referenced_fields = extract_field_names(error_message);
for field_name in referenced_fields {
if let Some(field) = fields.iter().find(|f| f.rust_name == field_name)
&& field.is_option
{
return Err(syn::Error::new_spanned(
&field.rust_name,
format!(
"optional field `{field_name}` cannot be used in error message; \
Option<T> does not implement Display"
),
));
}
}
Ok(())
}
fn extract_field_names(format_str: &str) -> Vec<String> {
let mut fields = Vec::new();
let mut chars = format_str.chars().peekable();
while let Some(c) = chars.next() {
if c == '{' {
if chars.peek() == Some(&'{') {
chars.next();
continue;
}
let mut content = String::new();
for inner in chars.by_ref() {
if inner == '}' {
break;
}
content.push(inner);
}
let field_name = content.split(':').next().unwrap_or("");
if !field_name.is_empty() && !field_name.chars().all(|c| c.is_ascii_digit()) {
fields.push(field_name.to_string());
}
}
}
fields
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple_field() {
assert_eq!(extract_field_names("hello {name}"), vec!["name"]);
}
#[test]
fn multiple_fields() {
assert_eq!(extract_field_names("{a} and {b}"), vec!["a", "b"]);
}
#[test]
fn field_with_format() {
assert_eq!(extract_field_names("{count:03}"), vec!["count"]);
}
#[test]
fn escaped_braces() {
assert_eq!(
extract_field_names("literal {{ and }}"),
Vec::<String>::new()
);
}
#[test]
fn mixed() {
assert_eq!(
extract_field_names("Error: {message} (code: {code:04})"),
vec!["message", "code"]
);
}
#[test]
fn positional_args_ignored() {
assert_eq!(extract_field_names("{0} and {1}"), Vec::<String>::new());
}
#[test]
fn empty_braces_ignored() {
assert_eq!(extract_field_names("{}"), Vec::<String>::new());
}
}