use crate::types::*;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ManipulatorError {
PluralOrSelectInTag,
ConflictingVariableType {
variable: String,
expected: Type,
found: Type,
},
}
impl fmt::Display for ManipulatorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ManipulatorError::PluralOrSelectInTag => {
write!(
f,
"Cannot hoist plural/select within a tag element. \
Please put the tag element inside each plural/select option"
)
}
ManipulatorError::ConflictingVariableType {
variable,
expected,
found,
} => {
write!(
f,
"Variable '{}' has conflicting types: {:?} vs {:?}",
variable, expected, found
)
}
}
}
}
impl std::error::Error for ManipulatorError {}
pub type StructuralComparisonResult = Result<(), StructuralComparisonError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StructuralComparisonError {
DifferentVariableCount {
a_vars: Vec<String>,
b_vars: Vec<String>,
context: Option<String>,
},
MissingVariable {
variable: String,
var_type: Type,
context: Option<String>,
},
}
impl fmt::Display for StructuralComparisonError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
StructuralComparisonError::DifferentVariableCount {
a_vars,
b_vars,
context,
} => {
if let Some(ctx) = context {
write!(
f,
"[{}] Different number of variables: [{}] vs [{}]",
ctx,
a_vars.join(", "),
b_vars.join(", ")
)
} else {
write!(
f,
"Different number of variables: [{}] vs [{}]",
a_vars.join(", "),
b_vars.join(", ")
)
}
}
StructuralComparisonError::MissingVariable {
variable,
var_type,
context,
} => {
if let Some(ctx) = context {
write!(
f,
"[{}] Missing variable '{}' of type {:?} in message",
ctx, variable, var_type
)
} else {
write!(
f,
"Missing variable '{}' of type {:?} in message",
variable, var_type
)
}
}
}
}
}
impl std::error::Error for StructuralComparisonError {}
#[inline]
fn is_plural_or_select_element(el: &MessageFormatElement) -> bool {
matches!(
el,
MessageFormatElement::Plural(_) | MessageFormatElement::Select(_)
)
}
fn replace_pound_with_argument(
ast: &[MessageFormatElement],
variable_name: &str,
) -> Vec<MessageFormatElement> {
ast.iter()
.map(|el| match el {
MessageFormatElement::Pound(pound) => {
MessageFormatElement::Number(NumberElement {
value: variable_name.to_string(),
style: None,
location: pound.location.clone(),
})
}
MessageFormatElement::Plural(plural) => {
let options = plural
.options
.iter()
.map(|(key, option)| {
let value = replace_pound_with_argument(&option.value, variable_name);
(
key.clone(),
PluralOrSelectOption {
value,
location: option.location.clone(),
},
)
})
.collect();
MessageFormatElement::Plural(PluralElement {
value: plural.value.clone(),
options,
offset: plural.offset,
plural_type: plural.plural_type,
location: plural.location.clone(),
})
}
MessageFormatElement::Select(select) => {
let options = select
.options
.iter()
.map(|(key, option)| {
let value = replace_pound_with_argument(&option.value, variable_name);
(
key.clone(),
PluralOrSelectOption {
value,
location: option.location.clone(),
},
)
})
.collect();
MessageFormatElement::Select(SelectElement {
value: select.value.clone(),
options,
location: select.location.clone(),
})
}
MessageFormatElement::Tag(tag) => {
let children = replace_pound_with_argument(&tag.children, variable_name);
MessageFormatElement::Tag(TagElement {
value: tag.value.clone(),
children,
location: tag.location.clone(),
})
}
_ => el.clone(),
})
.collect()
}
fn find_plural_or_select_element(ast: &[MessageFormatElement]) -> bool {
ast.iter().any(|el| match el {
MessageFormatElement::Plural(_) | MessageFormatElement::Select(_) => true,
MessageFormatElement::Tag(tag) => find_plural_or_select_element(&tag.children),
_ => false,
})
}
fn hoist_element(
ast: &[MessageFormatElement],
el: &MessageFormatElement,
position: usize,
) -> MessageFormatElement {
let before = &ast[..position];
let after = &ast[position + 1..];
let has_subsequent_plural_or_select = after.iter().any(is_plural_or_select_element);
fn build_option_value(
before: &[MessageFormatElement],
option_value: &[MessageFormatElement],
after: &[MessageFormatElement],
) -> Vec<MessageFormatElement> {
let capacity = before.len() + option_value.len() + after.len();
let mut new_value = Vec::with_capacity(capacity);
new_value.extend_from_slice(before);
new_value.extend_from_slice(option_value);
new_value.extend_from_slice(after);
hoist_selectors_impl(new_value).unwrap_or_else(|e| {
let mut result = Vec::with_capacity(capacity);
result.extend_from_slice(before);
result.extend_from_slice(option_value);
result.extend_from_slice(after);
panic!("{}", e)
})
}
match el {
MessageFormatElement::Plural(plural) => {
let options = plural
.options
.iter()
.map(|(key, option)| {
let option_value = if has_subsequent_plural_or_select {
replace_pound_with_argument(&option.value, &plural.value)
} else {
option.value.clone()
};
let value = build_option_value(before, &option_value, after);
(
key.clone(),
PluralOrSelectOption {
value,
location: option.location.clone(),
},
)
})
.collect();
MessageFormatElement::Plural(PluralElement {
value: plural.value.clone(),
options,
offset: plural.offset,
plural_type: plural.plural_type,
location: plural.location.clone(),
})
}
MessageFormatElement::Select(select) => {
let options = select
.options
.iter()
.map(|(key, option)| {
let value = build_option_value(before, &option.value, after);
(
key.clone(),
PluralOrSelectOption {
value,
location: option.location.clone(),
},
)
})
.collect();
MessageFormatElement::Select(SelectElement {
value: select.value.clone(),
options,
location: select.location.clone(),
})
}
_ => unreachable!("Only plural or select elements should be passed to this function"),
}
}
fn hoist_selectors_impl(
ast: Vec<MessageFormatElement>,
) -> Result<Vec<MessageFormatElement>, ManipulatorError> {
for (i, el) in ast.iter().enumerate() {
if is_plural_or_select_element(el) {
return Ok(vec![hoist_element(&ast, el, i)]);
}
if matches!(el, MessageFormatElement::Tag(_))
&& find_plural_or_select_element(std::slice::from_ref(el))
{
return Err(ManipulatorError::PluralOrSelectInTag);
}
}
Ok(ast)
}
pub fn hoist_selectors(ast: Vec<MessageFormatElement>) -> Vec<MessageFormatElement> {
hoist_selectors_impl(ast).unwrap_or_else(|e| panic!("{}", e))
}
pub fn try_hoist_selectors(
ast: Vec<MessageFormatElement>,
) -> Result<Vec<MessageFormatElement>, ManipulatorError> {
hoist_selectors_impl(ast)
}
fn collect_variables(
ast: &[MessageFormatElement],
vars: &mut Vec<(String, Type)>,
) -> Result<(), ManipulatorError> {
for el in ast {
match el {
MessageFormatElement::Argument(arg) => {
vars.push((arg.value.clone(), Type::Argument));
}
MessageFormatElement::Number(num) => {
vars.push((num.value.clone(), Type::Number));
}
MessageFormatElement::Date(date) => {
vars.push((date.value.clone(), Type::Date));
}
MessageFormatElement::Time(time) => {
vars.push((time.value.clone(), Type::Time));
}
MessageFormatElement::Plural(plural) => {
vars.push((plural.value.clone(), Type::Plural));
for option in plural.options.values() {
collect_variables(&option.value, vars)?;
}
}
MessageFormatElement::Select(select) => {
vars.push((select.value.clone(), Type::Select));
for option in select.options.values() {
collect_variables(&option.value, vars)?;
}
}
MessageFormatElement::Tag(tag) => {
vars.push((tag.value.clone(), Type::Tag));
collect_variables(&tag.children, vars)?;
}
MessageFormatElement::Literal(_) | MessageFormatElement::Pound(_) => {}
}
}
Ok(())
}
pub fn is_structurally_same(
a: &[MessageFormatElement],
b: &[MessageFormatElement],
context: String,
) -> StructuralComparisonResult {
let mut a_vars = Vec::new();
let mut b_vars = Vec::new();
collect_variables(a, &mut a_vars).unwrap_or_else(|e| panic!("{}", e));
collect_variables(b, &mut b_vars).unwrap_or_else(|e| panic!("{}", e));
if a_vars.len() != b_vars.len() {
return Err(StructuralComparisonError::DifferentVariableCount {
a_vars: a_vars.iter().map(|f| f.0.clone()).collect(),
b_vars: b_vars.iter().map(|f| f.0.clone()).collect(),
context: Some(context),
});
}
for (key, a_type) in &a_vars {
if b_vars
.iter()
.find(|b| &b.0 == key && &b.1 == a_type)
.is_none()
{
return Err(StructuralComparisonError::MissingVariable {
variable: key.clone(),
var_type: a_type.clone(),
context: Some(context.clone()),
});
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use indexmap::IndexMap;
#[test]
fn test_hoist_simple_plural() {
let mut plural_options = IndexMap::new();
plural_options.insert(
ValidPluralRule::One,
PluralOrSelectOption {
value: vec![MessageFormatElement::Literal(LiteralElement::new(
"a dog".to_string(),
))],
location: None,
},
);
plural_options.insert(
ValidPluralRule::Other,
PluralOrSelectOption {
value: vec![MessageFormatElement::Literal(LiteralElement::new(
"many dogs".to_string(),
))],
location: None,
},
);
let ast = vec![
MessageFormatElement::Literal(LiteralElement::new("I have ".to_string())),
MessageFormatElement::Plural(PluralElement {
value: "count".to_string(),
options: plural_options,
offset: 0,
plural_type: PluralType::Cardinal,
location: None,
}),
];
let result = hoist_selectors(ast);
assert_eq!(result.len(), 1);
if let MessageFormatElement::Plural(plural) = &result[0] {
let one_value = &plural.options[&ValidPluralRule::One].value;
assert_eq!(one_value.len(), 2);
let other_value = &plural.options[&ValidPluralRule::Other].value;
assert_eq!(other_value.len(), 2);
} else {
panic!("Expected plural element at top level");
}
}
#[test]
fn test_collect_variables() {
let ast = vec![
MessageFormatElement::Argument(ArgumentElement::new("name".to_string())),
MessageFormatElement::Number(NumberElement {
value: "count".to_string(),
style: None,
location: None,
}),
MessageFormatElement::Date(DateElement {
value: "today".to_string(),
style: None,
location: None,
}),
];
let mut vars = Vec::new();
collect_variables(&ast, &mut vars).unwrap();
assert_eq!(vars.len(), 3);
assert_eq!(
vars,
Vec::from([
("name".to_string(), Type::Argument),
("count".to_string(), Type::Number),
("today".to_string(), Type::Date),
])
);
}
#[test]
fn test_is_structurally_same_success() {
let ast_a = vec![
MessageFormatElement::Literal(LiteralElement::new("Hello ".to_string())),
MessageFormatElement::Argument(ArgumentElement::new("name".to_string())),
];
let ast_b = vec![
MessageFormatElement::Literal(LiteralElement::new("Hola ".to_string())),
MessageFormatElement::Argument(ArgumentElement::new("name".to_string())),
];
let result = is_structurally_same(&ast_a, &ast_b, "test".to_string());
assert!(result.is_ok());
}
#[test]
fn test_is_structurally_same_includes_message_id() {
let ast_a = vec![
MessageFormatElement::Argument(ArgumentElement::new("name".to_string())),
MessageFormatElement::Argument(ArgumentElement::new("count".to_string())),
];
let ast_b = vec![MessageFormatElement::Argument(ArgumentElement::new(
"name".to_string(),
))];
let result = is_structurally_same(&ast_a, &ast_b, "app.welcome.message".to_string());
assert!(result.is_err());
let error_msg = result.unwrap_err().to_string();
assert!(
error_msg.contains("[app.welcome.message]"),
"Error message should include context: {}",
error_msg
);
assert!(error_msg.contains("Different number of variables"));
}
#[test]
fn test_is_structurally_same_missing_variable() {
let ast_a = vec![
MessageFormatElement::Argument(ArgumentElement::new("name".to_string())),
MessageFormatElement::Argument(ArgumentElement::new("count".to_string())),
];
let ast_b = vec![MessageFormatElement::Argument(ArgumentElement::new(
"name".to_string(),
))];
let result = is_structurally_same(&ast_a, &ast_b, "test".to_string());
assert!(result.is_err());
assert!(matches!(
result.unwrap_err(),
StructuralComparisonError::DifferentVariableCount {
context: Some(_),
..
}
));
}
#[test]
fn test_is_structurally_same_type_mismatch() {
let ast_a = vec![MessageFormatElement::Number(NumberElement {
value: "count".to_string(),
style: None,
location: None,
})];
let ast_b = vec![MessageFormatElement::Date(DateElement {
value: "count".to_string(),
style: None,
location: None,
})];
let result = is_structurally_same(&ast_a, &ast_b, "test".to_string());
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(
err,
StructuralComparisonError::MissingVariable {
context: Some(_),
..
}
),
"Expected MissingVariable error for type mismatch, got: {:?}",
err
);
}
#[test]
#[should_panic(expected = "Cannot hoist plural/select within a tag element")]
fn test_hoist_panics_on_plural_in_tag() {
let mut plural_options = IndexMap::new();
plural_options.insert(
ValidPluralRule::Other,
PluralOrSelectOption {
value: vec![MessageFormatElement::Literal(LiteralElement::new(
"text".to_string(),
))],
location: None,
},
);
let ast = vec![MessageFormatElement::Tag(TagElement {
value: "b".to_string(),
children: vec![MessageFormatElement::Plural(PluralElement {
value: "count".to_string(),
options: plural_options,
offset: 0,
plural_type: PluralType::Cardinal,
location: None,
})],
location: None,
})];
hoist_selectors(ast);
}
#[test]
fn test_is_structurally_same_with_plural_translations() {
use crate::parser::{Parser, ParserOptions};
let english = "I have {count} {count, plural, one{dog} other{dogs}}";
let spanish = "Tengo {count} {count, plural, one{perro} other{perros}}";
let parser_options = ParserOptions::default();
let english_ast = Parser::new(english, parser_options.clone())
.parse()
.expect("Failed to parse English message");
let spanish_ast = Parser::new(spanish, parser_options)
.parse()
.expect("Failed to parse Spanish message");
let result = is_structurally_same(&english_ast, &spanish_ast, "test.dogs".to_string());
assert!(
result.is_ok(),
"Expected messages to be structurally the same, but got error: {:?}",
result.err()
);
}
}