From df5e24738e327ae4059bd55f31c663fc403a3073 Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Tue, 9 Apr 2019 11:59:20 -0700 Subject: [PATCH] Refactor resolver errors to provide better fallbacking and return errors out of formatting (#93) * Refactor fluent-bundle * Add DisplayableNode * Format errors from variables * Add ResolverError::Reference * Migrate more errors to specific error types * Remove ResolverError::None * Add values_ref_test * Fast-path patterns with a single placeable * Rename Env to Scope * Add functions tests * More tests added * Address reviewers feedback and add a couple more tests --- fluent-bundle/benches/resolver.rs | 7 +- fluent-bundle/examples/external_arguments.rs | 2 +- fluent-bundle/examples/functions.rs | 12 +- fluent-bundle/examples/simple-app.rs | 2 +- fluent-bundle/src/bundle.rs | 133 ++++--- fluent-bundle/src/entry.rs | 4 +- fluent-bundle/src/resolve.rs | 348 ++++++++++-------- fluent-bundle/src/types.rs | 159 ++++++-- fluent-bundle/tests/bundle_test.rs | 68 ++++ fluent-bundle/tests/compound.rs | 33 +- .../tests/{bundle.rs => constructor_test.rs} | 0 fluent-bundle/tests/functions_runtime_test.rs | 50 +++ fluent-bundle/tests/functions_test.rs | 74 ++++ fluent-bundle/tests/helpers/mod.rs | 27 +- fluent-bundle/tests/primitives_test.rs | 102 +++++ .../tests/resolve_attribute_expression.rs | 66 +++- .../tests/resolve_external_argument.rs | 2 +- .../tests/resolve_message_reference.rs | 32 +- fluent-bundle/tests/resolve_plural_rule.rs | 4 +- .../tests/resolve_select_expression.rs | 51 ++- .../tests/resolve_variant_expression.rs | 13 +- fluent-bundle/tests/resource_test.rs | 10 + fluent-bundle/tests/select_expression_test.rs | 113 ++++++ fluent-bundle/tests/types_test.rs | 73 ++++ fluent-bundle/tests/values_format_test.rs | 55 +++ fluent-bundle/tests/values_ref_test.rs | 68 ++++ 26 files changed, 1205 insertions(+), 303 deletions(-) create mode 100644 fluent-bundle/tests/bundle_test.rs rename fluent-bundle/tests/{bundle.rs => constructor_test.rs} (100%) create mode 100644 fluent-bundle/tests/functions_runtime_test.rs create mode 100644 fluent-bundle/tests/functions_test.rs create mode 100644 fluent-bundle/tests/primitives_test.rs create mode 100644 fluent-bundle/tests/resource_test.rs create mode 100644 fluent-bundle/tests/select_expression_test.rs create mode 100644 fluent-bundle/tests/types_test.rs create mode 100644 fluent-bundle/tests/values_format_test.rs create mode 100644 fluent-bundle/tests/values_ref_test.rs diff --git a/fluent-bundle/benches/resolver.rs b/fluent-bundle/benches/resolver.rs index 9934c782..86f3cf6b 100644 --- a/fluent-bundle/benches/resolver.rs +++ b/fluent-bundle/benches/resolver.rs @@ -63,7 +63,7 @@ fn add_functions(name: &'static str, bundle: &mut FluentBundle) { "preferences" => { bundle .add_function("PLATFORM", |_args, _named_args| { - return Some("linux".into()); + return "linux".into(); }) .expect("Failed to add a function to the bundle."); } @@ -92,10 +92,7 @@ fn resolver_bench(c: &mut Criterion) { b.iter(|| { for id in &ids { let (_msg, errors) = bundle.compound(id, args.as_ref()).expect("Message found"); - if !errors.is_empty() { - println!("{:#?}", errors); - } - assert!(errors.len() == 0); + assert!(errors.len() == 0, "Resolver errors: {:#?}", errors); } }) }, diff --git a/fluent-bundle/examples/external_arguments.rs b/fluent-bundle/examples/external_arguments.rs index e40a7b37..144faaa8 100644 --- a/fluent-bundle/examples/external_arguments.rs +++ b/fluent-bundle/examples/external_arguments.rs @@ -33,7 +33,7 @@ unread-emails = } let mut args = HashMap::new(); - args.insert("emailCount", FluentValue::into_number("1.0").unwrap()); + args.insert("emailCount", FluentValue::into_number("1.0")); match bundle.format("unread-emails", Some(&args)) { Some((value, _)) => println!("{}", value), diff --git a/fluent-bundle/examples/functions.rs b/fluent-bundle/examples/functions.rs index 25fb7bab..827b89c5 100644 --- a/fluent-bundle/examples/functions.rs +++ b/fluent-bundle/examples/functions.rs @@ -15,7 +15,7 @@ fn main() { // Test for a simple function that returns a string bundle .add_function("HELLO", |_args, _named_args| { - return Some("I'm a function!".into()); + return "I'm a function!".into(); }) .expect("Failed to add a function to the bundle."); @@ -23,12 +23,12 @@ fn main() { bundle .add_function("MEANING_OF_LIFE", |args, _named_args| { if let Some(arg0) = args.get(0) { - if *arg0 == Some(FluentValue::Number(String::from("42"))) { - return Some("The answer to life, the universe, and everything".into()); + if *arg0 == FluentValue::Number("42".into()) { + return "The answer to life, the universe, and everything".into(); } } - None + FluentValue::None() }) .expect("Failed to add a function to the bundle."); @@ -37,9 +37,9 @@ fn main() { .add_function("BASE_OWNERSHIP", |_args, named_args| { return match named_args.get("ownership") { Some(FluentValue::String(ref string)) => { - Some(format!("All your base belong to {}", string).into()) + format!("All your base belong to {}", string).into() } - _ => None, + _ => FluentValue::None(), }; }) .expect("Failed to add a function to the bundle."); diff --git a/fluent-bundle/examples/simple-app.rs b/fluent-bundle/examples/simple-app.rs index 0f09ffe7..145393a6 100644 --- a/fluent-bundle/examples/simple-app.rs +++ b/fluent-bundle/examples/simple-app.rs @@ -147,7 +147,7 @@ fn main() { } Err(err) => { let mut args = HashMap::new(); - args.insert("input", FluentValue::from(input.to_string())); + args.insert("input", FluentValue::from(input.as_str())); args.insert("reason", FluentValue::from(err.to_string())); let (value, _) = bundle .format("input-parse-error-msg", Some(&args)) diff --git a/fluent-bundle/src/bundle.rs b/fluent-bundle/src/bundle.rs index 7d9101b7..baf61b5a 100644 --- a/fluent-bundle/src/bundle.rs +++ b/fluent-bundle/src/bundle.rs @@ -4,21 +4,23 @@ //! internationalization formatters, functions, environmental variables and are expected to be used //! together. +use std::borrow::Cow; use std::collections::hash_map::{Entry as HashEntry, HashMap}; use super::entry::{Entry, GetEntry}; pub use super::errors::FluentError; -use super::resolve::{Env, ResolveValue}; +use super::resolve::{resolve_value_for_entry, Scope}; use super::resource::FluentResource; +use super::types::DisplayableNode; use super::types::FluentValue; use fluent_locale::{negotiate_languages, NegotiationStrategy}; use fluent_syntax::ast; use intl_pluralrules::{IntlPluralRules, PluralRuleType}; #[derive(Debug, PartialEq)] -pub struct Message { - pub value: Option, - pub attributes: HashMap, +pub struct Message<'m> { + pub value: Option>, + pub attributes: HashMap<&'m str, Cow<'m, str>>, } /// A collection of localization messages for a single locale, which are meant @@ -165,8 +167,8 @@ impl<'bundle> FluentBundle<'bundle> { /// /// // Register a fn that maps from string to string length /// bundle.add_function("STRLEN", |positional, _named| match positional { - /// [Some(FluentValue::String(str))] => Some(FluentValue::Number(str.len().to_string())), - /// _ => None, + /// [FluentValue::String(str)] => FluentValue::Number(str.len().to_string().into()), + /// _ => FluentValue::None(), /// }).expect("Failed to add a function to the bundle."); /// /// let (value, _) = bundle.format("length", None) @@ -178,7 +180,7 @@ impl<'bundle> FluentBundle<'bundle> { pub fn add_function(&mut self, id: &str, func: F) -> Result<(), FluentError> where F: 'bundle - + Fn(&[Option], &HashMap<&str, FluentValue>) -> Option + + for<'a> Fn(&[FluentValue<'a>], &HashMap<&str, FluentValue<'a>>) -> FluentValue<'a> + Sync + Send, { @@ -316,7 +318,7 @@ impl<'bundle> FluentBundle<'bundle> { /// /// In all other cases `format` returns a string even if it /// encountered errors. Generally, during partial errors `format` will - /// use `'___'` to replace parts of the formatted message that it could + /// use ids to replace parts of the formatted message that it could /// not successfuly build. For more fundamental errors `format` will return /// the path itself as the translation. /// @@ -335,56 +337,55 @@ impl<'bundle> FluentBundle<'bundle> { /// bundle.add_resource(&resource) /// .expect("Failed to add FTL resources to the bundle."); /// - /// // The result falls back to "___" + /// // The result falls back to "a foo b" /// let (value, _) = bundle.format("foo", None) /// .expect("Failed to format a message."); - /// assert_eq!(&value, "___"); + /// assert_eq!(&value, "a foo b"); /// ``` pub fn format( - &self, + &'bundle self, path: &str, - args: Option<&HashMap<&str, FluentValue>>, - ) -> Option<(String, Vec)> { - let env = Env::new(self, args); + args: Option<&'bundle HashMap<&str, FluentValue>>, + ) -> Option<(Cow<'bundle, str>, Vec)> { + let mut env = Scope::new(self, args); let mut errors = vec![]; - if let Some(ptr_pos) = path.find('.') { + let string = if let Some(ptr_pos) = path.find('.') { let message_id = &path[..ptr_pos]; let message = self.entries.get_message(message_id)?; let attr_name = &path[(ptr_pos + 1)..]; - for attribute in message.attributes.iter() { - if attribute.id.name == attr_name { - match attribute.to_value(&env) { - Ok(val) => { - return Some((val.format(self), errors)); - } - Err(err) => { - errors.push(FluentError::ResolverError(err)); - // XXX: In the future we'll want to get the partial - // value out of resolver and return it here. - // We also expect to get a Vec or errors out of resolver. - return Some((path.to_string(), errors)); - } - } - } - } + let attr = message + .attributes + .iter() + .find(|attr| attr.id.name == attr_name)?; + resolve_value_for_entry( + &attr.value, + DisplayableNode::new(message.id.name, Some(attr.id.name)), + &mut env, + ) + .to_string() } else { let message_id = path; let message = self.entries.get_message(message_id)?; - match message.to_value(&env) { - Ok(val) => { - let s = val.format(self); - return Some((s, errors)); - } - Err(err) => { - errors.push(FluentError::ResolverError(err)); - return Some((message_id.to_string(), errors)); - } - } + message + .value + .as_ref() + .map(|value| { + resolve_value_for_entry( + value, + DisplayableNode::new(message.id.name, None), + &mut env, + ) + })? + .to_string() + }; + + for err in env.errors { + errors.push(err.into()); } - None + Some((string, errors)) } /// Formats both the message value and attributes identified by `message_id` @@ -412,8 +413,8 @@ impl<'bundle> FluentBundle<'bundle> { /// /// let (message, _) = bundle.compound("login-input", None) /// .expect("Failed to format a message."); - /// assert_eq!(message.value, Some("Predefined value".to_string())); - /// assert_eq!(message.attributes.get("title"), Some(&"Type your login email".to_string())); + /// assert_eq!(message.value, Some("Predefined value".into())); + /// assert_eq!(message.attributes.get("title"), Some(&"Type your login email".into())); /// ``` /// /// # Errors @@ -422,7 +423,7 @@ impl<'bundle> FluentBundle<'bundle> { /// /// In all other cases `compound` returns a message even if it /// encountered errors. Generally, during partial errors `compound` will - /// use `'___'` to replace parts of the formatted message that it could + /// use ids to replace parts of the formatted message that it could /// not successfuly build. For more fundamental errors `compound` will return /// the path itself as the translation. /// @@ -430,38 +431,32 @@ impl<'bundle> FluentBundle<'bundle> { /// gathered during formatting. A caller may safely ignore the extra errors /// if the fallback formatting policies are acceptable. pub fn compound( - &self, + &'bundle self, message_id: &str, - args: Option<&HashMap<&str, FluentValue>>, - ) -> Option<(Message, Vec)> { + args: Option<&'bundle HashMap<&str, FluentValue>>, + ) -> Option<(Message<'bundle>, Vec)> { + let mut env = Scope::new(self, args); let mut errors = vec![]; - - let env = Env::new(self, args); let message = self.entries.get_message(message_id)?; - let value = message - .value - .as_ref() - .and_then(|value| match value.to_value(&env) { - Ok(value) => Some(value.format(self)), - Err(err) => { - errors.push(FluentError::ResolverError(err)); - None - } - }); + let value = message.value.as_ref().map(|value| { + resolve_value_for_entry(value, DisplayableNode::new(message.id.name, None), &mut env) + .to_string() + }); let mut attributes = HashMap::new(); for attr in message.attributes.iter() { - match attr.to_value(&env) { - Ok(value) => { - let val = value.format(self); - attributes.insert(attr.id.name.to_owned(), val); - } - Err(err) => { - errors.push(FluentError::ResolverError(err)); - } - } + let val = resolve_value_for_entry( + &attr.value, + DisplayableNode::new(message.id.name, Some(attr.id.name)), + &mut env, + ); + attributes.insert(attr.id.name, val.to_string()); + } + + for err in env.errors { + errors.push(err.into()); } Some((Message { value, attributes }, errors)) diff --git a/fluent-bundle/src/entry.rs b/fluent-bundle/src/entry.rs index 0a08c469..76093495 100644 --- a/fluent-bundle/src/entry.rs +++ b/fluent-bundle/src/entry.rs @@ -1,12 +1,12 @@ //! `Entry` is used to store Messages, Terms and Functions in `FluentBundle` instances. use std::collections::hash_map::HashMap; -use super::types::FluentValue; +use super::types::*; use fluent_syntax::ast; type FluentFunction<'bundle> = Box< 'bundle - + Fn(&[Option], &HashMap<&str, FluentValue>) -> Option + + for<'a> Fn(&[FluentValue<'a>], &HashMap<&str, FluentValue<'a>>) -> FluentValue<'a> + Send + Sync, >; diff --git a/fluent-bundle/src/resolve.rs b/fluent-bundle/src/resolve.rs index b77f1aa0..d743465d 100644 --- a/fluent-bundle/src/resolve.rs +++ b/fluent-bundle/src/resolve.rs @@ -13,271 +13,299 @@ use std::hash::{Hash, Hasher}; use super::bundle::FluentBundle; use super::entry::GetEntry; +use super::types::DisplayableNode; use super::types::FluentValue; use fluent_syntax::ast; use fluent_syntax::unicode::unescape_unicode; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum ResolverError { - None, + Reference(String), + MissingDefault, + Argument(String), Value, Cyclic, } /// State for a single `ResolveValue::to_value` call. -pub struct Env<'env> { +pub struct Scope<'bundle> { /// The current `FluentBundle` instance. - pub bundle: &'env FluentBundle<'env>, + pub bundle: &'bundle FluentBundle<'bundle>, /// The current arguments passed by the developer. - pub args: Option<&'env HashMap<&'env str, FluentValue>>, + pub args: Option<&'bundle HashMap<&'bundle str, FluentValue<'bundle>>>, + /// Local args + pub local_args: Option>>, /// Tracks hashes to prevent infinite recursion. pub travelled: RefCell>, + /// Track errors accumulated during resolving. + pub errors: Vec, } -impl<'env> Env<'env> { +impl<'bundle> Scope<'bundle> { pub fn new( - bundle: &'env FluentBundle, - args: Option<&'env HashMap<&'env str, FluentValue>>, + bundle: &'bundle FluentBundle<'bundle>, + args: Option<&'bundle HashMap<&str, FluentValue>>, ) -> Self { - Env { + Scope { bundle, args, + local_args: None, travelled: RefCell::new(Vec::new()), + errors: vec![], } } - fn track(&self, identifier: &str, action: F) -> Result + pub fn track( + &mut self, + entry: DisplayableNode<'bundle>, + mut action: F, + ) -> FluentValue<'bundle> where - F: FnMut() -> Result, + F: FnMut(&mut Scope<'bundle>) -> FluentValue<'bundle>, { let mut hasher = DefaultHasher::new(); - identifier.hash(&mut hasher); + (entry.id, entry.attribute).hash(&mut hasher); + entry.id.hash(&mut hasher); + if let Some(attr) = entry.attribute { + attr.hash(&mut hasher); + } let hash = hasher.finish(); if self.travelled.borrow().contains(&hash) { - Err(ResolverError::Cyclic) + self.errors.push(ResolverError::Cyclic); + FluentValue::Error(entry) } else { self.travelled.borrow_mut().push(hash); - self.scope(action) + let result = action(self); + self.travelled.borrow_mut().pop(); + result } } - fn scope T>(&self, mut action: F) -> T { - let level = self.travelled.borrow().len(); - let output = action(); - self.travelled.borrow_mut().truncate(level); - output + fn maybe_resolve_attribute( + &mut self, + attributes: &[ast::Attribute<'bundle>], + entry: DisplayableNode<'bundle>, + name: &str, + ) -> Option> { + attributes + .iter() + .find(|attr| attr.id.name == name) + .map(|attr| self.track(entry, |env| attr.value.resolve(env))) } } -/// Converts an AST node to a `FluentValue`. -pub trait ResolveValue { - fn to_value(&self, env: &Env) -> Result; +// Converts an AST node to a `FluentValue`. +pub trait ResolveValue<'source> { + fn resolve(&self, env: &mut Scope<'source>) -> FluentValue<'source>; } -impl<'source> ResolveValue for ast::Message<'source> { - fn to_value(&self, env: &Env) -> Result { - env.track(&self.id.name, || { - self.value - .as_ref() - .ok_or(ResolverError::None)? - .to_value(env) - }) - } +fn generate_ref_error<'source>( + env: &mut Scope<'source>, + node: DisplayableNode<'source>, +) -> FluentValue<'source> { + env.errors.push(ResolverError::Reference(node.get_error())); + FluentValue::Error(node) } -impl<'source> ResolveValue for ast::Term<'source> { - fn to_value(&self, env: &Env) -> Result { - env.track(&self.id.name, || self.value.to_value(env)) +impl<'source> ResolveValue<'source> for ast::Term<'source> { + fn resolve(&self, env: &mut Scope<'source>) -> FluentValue<'source> { + resolve_value_for_entry(&self.value, self.into(), env) } } -impl<'source> ResolveValue for ast::Attribute<'source> { - fn to_value(&self, env: &Env) -> Result { - env.track(&self.id.name, || self.value.to_value(env)) +pub fn resolve_value_for_entry<'source>( + value: &ast::Pattern<'source>, + entry: DisplayableNode<'source>, + env: &mut Scope<'source>, +) -> FluentValue<'source> { + if value.elements.len() == 1 { + return match value.elements[0] { + ast::PatternElement::TextElement(s) => FluentValue::String(s.into()), + ast::PatternElement::Placeable(ref p) => env.track(entry.clone(), |env| p.resolve(env)), + }; } -} -impl<'source> ResolveValue for ast::Pattern<'source> { - fn to_value(&self, env: &Env) -> Result { - let mut string = String::with_capacity(128); - for elem in &self.elements { - let result: Result = env.scope(|| match elem.to_value(env) { - Err(ResolverError::Cyclic) => Err(()), - Err(_) => Ok("___".into()), - Ok(elem) => Ok(elem.format(env.bundle)), - }); - - match result { - Err(()) => return Ok("___".into()), - Ok(value) => { - string.push_str(&value); - } + let mut string = String::new(); + for elem in &value.elements { + match elem { + ast::PatternElement::TextElement(s) => { + string.push_str(&s); + } + ast::PatternElement::Placeable(p) => { + let result = env.track(entry.clone(), |env| p.resolve(env)); + string.push_str(&result.to_string()); } } - string.shrink_to_fit(); - Ok(FluentValue::from(string)) } + FluentValue::String(string.into()) } -impl<'source> ResolveValue for ast::PatternElement<'source> { - fn to_value(&self, env: &Env) -> Result { - match self { - ast::PatternElement::TextElement(s) => Ok(FluentValue::from(*s)), - ast::PatternElement::Placeable(p) => p.to_value(env), +impl<'source> ResolveValue<'source> for ast::Pattern<'source> { + fn resolve(&self, env: &mut Scope<'source>) -> FluentValue<'source> { + if self.elements.len() == 1 { + return match self.elements[0] { + ast::PatternElement::TextElement(s) => FluentValue::String(s.into()), + ast::PatternElement::Placeable(ref p) => p.resolve(env), + }; } - } -} -impl<'source> ResolveValue for ast::VariantKey<'source> { - fn to_value(&self, _env: &Env) -> Result { - match self { - ast::VariantKey::Identifier { name } => Ok(FluentValue::from(*name)), - ast::VariantKey::NumberLiteral { value } => { - FluentValue::into_number(value).map_err(|_| ResolverError::Value) + let mut string = String::new(); + for elem in &self.elements { + match elem { + ast::PatternElement::TextElement(s) => { + string.push_str(&s); + } + ast::PatternElement::Placeable(p) => { + let result = p.resolve(env).to_string(); + string.push_str(&result); + } } } + FluentValue::String(string.into()) } } -impl<'source> ResolveValue for ast::Expression<'source> { - fn to_value(&self, env: &Env) -> Result { +impl<'source> ResolveValue<'source> for ast::Expression<'source> { + fn resolve(&self, env: &mut Scope<'source>) -> FluentValue<'source> { match self { - ast::Expression::InlineExpression(exp) => exp.to_value(env), - ast::Expression::SelectExpression { selector, variants } => { - if let Ok(ref selector) = selector.to_value(env) { - for variant in variants { - match variant.key { - ast::VariantKey::Identifier { name } => { - let key = FluentValue::from(name); - if key.matches(env.bundle, selector) { - return variant.value.to_value(env); + ast::Expression::InlineExpression(exp) => exp.resolve(env), + ast::Expression::SelectExpression { + selector, + ref variants, + } => { + let selector = selector.resolve(env); + match selector { + FluentValue::String(_) | FluentValue::Number(_) => { + for variant in variants { + match variant.key { + ast::VariantKey::Identifier { name } => { + let key = FluentValue::String(name.into()); + if key.matches(&selector, &env) { + return variant.value.resolve(env); + } } - } - ast::VariantKey::NumberLiteral { value } => { - if let Ok(key) = FluentValue::into_number(value) { - if key.matches(env.bundle, selector) { - return variant.value.to_value(env); + ast::VariantKey::NumberLiteral { value } => { + let key = FluentValue::into_number(value); + if key.matches(&selector, &env) { + return variant.value.resolve(env); } - } else { - return Err(ResolverError::Value); } } } } + _ => {} } - select_default(variants) - .ok_or(ResolverError::None)? - .value - .to_value(env) + for variant in variants { + if variant.default { + return variant.value.resolve(env); + } + } + env.errors.push(ResolverError::MissingDefault); + FluentValue::None() } } } } -impl<'source> ResolveValue for ast::InlineExpression<'source> { - fn to_value(&self, env: &Env) -> Result { +impl<'source> ResolveValue<'source> for ast::InlineExpression<'source> { + fn resolve(&self, mut env: &mut Scope<'source>) -> FluentValue<'source> { match self { ast::InlineExpression::StringLiteral { value } => { - Ok(FluentValue::from(unescape_unicode(value).into_owned())) - } - ast::InlineExpression::NumberLiteral { value } => { - FluentValue::into_number(*value).map_err(|_| ResolverError::None) - } - ast::InlineExpression::FunctionReference { id, arguments } => { - let (resolved_positional_args, resolved_named_args) = get_arguments(env, arguments); - - let func = env.bundle.entries.get_function(id.name); - - func.ok_or(ResolverError::None).and_then(|func| { - func(resolved_positional_args.as_slice(), &resolved_named_args) - .ok_or(ResolverError::None) - }) + FluentValue::String(unescape_unicode(value)) } ast::InlineExpression::MessageReference { id, attribute } => { - let msg = env - .bundle - .entries - .get_message(&id.name) - .ok_or(ResolverError::None)?; - if let Some(attribute) = attribute { - for attr in msg.attributes.iter() { - if attr.id.name == attribute.name { - return attr.to_value(env); - } + let msg = env.bundle.entries.get_message(&id.name); + + if let Some(msg) = msg { + if let Some(attr) = attribute { + env.maybe_resolve_attribute(&msg.attributes, self.into(), attr.name) + .unwrap_or_else(|| generate_ref_error(env, self.into())) + } else if let Some(value) = msg.value.as_ref() { + env.track(self.into(), |env| value.resolve(env)) + } else { + generate_ref_error(env, self.into()) } - Err(ResolverError::None) } else { - msg.to_value(env) + generate_ref_error(env, self.into()) } } + ast::InlineExpression::NumberLiteral { value } => FluentValue::into_number(*value), ast::InlineExpression::TermReference { id, attribute, arguments, } => { - let term = env - .bundle - .entries - .get_term(&id.name) - .ok_or(ResolverError::None)?; + let term = env.bundle.entries.get_term(&id.name); - let (.., resolved_named_args) = get_arguments(env, arguments); - let env = Env::new(env.bundle, Some(&resolved_named_args)); + let (_, resolved_named_args) = get_arguments(env, arguments); - if let Some(attribute) = attribute { - for attr in term.attributes.iter() { - if attr.id.name == attribute.name { - return attr.to_value(&env); - } + env.local_args = Some(resolved_named_args); + + let value = if let Some(term) = term { + if let Some(attr) = attribute { + env.maybe_resolve_attribute(&term.attributes, self.into(), attr.name) + .unwrap_or_else(|| generate_ref_error(env, self.into())) + } else { + term.resolve(&mut env) } - Err(ResolverError::None) } else { - term.to_value(&env) + generate_ref_error(env, self.into()) + }; + env.local_args = None; + value + } + ast::InlineExpression::FunctionReference { id, arguments } => { + let (resolved_positional_args, resolved_named_args) = get_arguments(env, arguments); + + let func = env.bundle.entries.get_function(id.name); + + if let Some(func) = func { + func(resolved_positional_args.as_slice(), &resolved_named_args) + } else { + generate_ref_error(env, self.into()) } } - ast::InlineExpression::VariableReference { id } => env - .args - .and_then(|args| args.get(&id.name)) - .cloned() - .ok_or(ResolverError::None), - ast::InlineExpression::Placeable { ref expression } => { - let exp = expression.as_ref(); - exp.to_value(env) + ast::InlineExpression::VariableReference { id } => { + let arg = if let Some(args) = &env.local_args { + args.get(&id.name) + } else { + env.args.and_then(|args| args.get(&id.name)) + }; + if let Some(arg) = arg { + arg.clone() + } else { + let displayable_node: DisplayableNode = self.into(); + if env.local_args.is_none() { + env.errors + .push(ResolverError::Reference(displayable_node.get_error())); + } + FluentValue::Error(displayable_node) + } } + ast::InlineExpression::Placeable { expression } => expression.resolve(env), } } } -fn select_default<'source>( - variants: &'source [ast::Variant<'source>], -) -> Option<&ast::Variant<'source>> { - for variant in variants { - if variant.default { - return Some(variant); - } - } - - None -} - -fn get_arguments<'env>( - env: &Env, - arguments: &'env Option, -) -> (Vec>, HashMap<&'env str, FluentValue>) { +fn get_arguments<'bundle>( + env: &mut Scope<'bundle>, + arguments: &Option>, +) -> ( + Vec>, + HashMap<&'bundle str, FluentValue<'bundle>>, +) { let mut resolved_positional_args = Vec::new(); let mut resolved_named_args = HashMap::new(); if let Some(ast::CallArguments { named, positional }) = arguments { for expression in positional { - resolved_positional_args.push(expression.to_value(env).ok()); + resolved_positional_args.push(expression.resolve(env)); } for arg in named { - if let Ok(arg_value) = arg.value.to_value(env) { - resolved_named_args.insert(arg.name.name, arg_value); - } + resolved_named_args.insert(arg.name.name, arg.value.resolve(env)); } } diff --git a/fluent-bundle/src/types.rs b/fluent-bundle/src/types.rs index 753c869b..186bf6ac 100644 --- a/fluent-bundle/src/types.rs +++ b/fluent-bundle/src/types.rs @@ -11,41 +11,126 @@ //! [`resolve`]: ../resolve //! [`FluentBundle::format`]: ../bundle/struct.FluentBundle.html#method.format -use std::f32; -use std::num::ParseFloatError; +use std::borrow::Cow; use std::str::FromStr; use intl_pluralrules::PluralCategory; -use super::bundle::FluentBundle; +use super::resolve::Scope; +use fluent_syntax::ast; -/// Value types which can be formatted to a String. -#[derive(Clone, Debug, PartialEq)] -pub enum FluentValue { - /// Fluent String type. - String(String), - /// Fluent Number type. - Number(String), +#[derive(Debug, PartialEq, Clone)] +pub enum DisplayableNodeType { + Message, + Term, + Variable, + Function, } -impl FluentValue { - pub fn into_number(v: S) -> Result { - f64::from_str(&v.to_string()).map(|_| FluentValue::Number(v.to_string())) +#[derive(Debug, PartialEq, Clone)] +pub struct DisplayableNode<'source> { + pub node_type: DisplayableNodeType, + pub id: &'source str, + pub attribute: Option<&'source str>, +} + +impl<'source> DisplayableNode<'source> { + pub fn display(&self) -> String { + let mut id = self.id.to_owned(); + if let Some(attr) = self.attribute { + id.push_str("."); + id.push_str(attr); + } + match self.node_type { + DisplayableNodeType::Message => id, + DisplayableNodeType::Term => format!("-{}", id), + DisplayableNodeType::Variable => format!("${}", id), + DisplayableNodeType::Function => format!("{}()", id), + } } - pub fn format(&self, _bundle: &FluentBundle) -> String { - match self { - FluentValue::String(s) => s.clone(), - FluentValue::Number(n) => n.clone(), + pub fn get_error(&self) -> String { + let mut id = match self.node_type { + DisplayableNodeType::Message => String::from("Unknown message: "), + DisplayableNodeType::Term => String::from("Unknown term: "), + DisplayableNodeType::Variable => String::from("Unknown variable: "), + DisplayableNodeType::Function => String::from("Unknown function: "), + }; + id.push_str(&self.display()); + id + } + + pub fn new(id: &'source str, attribute: Option<&'source str>) -> Self { + DisplayableNode { + node_type: DisplayableNodeType::Message, + id, + attribute, + } + } +} + +impl<'source> From<&ast::Term<'source>> for DisplayableNode<'source> { + fn from(term: &ast::Term<'source>) -> Self { + DisplayableNode { + node_type: DisplayableNodeType::Term, + id: term.id.name, + attribute: None, + } + } +} + +impl<'source> From<&ast::InlineExpression<'source>> for DisplayableNode<'source> { + fn from(expr: &ast::InlineExpression<'source>) -> Self { + match expr { + ast::InlineExpression::MessageReference { id, ref attribute } => DisplayableNode { + node_type: DisplayableNodeType::Message, + id: id.name, + attribute: attribute.as_ref().map(|attr| attr.name), + }, + ast::InlineExpression::TermReference { + id, ref attribute, .. + } => DisplayableNode { + node_type: DisplayableNodeType::Term, + id: id.name, + attribute: attribute.as_ref().map(|attr| attr.name), + }, + ast::InlineExpression::VariableReference { id } => DisplayableNode { + node_type: DisplayableNodeType::Variable, + id: id.name, + attribute: None, + }, + ast::InlineExpression::FunctionReference { id, .. } => DisplayableNode { + node_type: DisplayableNodeType::Function, + id: id.name, + attribute: None, + }, + _ => unimplemented!(), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum FluentValue<'source> { + String(Cow<'source, str>), + Number(Cow<'source, str>), + None(), + Error(DisplayableNode<'source>), +} + +impl<'source> FluentValue<'source> { + pub fn into_number(v: S) -> Self { + match f64::from_str(&v.to_string()) { + Ok(_) => FluentValue::Number(v.to_string().into()), + Err(_) => FluentValue::String(v.to_string().into()), } } - pub fn matches(&self, bundle: &FluentBundle, other: &FluentValue) -> bool { + pub fn matches(&self, other: &FluentValue, env: &Scope) -> bool { match (self, other) { (&FluentValue::String(ref a), &FluentValue::String(ref b)) => a == b, (&FluentValue::Number(ref a), &FluentValue::Number(ref b)) => a == b, (&FluentValue::String(ref a), &FluentValue::Number(ref b)) => { - let cat = match a.as_str() { + let cat = match a.as_ref() { "zero" => PluralCategory::ZERO, "one" => PluralCategory::ONE, "two" => PluralCategory::TWO, @@ -54,35 +139,43 @@ impl FluentValue { "other" => PluralCategory::OTHER, _ => return false, }; - - let pr = &bundle.plural_rules; - pr.select(b.as_str()) == Ok(cat) + let pr = &env.bundle.plural_rules; + pr.select(b.as_ref()) == Ok(cat) } - (&FluentValue::Number(..), &FluentValue::String(..)) => false, + _ => false, + } + } + + pub fn to_string(&self) -> Cow<'source, str> { + match self { + FluentValue::String(s) => s.clone(), + FluentValue::Number(n) => n.clone(), + FluentValue::Error(d) => d.display().into(), + FluentValue::None() => String::from("???").into(), } } } -impl From for FluentValue { +impl<'source> From for FluentValue<'source> { fn from(s: String) -> Self { - FluentValue::String(s) + FluentValue::String(s.into()) } } -impl<'a> From<&'a str> for FluentValue { - fn from(s: &'a str) -> Self { - FluentValue::String(String::from(s)) +impl<'source> From<&'source str> for FluentValue<'source> { + fn from(s: &'source str) -> Self { + FluentValue::String(s.into()) } } -impl From for FluentValue { - fn from(n: f32) -> Self { - FluentValue::Number(n.to_string()) +impl<'source> From for FluentValue<'source> { + fn from(n: f64) -> Self { + FluentValue::Number(n.to_string().into()) } } -impl From for FluentValue { +impl<'source> From for FluentValue<'source> { fn from(n: isize) -> Self { - FluentValue::Number(n.to_string()) + FluentValue::Number(n.to_string().into()) } } diff --git a/fluent-bundle/tests/bundle_test.rs b/fluent-bundle/tests/bundle_test.rs new file mode 100644 index 00000000..eb2e4c27 --- /dev/null +++ b/fluent-bundle/tests/bundle_test.rs @@ -0,0 +1,68 @@ +mod helpers; + +use self::helpers::{ + assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, +}; +use fluent_bundle::entry::GetEntry; +use fluent_bundle::errors::FluentError; + +// XXX: We skip addMessages because we +// don't support that API in Rust. + +#[test] +fn bundle_add_resource() { + let res = assert_get_resource_from_str_no_errors( + " +foo = Foo +-bar = Bar + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert!(bundle.entries.get_message("foo").is_some()); + assert!(bundle.entries.get_term("foo").is_none()); + assert!(bundle.entries.get_message("bar").is_none()); + assert!(bundle.entries.get_term("bar").is_some()); +} + +#[test] +fn bundle_allow_overrides_false() { + let res = assert_get_resource_from_str_no_errors( + " +key = Foo + ", + ); + let res2 = assert_get_resource_from_str_no_errors( + " +key = Bar + ", + ); + let mut bundle = assert_get_bundle_no_errors(&res, None); + assert_eq!( + bundle.add_resource(&res2), + Err(vec![FluentError::Overriding { + kind: "message".into(), + id: "key".into() + }]) + ); + + assert_format_no_errors(bundle.format("key", None), "Foo"); +} + +#[test] +fn bundle_has_message() { + let res = assert_get_resource_from_str_no_errors( + " +foo = Foo +bar = + .attr = Bar Attr +-term = Term + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_eq!(bundle.has_message("foo"), true); + assert_eq!(bundle.has_message("-term"), false); + assert_eq!(bundle.has_message("missing"), false); + assert_eq!(bundle.has_message("-missing"), false); +} diff --git a/fluent-bundle/tests/compound.rs b/fluent-bundle/tests/compound.rs index 7b40a786..318e1c86 100644 --- a/fluent-bundle/tests/compound.rs +++ b/fluent-bundle/tests/compound.rs @@ -1,7 +1,10 @@ mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; use self::helpers::{ - assert_compound_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, + assert_compound, assert_compound_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, }; use fluent_bundle::bundle::Message; use std::collections::HashMap; @@ -18,14 +21,36 @@ foo = Foo let bundle = assert_get_bundle_no_errors(&res, Some("en")); let mut attrs = HashMap::new(); - attrs.insert("attr".to_string(), "Attribute".to_string()); - attrs.insert("attr2".to_string(), "Attribute 2".to_string()); + attrs.insert("attr".into(), "Attribute".into()); + attrs.insert("attr2".into(), "Attribute 2".into()); assert_compound_no_errors( bundle.compound("foo", None), Message { - value: Some("Foo".to_string()), + value: Some("Foo".into()), attributes: attrs, }, ); } + +#[test] +fn message_reference_cyclic() { + { + let res = assert_get_resource_from_str_no_errors( + " +foo = Foo { bar } +bar = { foo } Bar + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_compound( + bundle.compound("foo", None), + Message { + value: Some("Foo foo Bar".into()), + attributes: HashMap::new(), + }, + vec![FluentError::ResolverError(ResolverError::Cyclic)], + ); + } +} diff --git a/fluent-bundle/tests/bundle.rs b/fluent-bundle/tests/constructor_test.rs similarity index 100% rename from fluent-bundle/tests/bundle.rs rename to fluent-bundle/tests/constructor_test.rs diff --git a/fluent-bundle/tests/functions_runtime_test.rs b/fluent-bundle/tests/functions_runtime_test.rs new file mode 100644 index 00000000..ded800a7 --- /dev/null +++ b/fluent-bundle/tests/functions_runtime_test.rs @@ -0,0 +1,50 @@ +mod helpers; +use fluent_bundle::FluentValue; + +use self::helpers::{ + assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, +}; + +#[test] +fn functions_runtime_passing_into_the_constructor() { + let res = assert_get_resource_from_str_no_errors( + r#" +foo = { CONCAT("Foo", "Bar") } +bar = { SUM(1, 2) } + "#, + ); + let mut bundle = assert_get_bundle_no_errors(&res, None); + bundle + .add_function("CONCAT", |args, _named_args| { + let mut result = String::new(); + for arg in args { + match arg { + FluentValue::String(s) => { + result.push_str(s); + } + _ => unimplemented!(), + } + } + FluentValue::String(result.into()) + }) + .expect("Failed to add a function to the bundle."); + bundle + .add_function("SUM", |args, _named_args| { + let mut result: isize = 0; + for arg in args { + match arg { + FluentValue::Number(n) => { + let part: isize = n.parse().unwrap(); + result += part; + } + _ => unimplemented!(), + } + } + FluentValue::Number(result.to_string().into()) + }) + .expect("Failed to add a function to the bundle."); + + assert_format_no_errors(bundle.format("foo", None), "FooBar"); + + assert_format_no_errors(bundle.format("bar", None), "3"); +} diff --git a/fluent-bundle/tests/functions_test.rs b/fluent-bundle/tests/functions_test.rs new file mode 100644 index 00000000..256d7515 --- /dev/null +++ b/fluent-bundle/tests/functions_test.rs @@ -0,0 +1,74 @@ +mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; +use fluent_bundle::FluentValue; +use std::collections::HashMap; + +use self::helpers::{ + assert_format, assert_format_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, +}; + +#[test] +fn functions_missing() { + let res = assert_get_resource_from_str_no_errors( + r#" +foo = { MISSING("Foo") } + "#, + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format( + bundle.format("foo", None), + "MISSING()", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown function: MISSING()".into(), + ))], + ); +} + +#[test] +fn functions_arguments() { + let res = assert_get_resource_from_str_no_errors( + r#" +foo = Foo + .attr = Attribute +pass-nothing = { IDENTITY() } +pass-string = { IDENTITY("a") } +pass-number = { IDENTITY(1) } +pass-message = { IDENTITY(foo) } +pass-attr = { IDENTITY(foo.attr) } +pass-variable = { IDENTITY($var) } +pass-function-call = { IDENTITY(IDENTITY(1)) } + "#, + ); + let mut bundle = assert_get_bundle_no_errors(&res, None); + bundle + .add_function("IDENTITY", |args, _named_args| { + if let Some(arg) = args.get(0) { + arg.clone().into() + } else { + FluentValue::None() + } + }) + .expect("Failed to add a function to the bundle."); + + // XXX: Different result from JS because + // we don't handle argument errors out + // of functions yet. + assert_format_no_errors(bundle.format("pass-nothing", None), "???"); + + assert_format_no_errors(bundle.format("pass-string", None), "a"); + + assert_format_no_errors(bundle.format("pass-number", None), "1"); + + assert_format_no_errors(bundle.format("pass-message", None), "Foo"); + + assert_format_no_errors(bundle.format("pass-attr", None), "Attribute"); + + let mut args = HashMap::new(); + args.insert("var", "Variable".into()); + assert_format_no_errors(bundle.format("pass-variable", Some(&args)), "Variable"); + + assert_format_no_errors(bundle.format("pass-function-call", None), "1"); +} diff --git a/fluent-bundle/tests/helpers/mod.rs b/fluent-bundle/tests/helpers/mod.rs index f0917628..00df3243 100644 --- a/fluent-bundle/tests/helpers/mod.rs +++ b/fluent-bundle/tests/helpers/mod.rs @@ -1,16 +1,28 @@ +use std::borrow::Cow; + use fluent_bundle::bundle::FluentError; use fluent_bundle::bundle::Message; use fluent_bundle::{FluentBundle, FluentResource}; #[allow(dead_code)] -pub fn assert_format_none(result: Option<(String, Vec)>) { +pub fn assert_format_none(result: Option<(Cow, Vec)>) { assert!(result.is_none()); } #[allow(dead_code)] -pub fn assert_format_no_errors(result: Option<(String, Vec)>, expected: &str) { +pub fn assert_format_no_errors(result: Option<(Cow, Vec)>, expected: &str) { + assert!(result.is_some()); + assert_eq!(result, Some((expected.into(), vec![]))); +} + +#[allow(dead_code)] +pub fn assert_format( + result: Option<(Cow, Vec)>, + expected: &str, + errors: Vec, +) { assert!(result.is_some()); - assert_eq!(result, Some((expected.to_string(), vec![]))); + assert_eq!(result, Some((expected.into(), errors))); } #[allow(dead_code)] @@ -18,6 +30,15 @@ pub fn assert_compound_no_errors(result: Option<(Message, Vec)>, ex assert_eq!(result, Some((expected, vec![]))); } +#[allow(dead_code)] +pub fn assert_compound( + result: Option<(Message, Vec)>, + expected: Message, + errors: Vec, +) { + assert_eq!(result, Some((expected, errors))); +} + pub fn assert_get_resource_from_str_no_errors(s: &str) -> FluentResource { FluentResource::try_new(s.to_owned()).expect("Failed to parse an FTL resource.") } diff --git a/fluent-bundle/tests/primitives_test.rs b/fluent-bundle/tests/primitives_test.rs new file mode 100644 index 00000000..dc80f906 --- /dev/null +++ b/fluent-bundle/tests/primitives_test.rs @@ -0,0 +1,102 @@ +mod helpers; + +use self::helpers::{ + assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, +}; + +#[test] +fn primitives_numbers() { + let res = assert_get_resource_from_str_no_errors( + " +one = { 1 } +select = { 1 -> + *[0] Zero + [1] One +} + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format_no_errors(bundle.format("one", None), "1"); + + assert_format_no_errors(bundle.format("select", None), "One"); +} + +#[test] +fn primitives_simple_string() { + let res = assert_get_resource_from_str_no_errors( + r#" +foo = Foo + +placeable-literal = { "Foo" } Bar +placeable-message = { foo } Bar + +selector-literal = { "Foo" -> + *[Foo] Member 1 +} + +bar = + .attr = Bar Attribute + +placeable-attr = { bar.attr } + +-baz = Baz + .attr = BazAttribute + +selector-attr = { -baz.attr -> + *[BazAttribute] Member 3 +} + "#, + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format_no_errors(bundle.format("foo", None), "Foo"); + + assert_format_no_errors(bundle.format("placeable-literal", None), "Foo Bar"); + + assert_format_no_errors(bundle.format("placeable-message", None), "Foo Bar"); + + assert_format_no_errors(bundle.format("selector-literal", None), "Member 1"); + + assert_format_no_errors(bundle.format("bar.attr", None), "Bar Attribute"); + + assert_format_no_errors(bundle.format("placeable-attr", None), "Bar Attribute"); + + assert_format_no_errors(bundle.format("selector-attr", None), "Member 3"); +} + +#[test] +fn primitives_complex_string() { + let res = assert_get_resource_from_str_no_errors( + r#" +foo = Foo +bar = { foo }Bar + +placeable-message = { bar }Baz + +baz = + .attr = { bar }BazAttribute + +-bazTerm = Value + .attr = { bar }BazAttribute + +placeable-attr = { baz.attr } + +selector-attr = { -bazTerm.attr -> + [FooBarBazAttribute] FooBarBaz + *[other] Other +} + "#, + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format_no_errors(bundle.format("bar", None), "FooBar"); + + assert_format_no_errors(bundle.format("placeable-message", None), "FooBarBaz"); + + assert_format_no_errors(bundle.format("baz.attr", None), "FooBarBazAttribute"); + + assert_format_no_errors(bundle.format("placeable-attr", None), "FooBarBazAttribute"); + + assert_format_no_errors(bundle.format("selector-attr", None), "FooBarBaz"); +} diff --git a/fluent-bundle/tests/resolve_attribute_expression.rs b/fluent-bundle/tests/resolve_attribute_expression.rs index e8c58631..6699e67b 100644 --- a/fluent-bundle/tests/resolve_attribute_expression.rs +++ b/fluent-bundle/tests/resolve_attribute_expression.rs @@ -1,7 +1,10 @@ mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; use self::helpers::{ - assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, + assert_format, assert_format_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, }; #[test] @@ -28,11 +31,66 @@ missing-missing = { missing.missing } assert_format_no_errors(bundle.format("use-foo-attr", None), "Foo Attr"); - assert_format_no_errors(bundle.format("use-bar", None), "___"); + assert_format( + bundle.format("use-bar", None), + "bar", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown message: bar".into(), + ))], + ); assert_format_no_errors(bundle.format("use-bar-attr", None), "Bar Attr"); - assert_format_no_errors(bundle.format("missing-attr", None), "___"); + assert_format( + bundle.format("missing-attr", None), + "foo.missing", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown message: foo.missing".into(), + ))], + ); + + assert_format( + bundle.format("missing-missing", None), + "missing.missing", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown message: missing.missing".into(), + ))], + ); +} + +#[test] +fn attribute_reference_cyclic() { + { + let res = assert_get_resource_from_str_no_errors( + " +foo = + .label = Foo { foo.label2 } + .label2 = { foo.label3 } Baz + .label3 = { foo.label } Bar + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format( + bundle.format("foo.label", None), + "Foo foo.label Bar Baz", + vec![FluentError::ResolverError(ResolverError::Cyclic)], + ); + } + + { + let res = assert_get_resource_from_str_no_errors( + " +foo = + .label = Foo { bar.label } +bar = + .label = Bar { baz.label } +baz = + .label = Baz +", + ); + let bundle = assert_get_bundle_no_errors(&res, None); - assert_format_no_errors(bundle.format("missing-missing", None), "___"); + assert_format_no_errors(bundle.format("foo.label", None), "Foo Bar Baz"); + } } diff --git a/fluent-bundle/tests/resolve_external_argument.rs b/fluent-bundle/tests/resolve_external_argument.rs index 3b377bba..79a37142 100644 --- a/fluent-bundle/tests/resolve_external_argument.rs +++ b/fluent-bundle/tests/resolve_external_argument.rs @@ -30,7 +30,7 @@ unread-emails-dec = You have { $emailsCountDec } unread emails. let mut args = HashMap::new(); args.insert("emailsCount", FluentValue::from(5)); - args.insert("emailsCountDec", FluentValue::into_number("5.0").unwrap()); + args.insert("emailsCountDec", FluentValue::into_number("5.0")); assert_format_no_errors( bundle.format("unread-emails", Some(&args)), diff --git a/fluent-bundle/tests/resolve_message_reference.rs b/fluent-bundle/tests/resolve_message_reference.rs index 0bcda5b1..c5a1beff 100644 --- a/fluent-bundle/tests/resolve_message_reference.rs +++ b/fluent-bundle/tests/resolve_message_reference.rs @@ -1,7 +1,10 @@ mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; use self::helpers::{ - assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, + assert_format, assert_format_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, }; #[test] @@ -48,7 +51,13 @@ baz = { bar } Baz fn message_reference_missing() { let res = assert_get_resource_from_str_no_errors("bar = { foo } Bar"); let bundle = assert_get_bundle_no_errors(&res, None); - assert_format_no_errors(bundle.format("bar", None), "___ Bar"); + assert_format( + bundle.format("bar", None), + "foo Bar", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown message: foo".into(), + ))], + ); } #[test] @@ -62,8 +71,11 @@ bar = { foo } Bar ); let bundle = assert_get_bundle_no_errors(&res, None); - assert_format_no_errors(bundle.format("foo", None), "Foo ___"); - assert_format_no_errors(bundle.format("bar", None), "___ Bar"); + assert_format( + bundle.format("foo", None), + "Foo foo Bar", + vec![FluentError::ResolverError(ResolverError::Cyclic)], + ); } { @@ -75,8 +87,16 @@ bar = { foo } ); let bundle = assert_get_bundle_no_errors(&res, None); - assert_format_no_errors(bundle.format("foo", None), "___"); - assert_format_no_errors(bundle.format("bar", None), "___"); + assert_format( + bundle.format("foo", None), + "foo", + vec![FluentError::ResolverError(ResolverError::Cyclic)], + ); + assert_format( + bundle.format("bar", None), + "bar", + vec![FluentError::ResolverError(ResolverError::Cyclic)], + ); } } diff --git a/fluent-bundle/tests/resolve_plural_rule.rs b/fluent-bundle/tests/resolve_plural_rule.rs index 7f28cd98..47a850b1 100644 --- a/fluent-bundle/tests/resolve_plural_rule.rs +++ b/fluent-bundle/tests/resolve_plural_rule.rs @@ -28,7 +28,7 @@ unread-emails-dec = let mut args = HashMap::new(); args.insert("emailsCount", FluentValue::from(1)); - args.insert("emailsCountDec", FluentValue::into_number("1.0").unwrap()); + args.insert("emailsCountDec", FluentValue::into_number("1.0")); assert_format_no_errors( bundle.format("unread-emails", Some(&args)), @@ -64,7 +64,7 @@ unread-emails-dec = let mut args = HashMap::new(); args.insert("emailsCount", FluentValue::from(1)); - args.insert("emailsCountDec", FluentValue::into_number("1.0").unwrap()); + args.insert("emailsCountDec", FluentValue::into_number("1.0")); assert_format_no_errors( bundle.format("unread-emails", Some(&args)), diff --git a/fluent-bundle/tests/resolve_select_expression.rs b/fluent-bundle/tests/resolve_select_expression.rs index 7cda0a73..0b489b5b 100644 --- a/fluent-bundle/tests/resolve_select_expression.rs +++ b/fluent-bundle/tests/resolve_select_expression.rs @@ -1,9 +1,12 @@ mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; use std::collections::HashMap; use self::helpers::{ - assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, + assert_format, assert_format_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, }; use fluent_bundle::types::FluentValue; @@ -169,19 +172,37 @@ baz-unknown = assert_format_no_errors(bundle.format("foo-miss", Some(&args)), "Foo"); - assert_format_no_errors(bundle.format("foo-unknown", Some(&args)), "Foo"); + assert_format( + bundle.format("foo-unknown", Some(&args)), + "Foo", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown variable: $unknown".into(), + ))], + ); assert_format_no_errors(bundle.format("bar-hit", Some(&args)), "Bar 3"); assert_format_no_errors(bundle.format("bar-miss", Some(&args)), "Bar 1"); - assert_format_no_errors(bundle.format("bar-unknown", Some(&args)), "Bar 1"); + assert_format( + bundle.format("bar-unknown", Some(&args)), + "Bar 1", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown variable: $unknown".into(), + ))], + ); assert_format_no_errors(bundle.format("baz-hit", Some(&args)), "Baz E"); assert_format_no_errors(bundle.format("baz-miss", Some(&args)), "Baz 1"); - assert_format_no_errors(bundle.format("baz-unknown", Some(&args)), "Baz 1"); + assert_format( + bundle.format("baz-unknown", Some(&args)), + "Baz 1", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown variable: $unknown".into(), + ))], + ); } #[test] @@ -221,3 +242,25 @@ use-foo = assert_format_no_errors(bundle.format("use-foo", None), "Foo"); } + +#[test] +fn select_selector_error() { + let res = assert_get_resource_from_str_no_errors( + " +use-foo = + { -foo.attr -> + [FooAttr] Foo + *[other] Other + } + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format( + bundle.format("use-foo", None), + "Other", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown term: -foo.attr".into(), + ))], + ); +} diff --git a/fluent-bundle/tests/resolve_variant_expression.rs b/fluent-bundle/tests/resolve_variant_expression.rs index 58f1fec7..b5425a6e 100644 --- a/fluent-bundle/tests/resolve_variant_expression.rs +++ b/fluent-bundle/tests/resolve_variant_expression.rs @@ -1,7 +1,10 @@ mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; use self::helpers::{ - assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, + assert_format, assert_format_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, }; #[test] @@ -43,5 +46,11 @@ missing-missing = { -missing(gender: "missing") } assert_format_no_errors(bundle.format("use-bar-missing", None), "Bar"); - assert_format_no_errors(bundle.format("missing-missing", None), "___"); + assert_format( + bundle.format("missing-missing", None), + "-missing", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown term: -missing".into(), + ))], + ); } diff --git a/fluent-bundle/tests/resource_test.rs b/fluent-bundle/tests/resource_test.rs new file mode 100644 index 00000000..9446784a --- /dev/null +++ b/fluent-bundle/tests/resource_test.rs @@ -0,0 +1,10 @@ +use fluent_bundle::resource::FluentResource; + +#[test] +fn resource_try_new() { + let res = FluentResource::try_new("key = Value".into()); + assert_eq!(res.is_ok(), true); + + let res_err = FluentResource::try_new("2key = Value".into()); + assert_eq!(res_err.is_err(), true); +} diff --git a/fluent-bundle/tests/select_expression_test.rs b/fluent-bundle/tests/select_expression_test.rs new file mode 100644 index 00000000..719dfa67 --- /dev/null +++ b/fluent-bundle/tests/select_expression_test.rs @@ -0,0 +1,113 @@ +mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; +use fluent_bundle::types::FluentValue; + +use std::collections::HashMap; + +use self::helpers::{ + assert_format, assert_format_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, +}; + +#[test] +fn missing_selector() { + let res = assert_get_resource_from_str_no_errors( + " +select = {$none -> + [a] A + *[b] B +} + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format( + bundle.format("select", None), + "B", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown variable: $none".into(), + ))], + ); +} + +#[test] +fn string_selector() { + let res = assert_get_resource_from_str_no_errors( + " +select = {$selector -> + [a] A + *[b] B +} + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from("a")); + assert_format_no_errors(bundle.format("select", Some(&args)), "A"); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from("c")); + assert_format_no_errors(bundle.format("select", Some(&args)), "B"); +} + +#[test] +fn number_selectors() { + let res = assert_get_resource_from_str_no_errors( + " +select = {$selector -> + [0] A + *[1] B +} + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from(0)); + assert_format_no_errors(bundle.format("select", Some(&args)), "A"); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from(2)); + assert_format_no_errors(bundle.format("select", Some(&args)), "B"); +} + +#[test] +fn plural_categories() { + let res = assert_get_resource_from_str_no_errors( + " +select = {$selector -> + [one] A + *[other] B +} + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from(1)); + assert_format_no_errors(bundle.format("select", Some(&args)), "A"); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from("one")); + assert_format_no_errors(bundle.format("select", Some(&args)), "A"); + + let res = assert_get_resource_from_str_no_errors( + " +select = {$selector -> + [one] A + *[default] D +} + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from(2)); + assert_format_no_errors(bundle.format("select", Some(&args)), "D"); + + let mut args = HashMap::new(); + args.insert("selector", FluentValue::from("default")); + assert_format_no_errors(bundle.format("select", Some(&args)), "D"); +} diff --git a/fluent-bundle/tests/types_test.rs b/fluent-bundle/tests/types_test.rs new file mode 100644 index 00000000..fb193c62 --- /dev/null +++ b/fluent-bundle/tests/types_test.rs @@ -0,0 +1,73 @@ +use fluent_bundle::resolve::Scope; +use fluent_bundle::types::FluentValue; +use fluent_bundle::FluentBundle; + +#[test] +fn fluent_value_number() { + let value = FluentValue::into_number("invalid"); + assert_eq!(value, FluentValue::String("invalid".into())); +} + +#[test] +fn fluent_value_matches() { + // We'll use `ars` locale since it happens to have all + // plural rules categories. + let bundle = FluentBundle::new(&["ars"]); + let scope = Scope::new(&bundle, None); + + let string_val = FluentValue::String("string1".into()); + let string_val_copy = FluentValue::String("string1".into()); + let string_val2 = FluentValue::String("23.5".into()); + + let number_val = FluentValue::Number("-23.5".into()); + let number_val_copy = FluentValue::Number("-23.5".into()); + let number_val2 = FluentValue::Number("23.5".into()); + + assert_eq!(string_val.matches(&string_val_copy, &scope), true); + assert_eq!(string_val.matches(&string_val2, &scope), false); + + assert_eq!(number_val.matches(&number_val_copy, &scope), true); + assert_eq!(number_val.matches(&number_val2, &scope), false); + + assert_eq!(string_val2.matches(&number_val2, &scope), false); + + assert_eq!(string_val2.matches(&number_val2, &scope), false); + + let string_cat_zero = FluentValue::String("zero".into()); + let string_cat_one = FluentValue::String("one".into()); + let string_cat_two = FluentValue::String("two".into()); + let string_cat_few = FluentValue::String("few".into()); + let string_cat_many = FluentValue::String("many".into()); + let string_cat_other = FluentValue::String("other".into()); + + let number_cat_zero = FluentValue::Number("0".into()); + let number_cat_one = FluentValue::Number("1".into()); + let number_cat_two = FluentValue::Number("2".into()); + let number_cat_few = FluentValue::Number("3".into()); + let number_cat_many = FluentValue::Number("11".into()); + let number_cat_other = FluentValue::Number("101".into()); + + assert_eq!(string_cat_zero.matches(&number_cat_zero, &scope), true); + assert_eq!(string_cat_one.matches(&number_cat_one, &scope), true); + assert_eq!(string_cat_two.matches(&number_cat_two, &scope), true); + assert_eq!(string_cat_few.matches(&number_cat_few, &scope), true); + assert_eq!(string_cat_many.matches(&number_cat_many, &scope), true); + assert_eq!(string_cat_other.matches(&number_cat_other, &scope), true); + assert_eq!(string_cat_other.matches(&number_cat_one, &scope), false); + + assert_eq!(string_val2.matches(&number_cat_one, &scope), false); +} + +#[test] +fn fluent_value_from() { + let value_str = FluentValue::from("my str"); + let value_string = FluentValue::from(String::from("my string")); + let value_f64 = FluentValue::from(23.5 as f64); + let value_isize = FluentValue::from(-23 as isize); + + assert_eq!(value_str, FluentValue::String("my str".into())); + assert_eq!(value_string, FluentValue::String("my string".into())); + + assert_eq!(value_f64, FluentValue::Number("23.5".into())); + assert_eq!(value_isize, FluentValue::Number("-23".into())); +} diff --git a/fluent-bundle/tests/values_format_test.rs b/fluent-bundle/tests/values_format_test.rs new file mode 100644 index 00000000..32b63197 --- /dev/null +++ b/fluent-bundle/tests/values_format_test.rs @@ -0,0 +1,55 @@ +mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; + +use self::helpers::{ + assert_format, assert_format_no_errors, assert_format_none, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, +}; + +#[test] +fn formatting_values() { + let res = assert_get_resource_from_str_no_errors( + " +key1 = Value 1 +key2 = { $sel -> + [a] A2 + *[b] B2 +} +key3 = Value { 3 } +key4 = { $sel -> + [a] A{ 4 } + *[b] B{ 4 } +} +key5 = + .a = A5 + .b = B5 + ", + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format_no_errors(bundle.format("key1", None), "Value 1"); + + assert_format( + bundle.format("key2", None), + "B2", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown variable: $sel".into(), + ))], + ); + + assert_format_no_errors(bundle.format("key3", None), "Value 3"); + + assert_format( + bundle.format("key4", None), + "B4", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown variable: $sel".into(), + ))], + ); + + assert_format_none(bundle.format("key5", None)); + + assert_format_no_errors(bundle.format("key5.a", None), "A5"); + assert_format_no_errors(bundle.format("key5.b", None), "B5"); +} diff --git a/fluent-bundle/tests/values_ref_test.rs b/fluent-bundle/tests/values_ref_test.rs new file mode 100644 index 00000000..b3eb762a --- /dev/null +++ b/fluent-bundle/tests/values_ref_test.rs @@ -0,0 +1,68 @@ +mod helpers; +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; + +use self::helpers::{ + assert_format, assert_format_no_errors, assert_get_bundle_no_errors, + assert_get_resource_from_str_no_errors, +}; + +#[test] +fn referencing_values() { + let res = assert_get_resource_from_str_no_errors( + r#" +key1 = Value 1 +-key2 = { $sel -> + [a] A2 + *[b] B2 +} +key3 = Value { 3 } +-key4 = { $sel -> + [a] A{ 4 } + *[b] B{ 4 } +} +key5 = + .a = A5 + .b = B5 +ref1 = { key1 } +ref2 = { -key2 } +ref3 = { key3 } +ref4 = { -key4 } +ref5 = { key5 } +ref6 = { -key2(sel: "a") } +ref7 = { -key2(sel: "b") } +ref8 = { -key4(sel: "a") } +ref9 = { -key4(sel: "b") } +ref10 = { key5.a } +ref11 = { key5.b } + "#, + ); + let bundle = assert_get_bundle_no_errors(&res, None); + + assert_format_no_errors(bundle.format("ref1", None), "Value 1"); + + assert_format_no_errors(bundle.format("ref2", None), "B2"); + + assert_format_no_errors(bundle.format("ref3", None), "Value 3"); + + assert_format_no_errors(bundle.format("ref4", None), "B4"); + + // XXX: Seems like a bug in JS impl because + // it expects "???" here... + assert_format( + bundle.format("ref5", None), + "key5", + vec![FluentError::ResolverError(ResolverError::Reference( + "Unknown message: key5".into(), + ))], + ); + + assert_format_no_errors(bundle.format("ref6", None), "A2"); + assert_format_no_errors(bundle.format("ref7", None), "B2"); + + assert_format_no_errors(bundle.format("ref8", None), "A4"); + assert_format_no_errors(bundle.format("ref9", None), "B4"); + + assert_format_no_errors(bundle.format("ref10", None), "A5"); + assert_format_no_errors(bundle.format("ref11", None), "B5"); +}