From fc645cc05353b2002faeda723b0986236b748ffb Mon Sep 17 00:00:00 2001 From: Zibi Braniecki Date: Fri, 12 Jul 2019 16:00:13 -0700 Subject: [PATCH] Update FluentBundle to the latest API --- fluent-bundle/Cargo.toml | 9 +- fluent-bundle/benches/resolver.rs | 34 +- fluent-bundle/examples/external_arguments.rs | 35 +- fluent-bundle/examples/functions.rs | 31 +- fluent-bundle/examples/hello.rs | 10 +- fluent-bundle/examples/message_reference.rs | 22 +- fluent-bundle/examples/selector.rs | 24 +- fluent-bundle/examples/simple-app.rs | 35 +- fluent-bundle/src/bundle.rs | 273 +++--------- fluent-bundle/src/entry.rs | 14 +- fluent-bundle/src/errors.rs | 6 + fluent-bundle/src/lib.rs | 16 +- fluent-bundle/src/resolve.rs | 182 ++++---- fluent-bundle/src/types.rs | 54 +-- fluent-bundle/tests/bundle_test.rs | 103 ----- fluent-bundle/tests/compound.rs | 56 --- fluent-bundle/tests/constructor_test.rs | 95 ----- fluent-bundle/tests/fixtures/arguments.yaml | 133 ++++++ fluent-bundle/tests/fixtures/attributes.yaml | 178 ++++++++ fluent-bundle/tests/fixtures/context.yaml | 141 +++++++ fluent-bundle/tests/fixtures/defaults.yaml | 6 + fluent-bundle/tests/fixtures/errors.yaml | 21 + fluent-bundle/tests/fixtures/functions.yaml | 85 ++++ .../tests/fixtures/functions_runtime.yaml | 29 ++ fluent-bundle/tests/fixtures/isolating.yaml | 72 ++++ fluent-bundle/tests/fixtures/literals.yaml | 69 +++ fluent-bundle/tests/fixtures/macros.yaml | 392 ++++++++++++++++++ fluent-bundle/tests/fixtures/patterns.yaml | 218 ++++++++++ fluent-bundle/tests/fixtures/primitives.yaml | 141 +++++++ .../tests/fixtures/select_expression.yaml | 151 +++++++ .../tests/fixtures/values_format.yaml | 74 ++++ fluent-bundle/tests/fixtures/values_ref.yaml | 140 +++++++ fluent-bundle/tests/format.rs | 34 -- fluent-bundle/tests/functions_runtime_test.rs | 50 --- fluent-bundle/tests/functions_test.rs | 74 ---- fluent-bundle/tests/helpers/mod.rs | 219 ++++++++-- fluent-bundle/tests/primitives_test.rs | 114 ----- .../tests/resolve_attribute_expression.rs | 96 ----- .../tests/resolve_external_argument.rs | 63 --- .../tests/resolve_message_reference.rs | 114 ----- fluent-bundle/tests/resolve_plural_rule.rs | 78 ---- .../tests/resolve_select_expression.rs | 266 ------------ fluent-bundle/tests/resolve_value.rs | 30 -- .../tests/resolve_variant_expression.rs | 56 --- fluent-bundle/tests/resolver_fixtures.rs | 334 +++++++++++++++ fluent-bundle/tests/resource_test.rs | 10 - fluent-bundle/tests/select_expression_test.rs | 113 ----- fluent-bundle/tests/values_format_test.rs | 55 --- fluent-bundle/tests/values_ref_test.rs | 68 --- fluent-fallback/examples/simple.rs | 8 +- fluent-fallback/src/lib.rs | 10 +- fluent-resmgr/examples/simple.rs | 36 +- fluent-resmgr/tests/localization_test.rs | 5 +- fluent-syntax/Cargo.toml | 3 +- fluent-syntax/tests/ast/mod.rs | 1 - 55 files changed, 2695 insertions(+), 1991 deletions(-) delete mode 100644 fluent-bundle/tests/bundle_test.rs delete mode 100644 fluent-bundle/tests/compound.rs delete mode 100644 fluent-bundle/tests/constructor_test.rs create mode 100644 fluent-bundle/tests/fixtures/arguments.yaml create mode 100644 fluent-bundle/tests/fixtures/attributes.yaml create mode 100644 fluent-bundle/tests/fixtures/context.yaml create mode 100644 fluent-bundle/tests/fixtures/defaults.yaml create mode 100644 fluent-bundle/tests/fixtures/errors.yaml create mode 100644 fluent-bundle/tests/fixtures/functions.yaml create mode 100644 fluent-bundle/tests/fixtures/functions_runtime.yaml create mode 100644 fluent-bundle/tests/fixtures/isolating.yaml create mode 100644 fluent-bundle/tests/fixtures/literals.yaml create mode 100644 fluent-bundle/tests/fixtures/macros.yaml create mode 100644 fluent-bundle/tests/fixtures/patterns.yaml create mode 100644 fluent-bundle/tests/fixtures/primitives.yaml create mode 100644 fluent-bundle/tests/fixtures/select_expression.yaml create mode 100644 fluent-bundle/tests/fixtures/values_format.yaml create mode 100644 fluent-bundle/tests/fixtures/values_ref.yaml delete mode 100644 fluent-bundle/tests/format.rs delete mode 100644 fluent-bundle/tests/functions_runtime_test.rs delete mode 100644 fluent-bundle/tests/functions_test.rs delete mode 100644 fluent-bundle/tests/primitives_test.rs delete mode 100644 fluent-bundle/tests/resolve_attribute_expression.rs delete mode 100644 fluent-bundle/tests/resolve_external_argument.rs delete mode 100644 fluent-bundle/tests/resolve_message_reference.rs delete mode 100644 fluent-bundle/tests/resolve_plural_rule.rs delete mode 100644 fluent-bundle/tests/resolve_select_expression.rs delete mode 100644 fluent-bundle/tests/resolve_value.rs delete mode 100644 fluent-bundle/tests/resolve_variant_expression.rs create mode 100644 fluent-bundle/tests/resolver_fixtures.rs delete mode 100644 fluent-bundle/tests/resource_test.rs delete mode 100644 fluent-bundle/tests/select_expression_test.rs delete mode 100644 fluent-bundle/tests/values_format_test.rs delete mode 100644 fluent-bundle/tests/values_ref_test.rs diff --git a/fluent-bundle/Cargo.toml b/fluent-bundle/Cargo.toml index 908a432a..680a5578 100644 --- a/fluent-bundle/Cargo.toml +++ b/fluent-bundle/Cargo.toml @@ -17,9 +17,6 @@ readme = "README.md" keywords = ["localization", "l10n", "i18n", "intl", "internationalization"] categories = ["localization", "internationalization"] -[dev-dependencies] -criterion = "^0.2" - [dependencies] fluent-locale = "^0.4.1" fluent-syntax = "^0.9" @@ -29,6 +26,12 @@ intl_pluralrules = "^1.0" rental = "^0.5.4" smallvec = "0.6.10" +[dev-dependencies] +criterion = "^0.2" +serde = { version = "^1.0", features = ["derive"] } +serde_yaml = "^0.8" +rand = "^0.7.0" + [[bench]] name = "resolver" harness = false diff --git a/fluent-bundle/benches/resolver.rs b/fluent-bundle/benches/resolver.rs index a4c3e4b5..af15d6d1 100644 --- a/fluent-bundle/benches/resolver.rs +++ b/fluent-bundle/benches/resolver.rs @@ -38,20 +38,23 @@ fn get_ids(res: &FluentResource) -> Vec { .collect() } -fn get_args(name: &str) -> Option> { +fn get_args(name: &str) -> Option> { match name { "preferences" => { let mut prefs_args = HashMap::new(); - prefs_args.insert("name", FluentValue::from("John")); - prefs_args.insert("tabCount", FluentValue::from(5)); - prefs_args.insert("count", FluentValue::from(3)); - prefs_args.insert("version", FluentValue::from("65.0")); - prefs_args.insert("path", FluentValue::from("/tmp")); - prefs_args.insert("num", FluentValue::from(4)); - prefs_args.insert("email", FluentValue::from("john@doe.com")); - prefs_args.insert("value", FluentValue::from(4.5)); - prefs_args.insert("unit", FluentValue::from("mb")); - prefs_args.insert("service-name", FluentValue::from("Mozilla Disk")); + prefs_args.insert("name".to_string(), FluentValue::from("John")); + prefs_args.insert("tabCount".to_string(), FluentValue::from(5)); + prefs_args.insert("count".to_string(), FluentValue::from(3)); + prefs_args.insert("version".to_string(), FluentValue::from("65.0")); + prefs_args.insert("path".to_string(), FluentValue::from("/tmp")); + prefs_args.insert("num".to_string(), FluentValue::from(4)); + prefs_args.insert("email".to_string(), FluentValue::from("john@doe.com")); + prefs_args.insert("value".to_string(), FluentValue::from(4.5)); + prefs_args.insert("unit".to_string(), FluentValue::from("mb")); + prefs_args.insert( + "service-name".to_string(), + FluentValue::from("Mozilla Disk"), + ); Some(prefs_args) } _ => None, @@ -91,7 +94,14 @@ fn resolver_bench(c: &mut Criterion) { b.iter(|| { for id in &ids { - let (_msg, errors) = bundle.compound(id, args.as_ref()).expect("Message found"); + let msg = bundle.get_message(id).expect("Message found"); + let mut errors = vec![]; + if let Some(value) = msg.value { + let _ = bundle.format_pattern(value, args.as_ref(), &mut errors); + } + for (_, value) in msg.attributes { + let _ = bundle.format_pattern(value, args.as_ref(), &mut errors); + } assert!(errors.len() == 0, "Resolver errors: {:#?}", errors); } }) diff --git a/fluent-bundle/examples/external_arguments.rs b/fluent-bundle/examples/external_arguments.rs index 5c54a2af..10537e48 100644 --- a/fluent-bundle/examples/external_arguments.rs +++ b/fluent-bundle/examples/external_arguments.rs @@ -20,23 +20,30 @@ unread-emails = .expect("Failed to add FTL resources to the bundle."); let mut args = HashMap::new(); - args.insert("name", FluentValue::from("John")); + args.insert("name".to_string(), FluentValue::from("John")); - match bundle.format("hello-world", Some(&args)) { - Some((value, _)) => println!("{}", value), - _ => println!("None"), - } + let msg = bundle + .get_message("hello-world") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + println!("{}", value); - match bundle.format("ref", Some(&args)) { - Some((value, _)) => println!("{}", value), - _ => println!("None"), - } + let msg = bundle.get_message("ref").expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + println!("{}", value); let mut args = HashMap::new(); - args.insert("emailCount", FluentValue::into_number("1.0")); + args.insert("emailCount".to_string(), FluentValue::into_number("1.0")); - match bundle.format("unread-emails", Some(&args)) { - Some((value, _)) => println!("{}", value), - None => println!("None"), - } + let msg = bundle + .get_message("unread-emails") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + println!("{}", value); } diff --git a/fluent-bundle/examples/functions.rs b/fluent-bundle/examples/functions.rs index 845240ae..49a4d66e 100644 --- a/fluent-bundle/examples/functions.rs +++ b/fluent-bundle/examples/functions.rs @@ -28,7 +28,7 @@ fn main() { } } - FluentValue::None() + FluentValue::None }) .expect("Failed to add a function to the bundle."); @@ -39,7 +39,7 @@ fn main() { Some(FluentValue::String(ref string)) => { format!("All your base belong to {}", string).into() } - _ => FluentValue::None(), + _ => FluentValue::None, }; }) .expect("Failed to add a function to the bundle."); @@ -54,18 +54,27 @@ fn main() { .add_resource(res3) .expect("Failed to add FTL resources to the bundle."); - let (value, _) = bundle - .format("hello-world", None) - .expect("Failed to format a message."); + let msg = bundle + .get_message("hello-world") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); assert_eq!(&value, "Hey there! \u{2068}I'm a function!\u{2069}"); - let (value, _) = bundle - .format("meaning-of-life", None) - .expect("Failed to format a message."); + let msg = bundle + .get_message("meaning-of-life") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); assert_eq!(&value, "The answer to life, the universe, and everything"); - let (value, _) = bundle - .format("all-your-base", None) - .expect("Failed to format a message."); + let msg = bundle + .get_message("all-your-base") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); assert_eq!(&value, "All your base belong to us"); } diff --git a/fluent-bundle/examples/hello.rs b/fluent-bundle/examples/hello.rs index 9109497e..a388a84e 100644 --- a/fluent-bundle/examples/hello.rs +++ b/fluent-bundle/examples/hello.rs @@ -7,8 +7,12 @@ fn main() { bundle .add_resource(&res) .expect("Failed to add FTL resources to the bundle."); - let (value, _) = bundle - .format("hello-world", None) - .expect("Failed to format a message."); + + let msg = bundle + .get_message("hello-world") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); assert_eq!(&value, "Hello, world!"); } diff --git a/fluent-bundle/examples/message_reference.rs b/fluent-bundle/examples/message_reference.rs index 61429dd7..5fd14f5f 100644 --- a/fluent-bundle/examples/message_reference.rs +++ b/fluent-bundle/examples/message_reference.rs @@ -15,13 +15,19 @@ bazbar = { baz } Bar .add_resource(res) .expect("Failed to add FTL resources to the bundle."); - match bundle.format("foobar", None) { - Some((value, _)) => println!("{}", value), - _ => println!("None"), - } + let msg = bundle + .get_message("foobar") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); + println!("{}", value); - match bundle.format("bazbar", None) { - Some((value, _)) => println!("{}", value), - _ => println!("None"), - } + let msg = bundle + .get_message("bazbar") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); + println!("{}", value); } diff --git a/fluent-bundle/examples/selector.rs b/fluent-bundle/examples/selector.rs index 21109c5d..c8d275ac 100644 --- a/fluent-bundle/examples/selector.rs +++ b/fluent-bundle/examples/selector.rs @@ -21,16 +21,22 @@ hello-world2 = Hello { $name -> .add_resource(res) .expect("Failed to add FTL resources to the bundle."); - match bundle.format("hello-world", None) { - Some((value, _)) => println!("{}", value), - _ => println!("None"), - } + let msg = bundle + .get_message("hello-world") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); + println!("{}", value); let mut args = HashMap::new(); - args.insert("name", FluentValue::from("moon")); + args.insert("name".to_string(), FluentValue::from("moon")); - match bundle.format("hello-world2", Some(&args)) { - Some((value, _)) => println!("{}", value), - _ => println!("None"), - } + let msg = bundle + .get_message("hello-world2") + .expect("Message doesn't exist."); + let mut errors = vec![]; + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + println!("{}", value); } diff --git a/fluent-bundle/examples/simple-app.rs b/fluent-bundle/examples/simple-app.rs index 0ca7ebb2..27535c22 100644 --- a/fluent-bundle/examples/simple-app.rs +++ b/fluent-bundle/examples/simple-app.rs @@ -130,29 +130,38 @@ fn main() { // 7.2. Construct a map of arguments // to format the message. let mut args = HashMap::new(); - args.insert("input", FluentValue::from(i)); - args.insert("value", FluentValue::from(collatz(i))); + args.insert("input".to_string(), FluentValue::from(i)); + args.insert("value".to_string(), FluentValue::from(collatz(i))); // 7.3. Format the message. - let (value, _) = bundle - .format("response-msg", Some(&args)) - .expect("Failed to format a message."); + let mut errors = vec![]; + let msg = bundle + .get_message("response-msg") + .expect("Message doesn't exist."); + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); println!("{}", value); } Err(err) => { let mut args = HashMap::new(); - 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)) - .expect("Failed to format a message."); + args.insert("input".to_string(), FluentValue::from(input.as_str())); + args.insert("reason".to_string(), FluentValue::from(err.to_string())); + let mut errors = vec![]; + let msg = bundle + .get_message("input-parse-error-msg") + .expect("Message doesn't exist."); + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); println!("{}", value); } } } None => { - let (value, _) = bundle - .format("missing-arg-error", None) - .expect("Failed to format a message."); + let mut errors = vec![]; + let msg = bundle + .get_message("missing-arg-error") + .expect("Message doesn't exist."); + let pattern = msg.value.expect("Message has no value."); + let value = bundle.format_pattern(&pattern, None, &mut errors); println!("{}", value); } } diff --git a/fluent-bundle/src/bundle.rs b/fluent-bundle/src/bundle.rs index 5738c36e..57ed1d88 100644 --- a/fluent-bundle/src/bundle.rs +++ b/fluent-bundle/src/bundle.rs @@ -21,8 +21,8 @@ use crate::types::FluentValue; #[derive(Debug, PartialEq)] pub struct Message<'m> { - pub value: Option>, - pub attributes: HashMap<&'m str, Cow<'m, str>>, + pub value: Option<&'m ast::Pattern<'m>>, + pub attributes: HashMap<&'m str, &'m ast::Pattern<'m>>, } /// A collection of localization messages for a single locale, which are meant @@ -43,10 +43,12 @@ pub struct Message<'m> { /// .expect("Failed to add FTL resources to the bundle."); /// /// let mut args = HashMap::new(); -/// args.insert("name", FluentValue::from("Rustacean")); +/// args.insert("name".to_string(), FluentValue::from("Rustacean")); /// -/// let (value, _) = bundle.format("intro", Some(&args)) -/// .expect("Failed to format a message."); +/// let msg = bundle.get_message("intro").expect("Message doesn't exist."); +/// let mut errors = vec![]; +/// let pattern = msg.value.expect("Message has no value."); +/// let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); /// assert_eq!(&value, "Welcome, \u{2068}Rustacean\u{2069}."); /// /// ``` @@ -59,12 +61,11 @@ pub struct Message<'m> { /// Next, call [`add_resource`] one or more times, supplying translations in the FTL syntax. The /// `FluentBundle` instance is now ready to be used for localization. /// -/// To format a translation, call [`format`] with the path of a message or attribute in order to -/// retrieve the translated string. Alternately, [`compound`] provides a convenient way of -/// formatting all attributes of a message at once. +/// To format a translation, call [`get_message`] to retrieve a [`Message`], +/// and then call [`format_pattern`] on the message value or attribute in order to +/// retrieve the translated string. /// -/// The result of `format` is an [`Option`] wrapping a `(String, Vec)`. On success, -/// the string is a formatted value that should be displayed in the UI. It is +/// The result of `format_pattern` is an [`Cow`]. It is /// recommended to treat the result as opaque from the perspective of the program and use it only /// to display localized messages. Do not examine it or alter in any way before displaying. This /// is a general good practice as far as all internationalization operations are concerned. @@ -79,11 +80,11 @@ pub struct Message<'m> { /// /// [`add_resource`]: ./struct.FluentBundle.html#method.add_resource /// [`FluentBundle::new`]: ./struct.FluentBundle.html#method.new -/// [`fluent::bundle::Message`]: ./struct.FluentBundle.html#method.new -/// [`format`]: ./struct.FluentBundle.html#method.format -/// [`compound`]: ./struct.FluentBundle.html#method.compound +/// [`Message`]: ./struct.Message.html +/// [`get_message`]: ./struct.FluentBundle.html#method.get_message +/// [`format_pattern`]: ./struct.FluentBundle.html#method.format_pattern /// [`add_resource`]: ./struct.FluentBundle.html#method.add_resource -/// [`Option`]: http://doc.rust-lang.org/std/option/enum.Option.html +/// [`Cow`]: http://doc.rust-lang.org/std/borrow/enum.Cow.html pub struct FluentBundle { pub locales: Vec, pub(crate) resources: Vec, @@ -243,7 +244,44 @@ impl FluentBundle { where R: Borrow, { - self.get_message(id).is_some() + self.get_entry_message(id).is_some() + } + + pub fn get_message(&self, id: &str) -> Option + where + R: Borrow, + { + let message = self.get_entry_message(id)?; + let value = message.value.as_ref(); + let mut attributes = if message.attributes.is_empty() { + HashMap::new() + } else { + HashMap::with_capacity(message.attributes.len()) + }; + + for attr in message.attributes.iter() { + attributes.insert(attr.id.name, &attr.value); + } + return Some(Message { value, attributes }); + } + + pub fn format_pattern<'bundle>( + &'bundle self, + pattern: &'bundle ast::Pattern, + args: Option<&'bundle HashMap>, + errors: &mut Vec, + ) -> Cow<'bundle, str> + where + R: Borrow, + { + let mut scope = Scope::new(self, args); + let result = pattern.resolve(&mut scope).to_string(); + + for err in scope.errors { + errors.push(err.into()); + } + + result } /// Makes the provided rust function available to messages with the name `id`. See @@ -268,18 +306,20 @@ impl FluentBundle { /// // Register a fn that maps from string to string length /// bundle.add_function("STRLEN", |positional, _named| match positional { /// [FluentValue::String(str)] => FluentValue::Number(str.len().to_string().into()), - /// _ => FluentValue::None(), + /// _ => FluentValue::None, /// }).expect("Failed to add a function to the bundle."); /// - /// let (value, _) = bundle.format("length", None) - /// .expect("Failed to format a message."); + /// let msg = bundle.get_message("length").expect("Message doesn't exist."); + /// let mut errors = vec![]; + /// let pattern = msg.value.expect("Message has no value."); + /// let value = bundle.format_pattern(&pattern, None, &mut errors); /// assert_eq!(&value, "5"); /// ``` /// /// [FTL syntax guide]: https://projectfluent.org/fluent/guide/functions.html pub fn add_function(&mut self, id: &str, func: F) -> Result<(), FluentError> where - F: for<'a> Fn(&[FluentValue<'a>], &HashMap<&str, FluentValue<'a>>) -> FluentValue<'a> + F: for<'a> Fn(&[FluentValue<'a>], &HashMap>) -> FluentValue<'a> + Sync + Send, { @@ -294,199 +334,4 @@ impl FluentBundle { }), } } - - /// Formats the message value identified by `path` using `args` to - /// provide variables. `path` is either a message id ("hello"), or - /// message id plus attribute ("hello.tooltip"). - /// - /// # Examples - /// - /// ``` - /// use fluent_bundle::{FluentBundle, FluentResource, FluentValue}; - /// use std::collections::HashMap; - /// - /// let ftl_string = String::from("intro = Welcome, { $name }."); - /// let resource = FluentResource::try_new(ftl_string) - /// .expect("Could not parse an FTL string."); - /// let mut bundle = FluentBundle::new(&["en-US"]); - /// bundle.add_resource(resource) - /// .expect("Failed to add FTL resources to the bundle."); - /// - /// let mut args = HashMap::new(); - /// args.insert("name", FluentValue::from("Rustacean")); - /// - /// let (value, _) = bundle.format("intro", Some(&args)) - /// .expect("Failed to format a message."); - /// assert_eq!(&value, "Welcome, \u{2068}Rustacean\u{2069}."); - /// - /// ``` - /// - /// An example with attributes and no args: - /// - /// ``` - /// use fluent_bundle::{FluentBundle, FluentResource}; - /// - /// let ftl_string = String::from(" - /// hello = - /// .title = Hi! - /// .tooltip = This says 'Hi!' - /// "); - /// let resource = FluentResource::try_new(ftl_string) - /// .expect("Could not parse an FTL string."); - /// let mut bundle = FluentBundle::new(&["en-US"]); - /// bundle.add_resource(resource) - /// .expect("Failed to add FTL resources to the bundle."); - /// - /// let (value, _) = bundle.format("hello.title", None) - /// .expect("Failed to format a message."); - /// assert_eq!(&value, "Hi!"); - /// ``` - /// - /// # Errors - /// - /// For some cases where `format` can't find a message it will return `None`. - /// - /// In all other cases `format` returns a string even if it - /// encountered errors. Generally, during partial errors `format` will - /// 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. - /// - /// The second term of the tuple will contain any extra error information - /// gathered during formatting. A caller may safely ignore the extra errors - /// if the fallback formatting policies are acceptable. - /// - /// ``` - /// use fluent_bundle::{FluentBundle, FluentResource}; - /// - /// // Create a message with bad cyclic reference - /// let ftl_string = String::from("foo = a { foo } b"); - /// let resource = FluentResource::try_new(ftl_string) - /// .expect("Could not parse an FTL string."); - /// let mut bundle = FluentBundle::new(&["en-US"]); - /// bundle.add_resource(resource) - /// .expect("Failed to add FTL resources to the bundle."); - /// - /// // The result falls back to "a foo b" - /// let (value, _) = bundle.format("foo", None) - /// .expect("Failed to format a message."); - /// assert_eq!(&value, "a foo b"); - /// ``` - pub fn format<'bundle>( - &'bundle self, - path: &str, - args: Option<&'bundle HashMap<&str, FluentValue>>, - ) -> Option<(Cow<'bundle, str>, Vec)> - where - R: Borrow, - { - let mut scope = Scope::new(self, args); - - let mut errors = vec![]; - - let string = if let Some(ptr_pos) = path.find('.') { - let message_id = &path[..ptr_pos]; - let message = self.get_message(message_id)?; - let attr_name = &path[(ptr_pos + 1)..]; - let attr = message - .attributes - .iter() - .find(|attr| attr.id.name == attr_name)?; - attr.value.resolve(&mut scope).to_string() - } else { - let message_id = path; - let message = self.get_message(message_id)?; - message - .value - .as_ref() - .map(|value| value.resolve(&mut scope))? - .to_string() - }; - - for err in scope.errors { - errors.push(err.into()); - } - - Some((string, errors)) - } - - /// Formats both the message value and attributes identified by `message_id` - /// using `args` to provide variables. This is useful for cases where a UI - /// element requires multiple related text fields, such as a button that has - /// both display text and assistive text. - /// - /// # Examples - /// - /// ``` - /// use fluent_bundle::{FluentBundle, FluentResource, FluentValue}; - /// use std::collections::HashMap; - /// - /// let ftl_string = String::from(" - /// login-input = Predefined value - /// .placeholder = example@email.com - /// .aria-label = Login input value - /// .title = Type your login email - /// "); - /// let resource = FluentResource::try_new(ftl_string) - /// .expect("Could not parse an FTL string."); - /// let mut bundle = FluentBundle::new(&["en-US"]); - /// bundle.add_resource(resource) - /// .expect("Failed to add FTL resources to the bundle."); - /// - /// let (message, _) = bundle.compound("login-input", None) - /// .expect("Failed to format a message."); - /// assert_eq!(message.value, Some("Predefined value".into())); - /// assert_eq!(message.attributes.get("title"), Some(&"Type your login email".into())); - /// ``` - /// - /// # Errors - /// - /// For some cases where `compound` can't find a message it will return `None`. - /// - /// In all other cases `compound` returns a message even if it - /// encountered errors. Generally, during partial errors `compound` will - /// 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. - /// - /// The second term of the tuple will contain any extra error information - /// gathered during formatting. A caller may safely ignore the extra errors - /// if the fallback formatting policies are acceptable. - pub fn compound<'bundle>( - &'bundle self, - message_id: &str, - args: Option<&'bundle HashMap<&str, FluentValue>>, - ) -> Option<(Message<'bundle>, Vec)> - where - R: Borrow, - { - let mut scope = Scope::new(self, args); - let mut errors = vec![]; - let message = self.get_message(message_id)?; - - let value = message - .value - .as_ref() - .map(|value| value.resolve(&mut scope).to_string()); - - // Setting capacity helps performance for cases with attributes, - // but is slower than `::new` for cases without. - // Maybe one day this will be fixed but for now let's use the trick. - let mut attributes = if message.attributes.is_empty() { - HashMap::new() - } else { - HashMap::with_capacity(message.attributes.len()) - }; - - for attr in message.attributes.iter() { - let val = attr.value.resolve(&mut scope); - attributes.insert(attr.id.name, val.to_string()); - } - - for err in scope.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 3b821454..641a9677 100644 --- a/fluent-bundle/src/entry.rs +++ b/fluent-bundle/src/entry.rs @@ -10,7 +10,7 @@ use crate::resource::FluentResource; use crate::types::FluentValue; pub type FluentFunction = Box< - dyn for<'a> Fn(&[FluentValue<'a>], &HashMap<&str, FluentValue<'a>>) -> FluentValue<'a> + dyn for<'a> Fn(&[FluentValue<'a>], &HashMap>) -> FluentValue<'a> + Send + Sync, >; @@ -22,13 +22,13 @@ pub enum Entry { } pub trait GetEntry { - fn get_message(&self, id: &str) -> Option<&ast::Message>; - fn get_term(&self, id: &str) -> Option<&ast::Term>; - fn get_function(&self, id: &str) -> Option<&FluentFunction>; + fn get_entry_message(&self, id: &str) -> Option<&ast::Message>; + fn get_entry_term(&self, id: &str) -> Option<&ast::Term>; + fn get_entry_function(&self, id: &str) -> Option<&FluentFunction>; } impl<'bundle, R: Borrow> GetEntry for FluentBundle { - fn get_message(&self, id: &str) -> Option<&ast::Message> { + fn get_entry_message(&self, id: &str) -> Option<&ast::Message> { self.entries.get(id).and_then(|entry| match *entry { Entry::Message(pos) => { let res = self.resources.get(pos[0])?.borrow(); @@ -44,7 +44,7 @@ impl<'bundle, R: Borrow> GetEntry for FluentBundle { }) } - fn get_term(&self, id: &str) -> Option<&ast::Term> { + fn get_entry_term(&self, id: &str) -> Option<&ast::Term> { self.entries.get(id).and_then(|entry| match *entry { Entry::Term(pos) => { let res = self.resources.get(pos[0])?.borrow(); @@ -60,7 +60,7 @@ impl<'bundle, R: Borrow> GetEntry for FluentBundle { }) } - fn get_function(&self, id: &str) -> Option<&FluentFunction> { + fn get_entry_function(&self, id: &str) -> Option<&FluentFunction> { self.entries.get(id).and_then(|entry| match entry { Entry::Function(function) => Some(function), _ => None, diff --git a/fluent-bundle/src/errors.rs b/fluent-bundle/src/errors.rs index a3a4aaac..45f35f1f 100644 --- a/fluent-bundle/src/errors.rs +++ b/fluent-bundle/src/errors.rs @@ -16,3 +16,9 @@ impl From for FluentError { FluentError::ResolverError(error) } } + +impl From for FluentError { + fn from(error: ParserError) -> Self { + FluentError::ParserError(error) + } +} diff --git a/fluent-bundle/src/lib.rs b/fluent-bundle/src/lib.rs index 841d46b0..f39c5657 100644 --- a/fluent-bundle/src/lib.rs +++ b/fluent-bundle/src/lib.rs @@ -27,18 +27,20 @@ //! .add_resource(res) //! .expect("Failed to add FTL resources to the bundle."); //! -//! let (value, _) = bundle -//! .format("hello-world", None) -//! .expect("Failed to format a message."); +//! let msg = bundle.get_message("hello-world").expect("Message doesn't exist."); +//! let mut errors = vec![]; +//! let pattern = msg.value.expect("Message has no value."); +//! let value = bundle.format_pattern(&pattern, None, &mut errors); //! //! assert_eq!(&value, "Hello, world!"); //! //! let mut args = HashMap::new(); -//! args.insert("name", FluentValue::from("John")); +//! args.insert("name".to_string(), FluentValue::from("John")); //! -//! let (value, _) = bundle -//! .format("intro", Some(&args)) -//! .expect("Failed to format a message."); +//! let msg = bundle.get_message("intro").expect("Message doesn't exist."); +//! let mut errors = vec![]; +//! let pattern = msg.value.expect("Message has no value."); +//! let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); //! //! assert_eq!(value, "Welcome, \u{2068}John\u{2069}."); //! ``` diff --git a/fluent-bundle/src/resolve.rs b/fluent-bundle/src/resolve.rs index 11e4c2f4..2f3e7cc0 100644 --- a/fluent-bundle/src/resolve.rs +++ b/fluent-bundle/src/resolve.rs @@ -7,7 +7,6 @@ //! [`FluentBundle`]: ../bundle/struct.FluentBundle.html use std::borrow::Borrow; -use std::cell::RefCell; use std::collections::HashMap; use std::fmt::Write; @@ -24,8 +23,6 @@ use crate::types::FluentValue; pub enum ResolverError { Reference(String), MissingDefault, - Argument(String), - Value, Cyclic, } @@ -34,11 +31,11 @@ pub struct Scope<'bundle, R: Borrow> { /// The current `FluentBundle` instance. pub bundle: &'bundle FluentBundle, /// The current arguments passed by the developer. - pub args: Option<&'bundle HashMap<&'bundle str, FluentValue<'bundle>>>, + args: Option<&'bundle HashMap>>, /// Local args - pub local_args: Option>>, + local_args: Option>>, /// Tracks hashes to prevent infinite recursion. - pub travelled: RefCell; 2]>>, + travelled: smallvec::SmallVec<[&'bundle ast::Pattern<'bundle>; 2]>, /// Track errors accumulated during resolving. pub errors: Vec, } @@ -46,13 +43,13 @@ pub struct Scope<'bundle, R: Borrow> { impl<'bundle, R: Borrow> Scope<'bundle, R> { pub fn new( bundle: &'bundle FluentBundle, - args: Option<&'bundle HashMap<&str, FluentValue>>, + args: Option<&'bundle HashMap>, ) -> Self { Scope { bundle, args, local_args: None, - travelled: RefCell::new(smallvec::SmallVec::new()), + travelled: Default::default(), errors: vec![], } } @@ -63,57 +60,32 @@ impl<'bundle, R: Borrow> Scope<'bundle, R> { // This is the case when pattern is called from Bundle and it // allows us to fast-path simple resolutions, and only use the stack // for placeables. - pub fn maybe_track( + pub fn maybe_track( &mut self, pattern: &'bundle ast::Pattern, - entry: Option>, - mut action: F, - ) -> FluentValue<'bundle> - where - F: FnMut(&mut Scope<'bundle, R>) -> FluentValue<'bundle>, - { - if self.travelled.borrow().is_empty() { - self.track(pattern, entry, action) - } else { - action(self) + placeable: &'bundle ast::Expression, + ) -> FluentValue<'bundle> { + if self.travelled.is_empty() { + self.travelled.push(pattern); } + placeable.resolve(self) } - pub fn track( + pub fn track( &mut self, pattern: &'bundle ast::Pattern, - entry: Option>, - mut action: F, - ) -> FluentValue<'bundle> - where - F: FnMut(&mut Scope<'bundle, R>) -> FluentValue<'bundle>, - { - if self.travelled.borrow().contains(&pattern) { + entry: DisplayableNode<'bundle>, + ) -> FluentValue<'bundle> { + if self.travelled.contains(&pattern) { self.errors.push(ResolverError::Cyclic); - if let Some(entry) = entry { - FluentValue::Error(entry) - } else { - FluentValue::None() - } + FluentValue::Error(entry) } else { - self.travelled.borrow_mut().push(pattern); - let result = action(self); - self.travelled.borrow_mut().pop(); + self.travelled.push(pattern); + let result = pattern.resolve(self); + self.travelled.pop(); result } } - - fn maybe_resolve_attribute( - &mut self, - attributes: &'bundle [ast::Attribute<'bundle>], - entry: DisplayableNode<'bundle>, - name: &str, - ) -> Option> { - attributes - .iter() - .find(|attr| attr.id.name == name) - .map(|attr| self.track(&attr.value, Some(entry), |scope| attr.value.resolve(scope))) - } } fn generate_ref_error<'source, R>( @@ -144,18 +116,14 @@ impl<'source> ResolveValue<'source> for ast::Pattern<'source> { if self.elements.len() == 1 { return match self.elements[0] { ast::PatternElement::TextElement(s) => s.into(), - ast::PatternElement::Placeable(ref p) => { - scope.maybe_track(self, None, |scope| p.resolve(scope)) - } + ast::PatternElement::Placeable(ref p) => scope.maybe_track(self, p), }; } let mut string = String::new(); for elem in &self.elements { match elem { - ast::PatternElement::TextElement(s) => { - string.push_str(&s); - } + ast::PatternElement::TextElement(s) => string.push_str(&s), ast::PatternElement::Placeable(p) => { let needs_isolation = scope.bundle.use_isolating && match p { @@ -171,12 +139,12 @@ impl<'source> ResolveValue<'source> for ast::Pattern<'source> { _ => true, }; if needs_isolation { - string.write_char('\u{2068}').expect("Writing succeeded"); + string.write_char('\u{2068}').expect("Writing failed"); } - let result = scope.maybe_track(self, None, |scope| p.resolve(scope)); - write!(string, "{}", result).expect("Writing succeeded"); + let result = scope.maybe_track(self, p); + write!(string, "{}", result).expect("Writing failed"); if needs_isolation { - string.write_char('\u{2069}').expect("Writing succeeded"); + string.write_char('\u{2069}').expect("Writing failed"); } } } @@ -192,27 +160,21 @@ impl<'source> ResolveValue<'source> for ast::Expression<'source> { { match self { ast::Expression::InlineExpression(exp) => exp.resolve(scope), - ast::Expression::SelectExpression { - selector, - ref variants, - } => { + ast::Expression::SelectExpression { selector, variants } => { let selector = selector.resolve(scope); match selector { FluentValue::String(_) | FluentValue::Number(_) => { for variant in variants { - match variant.key { + let key = match variant.key { ast::VariantKey::Identifier { name } => { - let key = FluentValue::String(name.into()); - if key.matches(&selector, &scope) { - return variant.value.resolve(scope); - } + FluentValue::String(name.into()) } ast::VariantKey::NumberLiteral { value } => { - let key = FluentValue::into_number(value); - if key.matches(&selector, &scope) { - return variant.value.resolve(scope); - } + FluentValue::into_number(value) } + }; + if key.matches(&selector, &scope) { + return variant.value.resolve(scope); } } } @@ -225,7 +187,7 @@ impl<'source> ResolveValue<'source> for ast::Expression<'source> { } } scope.errors.push(ResolverError::MissingDefault); - FluentValue::None() + FluentValue::None } } } @@ -240,48 +202,47 @@ impl<'source> ResolveValue<'source> for ast::InlineExpression<'source> { ast::InlineExpression::StringLiteral { value } => { FluentValue::String(unescape_unicode(value)) } - ast::InlineExpression::MessageReference { id, attribute } => { - let msg = scope.bundle.get_message(&id.name); - - if let Some(msg) = msg { + ast::InlineExpression::MessageReference { id, attribute } => scope + .bundle + .get_entry_message(&id.name) + .and_then(|msg| { if let Some(attr) = attribute { - scope - .maybe_resolve_attribute(&msg.attributes, self.into(), attr.name) - .unwrap_or_else(|| generate_ref_error(scope, self.into())) - } else if let Some(value) = msg.value.as_ref() { - scope.track(value, Some(msg.into()), |scope| value.resolve(scope)) + msg.attributes + .iter() + .find(|a| a.id.name == attr.name) + .map(|attr| scope.track(&attr.value, self.into())) } else { - generate_ref_error(scope, self.into()) + msg.value + .as_ref() + .map(|value| scope.track(value, self.into())) } - } else { - generate_ref_error(scope, self.into()) - } - } + }) + .unwrap_or_else(|| generate_ref_error(scope, self.into())), ast::InlineExpression::NumberLiteral { value } => FluentValue::into_number(*value), ast::InlineExpression::TermReference { id, attribute, arguments, } => { - let term = scope.bundle.get_term(&id.name); - let (_, resolved_named_args) = get_arguments(scope, arguments); scope.local_args = Some(resolved_named_args); - let value = if let Some(term) = term { - if let Some(attr) = attribute { - scope - .maybe_resolve_attribute(&term.attributes, self.into(), attr.name) - .unwrap_or_else(|| generate_ref_error(scope, self.into())) - } else { - scope.track(&term.value, Some(term.into()), |scope| { - term.value.resolve(scope) - }) - } - } else { - generate_ref_error(scope, self.into()) - }; + let value = scope + .bundle + .get_entry_term(&id.name) + .and_then(|term| { + if let Some(attr) = attribute { + term.attributes + .iter() + .find(|a| a.id.name == attr.name) + .map(|attr| scope.track(&attr.value, self.into())) + } else { + Some(scope.track(&term.value, self.into())) + } + }) + .unwrap_or_else(|| generate_ref_error(scope, self.into())); + scope.local_args = None; value } @@ -289,7 +250,7 @@ impl<'source> ResolveValue<'source> for ast::InlineExpression<'source> { let (resolved_positional_args, resolved_named_args) = get_arguments(scope, arguments); - let func = scope.bundle.get_function(id.name); + let func = scope.bundle.get_entry_function(id.name); if let Some(func) = func { func(resolved_positional_args.as_slice(), &resolved_named_args) @@ -298,21 +259,18 @@ impl<'source> ResolveValue<'source> for ast::InlineExpression<'source> { } } ast::InlineExpression::VariableReference { id } => { - let arg = if let Some(args) = &scope.local_args { - args.get(&id.name) - } else { - scope.args.and_then(|args| args.get(&id.name)) - }; - if let Some(arg) = arg { + let args = scope.local_args.as_ref().or(scope.args); + + if let Some(arg) = args.and_then(|args| args.get(id.name)) { arg.clone() } else { - let displayable_node: DisplayableNode = self.into(); + let entry: DisplayableNode = self.into(); if scope.local_args.is_none() { scope .errors - .push(ResolverError::Reference(displayable_node.get_error())); + .push(ResolverError::Reference(entry.get_error())); } - FluentValue::Error(displayable_node) + FluentValue::Error(entry) } } ast::InlineExpression::Placeable { expression } => expression.resolve(scope), @@ -325,7 +283,7 @@ fn get_arguments<'bundle, R>( arguments: &'bundle Option>, ) -> ( Vec>, - HashMap<&'bundle str, FluentValue<'bundle>>, + HashMap>, ) where R: Borrow, @@ -339,7 +297,7 @@ where } for arg in named { - resolved_named_args.insert(arg.name.name, arg.value.resolve(scope)); + resolved_named_args.insert(arg.name.name.to_string(), arg.value.resolve(scope)); } } diff --git a/fluent-bundle/src/types.rs b/fluent-bundle/src/types.rs index 42960cf2..510007d8 100644 --- a/fluent-bundle/src/types.rs +++ b/fluent-bundle/src/types.rs @@ -21,7 +21,7 @@ use intl_pluralrules::PluralCategory; use crate::resolve::Scope; use crate::resource::FluentResource; -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +#[derive(Debug, PartialEq, Clone)] pub enum DisplayableNodeType { Message, Term, @@ -29,7 +29,7 @@ pub enum DisplayableNodeType { Function, } -#[derive(Debug, PartialEq, Eq, Copy, Clone, Hash)] +#[derive(Debug, PartialEq, Clone)] pub struct DisplayableNode<'source> { node_type: DisplayableNodeType, id: &'source str, @@ -38,11 +38,15 @@ pub struct DisplayableNode<'source> { impl<'source> DisplayableNode<'source> { pub fn get_error(&self) -> String { - match self.node_type { - DisplayableNodeType::Message => format!("Unknown message: {}", self), - DisplayableNodeType::Term => format!("Unknown term: {}", self), - DisplayableNodeType::Variable => format!("Unknown variable: {}", self), - DisplayableNodeType::Function => format!("Unknown function: {}", self), + if self.attribute.is_some() { + format!("Unknown attribute: {}", self) + } else { + match self.node_type { + DisplayableNodeType::Message => format!("Unknown message: {}", self), + DisplayableNodeType::Term => format!("Unknown term: {}", self), + DisplayableNodeType::Variable => format!("Unknown variable: {}", self), + DisplayableNodeType::Function => format!("Unknown function: {}", self), + } } } } @@ -62,37 +66,15 @@ impl<'source> fmt::Display for DisplayableNode<'source> { } } -impl<'source> From<&ast::Message<'source>> for DisplayableNode<'source> { - fn from(msg: &ast::Message<'source>) -> Self { - DisplayableNode { - node_type: DisplayableNodeType::Message, - id: msg.id.name, - attribute: None, - } - } -} - -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 { + ast::InlineExpression::MessageReference { id, attribute } => DisplayableNode { node_type: DisplayableNodeType::Message, id: id.name, attribute: attribute.as_ref().map(|attr| attr.name), }, - ast::InlineExpression::TermReference { - id, ref attribute, .. - } => DisplayableNode { + ast::InlineExpression::TermReference { id, attribute, .. } => DisplayableNode { node_type: DisplayableNodeType::Term, id: id.name, attribute: attribute.as_ref().map(|attr| attr.name), @@ -116,8 +98,8 @@ impl<'source> From<&ast::InlineExpression<'source>> for DisplayableNode<'source> pub enum FluentValue<'source> { String(Cow<'source, str>), Number(Cow<'source, str>), - None(), Error(DisplayableNode<'source>), + None, } impl<'source> FluentValue<'source> { @@ -158,8 +140,8 @@ impl<'source> FluentValue<'source> { match self { FluentValue::String(s) => s.clone(), FluentValue::Number(n) => n.clone(), - FluentValue::Error(d) => d.to_string().into(), - FluentValue::None() => "???".into(), + FluentValue::Error(d) => format!("{{{}}}", d.to_string()).into(), + FluentValue::None => "???".into(), } } } @@ -169,8 +151,8 @@ impl<'source> fmt::Display for FluentValue<'source> { match self { FluentValue::String(s) => f.write_str(s), FluentValue::Number(n) => f.write_str(n), - FluentValue::Error(d) => write!(f, "{}", d), - FluentValue::None() => f.write_str("???"), + FluentValue::Error(d) => write!(f, "{{{}}}", d), + FluentValue::None => f.write_str("???"), } } } diff --git a/fluent-bundle/tests/bundle_test.rs b/fluent-bundle/tests/bundle_test.rs deleted file mode 100644 index 94f79855..00000000 --- a/fluent-bundle/tests/bundle_test.rs +++ /dev/null @@ -1,103 +0,0 @@ -mod helpers; - -use std::collections::HashMap; - -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.get_message("foo").is_some()); - assert!(bundle.get_term("foo").is_none()); - assert!(bundle.get_message("bar").is_none()); - assert!(bundle.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); -} - -#[test] -fn bundle_use_isolating() { - let res = assert_get_resource_from_str_no_errors( - r#" --brand-name = Fluent -foo = Foo -bar = Foo { $name } bar { -brand-name } baz { "Literal" } - "#, - ); - let mut bundle = assert_get_bundle_no_errors(&res, None); - - // By default `assert_get_bundle_no_errors` sets isolating to false. - bundle.set_use_isolating(true); - - assert_format_no_errors(bundle.format("foo", None), "Foo"); - let mut args = HashMap::new(); - args.insert("name", "John".into()); - assert_format_no_errors( - bundle.format("bar", Some(&args)), - "Foo \u{2068}John\u{2069} bar Fluent baz Literal", - ); - - bundle.set_use_isolating(false); - - assert_format_no_errors(bundle.format("foo", None), "Foo"); - let mut args = HashMap::new(); - args.insert("name", "John".into()); - assert_format_no_errors( - bundle.format("bar", Some(&args)), - "Foo John bar Fluent baz Literal", - ); -} diff --git a/fluent-bundle/tests/compound.rs b/fluent-bundle/tests/compound.rs deleted file mode 100644 index 318e1c86..00000000 --- a/fluent-bundle/tests/compound.rs +++ /dev/null @@ -1,56 +0,0 @@ -mod helpers; -use fluent_bundle::errors::FluentError; -use fluent_bundle::resolve::ResolverError; - -use self::helpers::{ - 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; - -#[test] -fn format() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo - .attr = Attribute - .attr2 = Attribute 2 - ", - ); - let bundle = assert_get_bundle_no_errors(&res, Some("en")); - - let mut attrs = HashMap::new(); - 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".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/constructor_test.rs b/fluent-bundle/tests/constructor_test.rs deleted file mode 100644 index 935d556d..00000000 --- a/fluent-bundle/tests/constructor_test.rs +++ /dev/null @@ -1,95 +0,0 @@ -use std::sync::Arc; - -use fluent_bundle::FluentBundle; -use fluent_bundle::FluentResource; - -#[test] -fn bundle_new_from_str() { - let arr_of_str = ["x-testing"]; - let _: FluentBundle = FluentBundle::new(&arr_of_str); - let _: FluentBundle = FluentBundle::new(&arr_of_str[..]); - - let vec_of_str = vec!["x-testing"]; - let _: FluentBundle = FluentBundle::new(&vec_of_str); - let _: FluentBundle = FluentBundle::new(&vec_of_str[..]); - - let iter_of_str = ["x-testing"].iter(); - let vec_from_iter = iter_of_str.cloned().collect::>(); - let _: FluentBundle = FluentBundle::new(&vec_from_iter); - let _: FluentBundle = FluentBundle::new(&vec_from_iter[..]); -} - -#[test] -fn bundle_new_from_strings() { - let arr_of_strings = ["x-testing".to_string()]; - let arr_of_str = [arr_of_strings[0].as_str()]; - - let _: FluentBundle = FluentBundle::new(&arr_of_str); - let _: FluentBundle = FluentBundle::new(&arr_of_str[..]); - - let vec_of_strings = ["x-testing".to_string()]; - let vec_of_str = [vec_of_strings[0].as_str()]; - - let _: FluentBundle = FluentBundle::new(&vec_of_str); - let _: FluentBundle = FluentBundle::new(&vec_of_str[..]); - - let iter_of_strings = arr_of_strings.iter(); - let vec_from_iter = iter_of_strings - .map(|elem| elem.as_str()) - .collect::>(); - let _: FluentBundle = FluentBundle::new(&vec_from_iter); - let _: FluentBundle = FluentBundle::new(&vec_from_iter[..]); -} - -fn create_bundle<'a, 'b>(locales: &'b Vec<&'b str>) -> FluentBundle { - FluentBundle::new(locales) -} - -#[test] -fn bundle_locale_diff_scope() { - let locales = vec!["x-testing"]; - create_bundle(&locales); -} - -#[test] -fn bundle_new_owned() { - let locs = ["x-testing"]; - - let mut bundle = FluentBundle::new(&locs); - - let res = FluentResource::try_new("key = Value".to_string()).unwrap(); - let res2 = FluentResource::try_new("key2 = Value 2".to_string()).unwrap(); - - bundle.add_resource(res).expect("Added a resource"); - bundle.add_resource(res2).expect("Added a resource"); -} - -#[test] -fn bundle_new_borrowed() { - let locs = ["x-testing"]; - - let mut bundle = FluentBundle::new(&locs); - - let res = FluentResource::try_new("key = Value".to_string()).unwrap(); - let res2 = FluentResource::try_new("key2 = Value 2".to_string()).unwrap(); - - bundle.add_resource(&res).expect("Added a resource"); - bundle.add_resource(&res2).expect("Added a resource"); -} - -#[test] -fn bundle_new_arc() { - let locs = ["x-testing"]; - - let mut bundle = FluentBundle::new(&locs); - - let res = FluentResource::try_new("key = Value".to_string()).unwrap(); - let res2 = FluentResource::try_new("key2 = Value 2".to_string()).unwrap(); - - bundle - .add_resource(Arc::new(res)) - .expect("Added a resource"); - bundle - .add_resource(Arc::new(res2)) - .expect("Added a resource"); -} diff --git a/fluent-bundle/tests/fixtures/arguments.yaml b/fluent-bundle/tests/fixtures/arguments.yaml new file mode 100644 index 00000000..6ffa2679 --- /dev/null +++ b/fluent-bundle/tests/fixtures/arguments.yaml @@ -0,0 +1,133 @@ +suites: + - + name: Variables + suites: + - + name: in values + resources: + - + source: |- + foo = Foo { $num } + bar = { foo } + baz = + .attr = Baz Attribute { $num } + qux = { "a" -> + *[a] Baz Variant A { $num } + } + tests: + - + name: can be used in the message value + asserts: + - + id: foo + args: + num: 3 + value: Foo 3 + - + name: can be used in the message value which is referenced + asserts: + - + id: bar + args: + num: 3 + value: Foo 3 + - + name: can be used in an attribute + asserts: + - + id: baz + attribute: attr + args: + num: 3 + value: Baz Attribute 3 + - + name: can be used in a variant + asserts: + - + id: qux + args: + num: 3 + value: Baz Variant A 3 + - + name: in selectors + resources: + - + source: |- + foo = { $num -> + *[3] Foo + } + tests: + - + name: can be used as a selector + asserts: + - + id: foo + args: + num: 3 + value: Foo + - + name: in function calls + resources: + - + source: |- + foo = { NUMBER($num) } + bundles: + - + functions: + - NUMBER + tests: + - + name: can be a positional argument + asserts: + - + id: foo + args: + num: 3 + value: 3 + - + name: simple errors + resources: + - + source: |- + foo = { $arg } + tests: + - + name: falls back to argument's name if it's missing + asserts: + - + id: foo + value: "{$arg}" + errors: + - + type: Reference + desc: "Unknown variable: $arg" + - + name: and strings + resources: + - + source: |- + foo = { $arg } + tests: + - + name: can be a string + asserts: + - + id: foo + args: + arg: Argument + value: Argument + - + name: and numbers + resources: + - + source: |- + foo = { $arg } + tests: + - + name: can be a number + asserts: + - + id: foo + args: + arg: 1 + value: 1 \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/attributes.yaml b/fluent-bundle/tests/fixtures/attributes.yaml new file mode 100644 index 00000000..af836e75 --- /dev/null +++ b/fluent-bundle/tests/fixtures/attributes.yaml @@ -0,0 +1,178 @@ +suites: + - + name: Attributes + suites: + - + name: missing + resources: + - + source: |- + foo = Foo + bar = Bar + .attr = Bar Attribute + baz = { foo } Baz + qux = { foo } Qux + .attr = Qux Attribute + ref-foo = { foo.missing } + ref-bar = { bar.missing } + ref-baz = { baz.missing } + ref-qux = { qux.missing } + tests: + - + name: falls back to id.attr for entities with string values and no attributes + asserts: + - + id: ref-foo + value: "{foo.missing}" + errors: + - + type: Reference + desc: "Unknown attribute: foo.missing" + - + name: falls back to id.attr for entities with string values and other attributes + asserts: + - + id: ref-bar + value: "{bar.missing}" + errors: + - + type: Reference + desc: "Unknown attribute: bar.missing" + - + name: falls back to id.attr for entities with pattern values and no attributes + asserts: + - + id: ref-baz + value: "{baz.missing}" + errors: + - + type: Reference + desc: "Unknown attribute: baz.missing" + - + name: falls back to id.attr for entities with pattern values and other attributes + asserts: + - + id: ref-qux + value: "{qux.missing}" + errors: + - + type: Reference + desc: "Unknown attribute: qux.missing" + - + name: with string values + resources: + - + source: |- + foo = Foo + .attr = Foo Attribute + bar = { foo } Bar + .attr = Bar Attribute + ref-foo = { foo.attr } + ref-bar = { bar.attr } + tests: + - + name: can be referenced for entities with string values + asserts: + - + id: ref-foo + value: Foo Attribute + - + name: can be formatted directly for entities with string values + asserts: + - + id: foo + attribute: attr + value: Foo Attribute + - + name: can be referenced for entities with pattern values + asserts: + - + id: ref-bar + value: Bar Attribute + - + name: can be formatted directly for entities with pattern values + asserts: + - + id: bar + attribute: attr + value: Bar Attribute + - + name: with simple pattern values + resources: + - + source: |- + foo = Foo + bar = Bar + .attr = { foo } Attribute + baz = { foo } Baz + .attr = { foo } Attribute + qux = Qux + .attr = { qux } Attribute + ref-bar = { bar.attr } + ref-baz = { baz.attr } + ref-qux = { qux.attr } + tests: + - + name: can be referenced for entities with string values + asserts: + - + id: ref-bar + value: Foo Attribute + - + name: can be formatted directly for entities with string values + asserts: + - + id: bar + attribute: attr + value: Foo Attribute + - + name: can be referenced for entities with simple pattern values + asserts: + - + id: ref-baz + value: Foo Attribute + - + name: can be formatted directly for entities with simple pattern values + asserts: + - + id: baz + attribute: attr + value: Foo Attribute + - + name: works with self-references + asserts: + - + id: ref-qux + value: Qux Attribute + - + name: can be formatted directly when it uses a self-reference + asserts: + - + id: qux + attribute: attr + value: Qux Attribute + - + name: with values with select expressions + resources: + - + source: |- + foo = Foo + .attr = { "a" -> + [a] A + *[b] B + } + ref-foo = { foo.attr } + tests: + - + name: can be referenced + asserts: + - + id: ref-foo + value: A + - + name: can be formatted directly + asserts: + - + id: foo + attribute: attr + value: A diff --git a/fluent-bundle/tests/fixtures/context.yaml b/fluent-bundle/tests/fixtures/context.yaml new file mode 100644 index 00000000..3e320d36 --- /dev/null +++ b/fluent-bundle/tests/fixtures/context.yaml @@ -0,0 +1,141 @@ +suites: + - + name: Bundle + suites: + - + name: addResource + resources: + - + source: |- + foo = Foo + -bar = Bar + tests: + - + name: adds messages + asserts: + - + id: foo + value: Foo + - + id: bar + missing: true + - + name: allowOverrides + resources: + - + source: key = Foo + tests: + - + name: addResource allowOverrides is false + resources: + - + source: key = Bar + bundles: + - + errors: + - + type: Overriding + asserts: + - + id: key + value: Foo + - + name: addResource allowOverrides is true + skip: true + resources: + - + source: key = Bar + asserts: + - + id: key + value: Bar + - + name: hasMessage + resources: + - + source: |- + foo = Foo + bar = + .attr = Bar Attr + -term = Term + # ERROR No value. + err1 = + # ERROR Broken value. + err2 = {} + # ERROR No attribute value. + err3 = + .attr = + # ERROR Broken attribute value. + err4 = + .attr1 = Attr + .attr2 = {} + errors: + - + type: Parser + - + type: Parser + - + type: Parser + - + type: Parser + tests: + - + name: returns true only for public messages + asserts: + - + id: foo + missing: false + - + name: returns false for terms and missing messages + asserts: + - + id: -term + missing: true + - + id: missing + missing: true + - + id: -missing + missing: true + - + name: returns false for broken messages + asserts: + - + id: err1 + missing: true + - + id: err2 + missing: true + - + id: err3 + missing: true + - + id: err4 + # XXX: Difference from JS. We handle partial messages + missing: false + - + name: getMessage + resources: + - + source: |- + foo = Foo + -bar = Bar + tests: + - + name: returns public messages + asserts: + - + id: foo + missing: false + - + name: returns undefined for terms and missing messages + asserts: + - + id: -bar + missing: true + - + id: baz + missing: true + - + id: -baz + missing: true \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/defaults.yaml b/fluent-bundle/tests/fixtures/defaults.yaml new file mode 100644 index 00000000..5f6051c6 --- /dev/null +++ b/fluent-bundle/tests/fixtures/defaults.yaml @@ -0,0 +1,6 @@ +# Those are default settings for all tests + +bundle: + useIsolating: false + locales: + - en-US \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/errors.yaml b/fluent-bundle/tests/fixtures/errors.yaml new file mode 100644 index 00000000..b488fb59 --- /dev/null +++ b/fluent-bundle/tests/fixtures/errors.yaml @@ -0,0 +1,21 @@ +suites: + - + name: Errors + resources: + - + source: |- + foo = {$one} and {$two} + tests: + - + name: Reporting into an array + asserts: + - + id: foo + value: "{$one} and {$two}" + errors: + - + type: Reference + desc: "Unknown variable: $one" + - + type: Reference + desc: "Unknown variable: $two" \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/functions.yaml b/fluent-bundle/tests/fixtures/functions.yaml new file mode 100644 index 00000000..c18afca1 --- /dev/null +++ b/fluent-bundle/tests/fixtures/functions.yaml @@ -0,0 +1,85 @@ +suites: + - + name: Functions + suites: + - + name: missing + resources: + - + source: |- + foo = { MISSING("Foo") } + tests: + - + name: falls back to the name of the function + asserts: + - + id: foo + value: "{MISSING()}" + errors: + - + type: Reference + desc: "Unknown function: MISSING()" + - + name: arguments + resources: + - + source: |- + 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)) } + bundles: + - + functions: + - IDENTITY + tests: + - + name: falls back when arguments don't match the arity + asserts: + - + id: pass-nothing + ## XXX: Difference from JS + value: "???" + - + name: accepts strings + asserts: + - + id: pass-string + value: a + - + name: accepts numbers + asserts: + - + id: pass-number + value: 1 + - + name: accepts entities + asserts: + - + id: pass-message + value: Foo + - + name: accepts attributes + asserts: + - + id: pass-attr + value: Attribute + - + name: accepts variables + asserts: + - + id: pass-variable + args: + var: Variable + value: Variable + - + name: accepts function calls + asserts: + - + id: pass-function-call + value: 1 \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/functions_runtime.yaml b/fluent-bundle/tests/fixtures/functions_runtime.yaml new file mode 100644 index 00000000..1acebbef --- /dev/null +++ b/fluent-bundle/tests/fixtures/functions_runtime.yaml @@ -0,0 +1,29 @@ +suites: + - + name: Runtime-specific functions + suites: + - + name: passing into the constructor + resources: + - + source: |- + foo = { CONCAT("Foo", "Bar") } + bar = { SUM(1, 2) } + bundles: + - + functions: + - CONCAT + - SUM + tests: + - + name: works for strings + asserts: + - + id: foo + value: FooBar + - + name: works for numbers + asserts: + - + id: bar + value: 3 \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/isolating.yaml b/fluent-bundle/tests/fixtures/isolating.yaml new file mode 100644 index 00000000..0aaf4065 --- /dev/null +++ b/fluent-bundle/tests/fixtures/isolating.yaml @@ -0,0 +1,72 @@ +suites: + - + name: Isolating interpolations + resources: + - + source: |- + foo = Foo + bar = { foo } Bar + baz = { $arg } Baz + qux = { bar } { baz } + bundles: + - + useIsolating: true + tests: + - + name: isolates interpolated message references + skip: true + asserts: + - + id: bar + value: "\u2068Foo\u2069 Bar" + - + name: isolates interpolated string-typed variables + asserts: + - + id: baz + args: + arg: Arg + value: "\u2068Arg\u2069 Baz" + - + name: isolates interpolated number-typed variables + asserts: + - + id: baz + args: + arg: 1 + value: "\u20681\u2069 Baz" + - + name: isolates interpolated date-typed variables + skip: true + asserts: + - + id: baz + args: + arg: 1976-07-31 + value: "\u20681976-07-31\u2069 Baz" + - + name: isolates complex interpolations + skip: true + asserts: + - + id: qux + args: + arg: Arg + value: "\u2068\u2068Foo\u2069 Bar\u2069 \u2068\u2068Arg\u2069 Baz\u2069" + - + name: Skip isolation cases + resources: + - + source: |- + -brand-short-name = Amaya + foo = { -brand-short-name } + bundles: + - + useIsolating: true + tests: + - + name: skips isolation if the only element is a placeable + asserts: + - + id: foo + value: "Amaya" \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/literals.yaml b/fluent-bundle/tests/fixtures/literals.yaml new file mode 100644 index 00000000..aadc5b52 --- /dev/null +++ b/fluent-bundle/tests/fixtures/literals.yaml @@ -0,0 +1,69 @@ +suites: + - + name: Literals as selectors + tests: + - + name: a matching string literal selector + resources: + - + source: |- + foo = { "a" -> + [a] A + *[b] B + } + asserts: + - + id: foo + value: A + - + name: a non-matching string literal selector + resources: + - + source: |- + foo = { "c" -> + [a] A + *[b] B + } + asserts: + - + id: foo + value: B + - + name: a matching number literal selector + resources: + - + source: |- + foo = { 0 -> + [0] A + *[1] B + } + asserts: + - + id: foo + value: A + - + name: a non-matching number literal selector + resources: + - + source: |- + foo = { 2 -> + [0] A + *[1] B + } + asserts: + - + id: foo + value: B + - + name: a number literal selector matching a plural category + resources: + - + source: |- + foo = { 1 -> + [one] A + *[other] B + } + asserts: + - + id: foo + value: A \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/macros.yaml b/fluent-bundle/tests/fixtures/macros.yaml new file mode 100644 index 00000000..eb0c2d40 --- /dev/null +++ b/fluent-bundle/tests/fixtures/macros.yaml @@ -0,0 +1,392 @@ +suites: + - + name: Macros + suites: + - + name: References and calls + resources: + - + source: |- + -bar = Bar + term-ref = {-bar} + term-call = {-bar()} + tests: + - + name: terms can be referenced without parens + asserts: + - + id: term-ref + value: Bar + - + name: terms can be parameterized + asserts: + - + id: term-call + value: Bar + - + name: Passing arguments + resources: + - + source: |- + -foo = Foo {$arg} + ref-foo = {-foo} + call-foo-no-args = {-foo()} + call-foo-with-expected-arg = {-foo(arg: 1)} + call-foo-with-other-arg = {-foo(other: 3)} + tests: + - + name: Not parameterized, no externals + asserts: + - + id: ref-foo + value: Foo {$arg} + - + name: Not parameterized but with externals + asserts: + - + id: ref-foo + args: + arg: 1 + value: Foo {$arg} + - + name: No arguments, no externals + asserts: + - + id: call-foo-no-args + value: Foo {$arg} + - + name: No arguments, but with externals + asserts: + - + id: call-foo-no-args + args: + arg: 1 + value: Foo {$arg} + - + name: With expected args, no externals + asserts: + - + id: call-foo-with-expected-arg + value: Foo 1 + - + name: With expected args, and with externals + asserts: + - + id: call-foo-with-expected-arg + args: + arg: 5 + value: Foo 1 + - + name: With other args, no externals + asserts: + - + id: call-foo-with-other-arg + value: Foo {$arg} + - + name: With other args, and with externals + asserts: + - + id: call-foo-with-other-arg + args: + arg: 5 + value: Foo {$arg} + - + name: Nesting message references + resources: + - + source: |- + foo = Foo {$arg} + -bar = {foo} + ref-bar = {-bar} + call-bar = {-bar()} + call-bar-with-arg = {-bar(arg: 1)} + tests: + - + name: No parameterization, no externals + asserts: + - + id: ref-bar + value: Foo {$arg} + - + name: No parameterization, but with externals + asserts: + - + id: ref-bar + args: + arg: 5 + value: Foo {$arg} + - + name: No arguments, no externals + asserts: + - + id: call-bar + value: Foo {$arg} + - + name: No arguments, but with externals + asserts: + - + id: call-bar + args: + arg: 5 + value: Foo {$arg} + - + name: With arguments, no externals + asserts: + - + id: call-bar-with-arg + value: Foo 1 + - + name: With arguments and with externals + asserts: + - + id: call-bar-with-arg + args: + arg: 5 + value: Foo 1 + - + name: Nesting term references + resources: + - + source: |- + -foo = Foo {$arg} + -bar = {-foo} + -baz = {-foo()} + -qux = {-foo(arg: 1)} + ref-bar = {-bar} + ref-baz = {-baz} + ref-qux = {-qux} + call-bar-no-args = {-bar()} + call-baz-no-args = {-baz()} + call-qux-no-args = {-qux()} + call-bar-with-arg = {-bar(arg: 2)} + call-baz-with-arg = {-baz(arg: 2)} + call-qux-with-arg = {-qux(arg: 2)} + call-qux-with-other = {-qux(other: 3)} + tests: + - + name: No parameterization, no parameterization, no externals + asserts: + - + id: ref-bar + value: Foo {$arg} + - + name: No parameterization, no parameterization, with externals + asserts: + - + id: ref-bar + args: + arg: 5 + value: Foo {$arg} + - + name: No parameterization, no arguments, no externals + asserts: + - + id: ref-baz + value: Foo {$arg} + - + name: No parameterization, no arguments, with externals + asserts: + - + id: ref-baz + args: + arg: 5 + value: Foo {$arg} + - + name: No parameterization, with arguments, no externals + asserts: + - + id: ref-qux + value: Foo 1 + - + name: No parameterization, with arguments, with externals + asserts: + - + id: ref-qux + args: + arg: 5 + value: Foo 1 + - + name: No arguments, no parametrization, no externals + asserts: + - + id: call-bar-no-args + value: Foo {$arg} + - + name: No arguments, no parametrization, with externals + asserts: + - + id: call-bar-no-args + args: + arg: 5 + value: Foo {$arg} + - + name: No arguments, no arguments, no externals + asserts: + - + id: call-baz-no-args + value: Foo {$arg} + - + name: No arguments, no arguments, with externals + asserts: + - + id: call-baz-no-args + args: + arg: 5 + value: Foo {$arg} + - + name: No arguments, with arguments, no externals + asserts: + - + id: call-qux-no-args + value: Foo 1 + - + name: No arguments, with arguments, with externals + asserts: + - + id: call-qux-no-args + args: + arg: 5 + value: Foo 1 + - + name: With arguments, no parametrization, no externals + asserts: + - + id: call-bar-with-arg + value: Foo {$arg} + - + name: With arguments, no parametrization, with externals + asserts: + - + id: call-bar-with-arg + args: + arg: 5 + value: Foo {$arg} + - + name: With arguments, no arguments, no externals + asserts: + - + id: call-baz-with-arg + value: Foo {$arg} + - + name: With arguments, no arguments, with externals + asserts: + - + id: call-baz-with-arg + args: + arg: 5 + value: Foo {$arg} + - + name: With arguments, with arguments, no externals + asserts: + - + id: call-qux-with-arg + value: Foo 1 + - + name: With arguments, with arguments, with externals + asserts: + - + id: call-qux-with-arg + args: + arg: 5 + value: Foo 1 + - + name: With unexpected arguments, with arguments, no externals + asserts: + - + id: call-qux-with-other + value: Foo 1 + - + name: With unexpected arguments, with arguments, with externals + asserts: + - + id: call-qux-with-other + args: + arg: 5 + value: Foo 1 + - + name: Parameterized term attributes + resources: + - + source: |- + -ship = Ship + .gender = {$style -> + *[traditional] neuter + [chicago] feminine + } + ref-attr = {-ship.gender -> + *[masculine] He + [feminine] She + [neuter] It + } + call-attr-no-args = {-ship.gender() -> + *[masculine] He + [feminine] She + [neuter] It + } + call-attr-with-expected-arg = {-ship.gender(style: "chicago") -> + *[masculine] He + [feminine] She + [neuter] It + } + call-attr-with-other-arg = {-ship.gender(other: 3) -> + *[masculine] He + [feminine] She + [neuter] It + } + tests: + - + name: Not parameterized, no externals + asserts: + - + id: ref-attr + value: It + - + name: Not parameterized but with externals + asserts: + - + id: ref-attr + args: + attr: chicago + value: It + - + name: No arguments, no externals + asserts: + - + id: call-attr-no-args + value: It + - + name: No arguments, but with externals + asserts: + - + id: call-attr-no-args + args: + style: chicago + value: It + - + name: With expected args, no externals + asserts: + - + id: call-attr-with-expected-arg + value: She + - + name: With expected args, and with externals + asserts: + - + id: call-attr-with-expected-arg + args: + style: chicago + value: She + - + name: With other args, no externals + asserts: + - + id: call-attr-with-other-arg + value: It + - + name: With other args, and with externals + asserts: + - + id: call-attr-with-other-arg + args: + style: chicago + value: It \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/patterns.yaml b/fluent-bundle/tests/fixtures/patterns.yaml new file mode 100644 index 00000000..167962f7 --- /dev/null +++ b/fluent-bundle/tests/fixtures/patterns.yaml @@ -0,0 +1,218 @@ +suites: + - + name: Patterns + suites: + - + name: Simple string value + resources: + - + source: foo = Foo + tests: + - + name: returns the value + asserts: + - + id: foo + value: Foo + - + name: Complex string value + resources: + - + source: |- + foo = Foo + -bar = Bar + ref-message = { foo } + ref-term = { -bar } + ref-missing-message = { missing } + ref-missing-term = { -missing } + ref-malformed = { malformed + errors: + - + type: Parser + tests: + - + name: resolves the reference to a message + asserts: + - + id: ref-message + value: Foo + - + name: resolves the reference to a term + asserts: + - + id: ref-term + value: Bar + - + name: returns the id if a message reference is missing + asserts: + - + id: ref-missing-message + value: "{missing}" + errors: + - + type: Reference + desc: "Unknown message: missing" + - + name: returns the id if a term reference is missing + asserts: + - + id: ref-missing-term + value: "{-missing}" + errors: + - + type: Reference + desc: "Unknown term: -missing" + - + name: Complex string referencing a message with null value + resources: + - + source: |- + foo = + .attr = Foo Attr + bar = { foo } Bar + tests: + - + name: returns the null value + skip: true + asserts: + - + id: foo + value: Foo + - + name: formats the attribute + asserts: + - + id: foo + attribute: attr + value: Foo Attr + - + name: formats ??? when the referenced message has no value and no default + asserts: + - + id: bar + # XXX: Difference from JS + value: "{foo} Bar" + errors: + - + type: Reference + desc: "Unknown message: foo" + - + name: Cyclic reference + resources: + - + source: |- + foo = { bar } + bar = { foo } + tests: + - + name: returns ??? + asserts: + - + id: foo + value: "{foo}" + errors: + - + type: Cyclic + - + name: Cyclic self-reference + resources: + - + source: foo = { foo } + tests: + - + name: returns the raw string + asserts: + - + id: foo + value: "{foo}" + errors: + - + type: Cyclic + - + name: Cyclic self-reference in a member + resources: + - + source: |- + foo = + { $sel -> + *[a] { foo } + [b] Bar + } + bar = { foo } + tests: + - + name: returns ??? + asserts: + - + id: foo + args: + sel: a + value: "{foo}" + errors: + - + type: Cyclic + - + name: returns the other member if requested + asserts: + - + id: foo + args: + sel: b + value: Bar + - + name: Cyclic reference in a selector + skip: true + resources: + - + source: |- + -foo = + { -bar.attr -> + *[a] Foo + } + -bar = Bar + .attr = { -foo } + foo = { -foo } + tests: + - + name: returns the default variant + asserts: + - + id: foo + value: Foo + errors: + - + type: Cyclic + - + name: Cyclic self-reference in a selector + skip: true + resources: + - + source: |- + -foo = + { -bar.attr -> + *[a] Foo + } + .attr = a + -bar = + { -foo.attr -> + *[a] Bar + } + .attr = { -foo } + foo = { -foo } + bar = { -bar } + tests: + - + name: returns the default variant + asserts: + - + id: foo + value: Foo + errors: + - + type: Cyclic + - + name: can reference an attribute + asserts: + - + id: bar + value: Bar \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/primitives.yaml b/fluent-bundle/tests/fixtures/primitives.yaml new file mode 100644 index 00000000..7cb36df3 --- /dev/null +++ b/fluent-bundle/tests/fixtures/primitives.yaml @@ -0,0 +1,141 @@ +suites: + - + name: Primitives + suites: + - + name: Numbers + resources: + - + source: |- + one = { 1 } + select = { 1 -> + *[0] Zero + [1] One + } + tests: + - + name: can be used in a placeable + asserts: + - + id: one + value: 1 + - + name: can be used as a selector + asserts: + - + id: select + value: One + - + name: Simple string value + resources: + - + source: |- + 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 + } + tests: + - + name: can be used as a value + asserts: + - + id: foo + value: Foo + - + name: can be used in a placeable + asserts: + - + id: placeable-literal + value: Foo Bar + - + name: can be a value of a message referenced in a placeable + asserts: + - + id: placeable-message + value: Foo Bar + - + name: can be a selector + asserts: + - + id: selector-literal + value: Member 1 + - + name: can be used as an attribute value + asserts: + - + id: bar + attribute: attr + value: Bar Attribute + - + name: can be a value of an attribute used in a placeable + asserts: + - + id: placeable-attr + value: Bar Attribute + - + name: can be a value of an attribute used as a selector + asserts: + - + id: selector-attr + value: Member 3 + - + name: Complex string value + resources: + - + source: |- + foo = Foo + bar = { foo }Bar + placeable-message = { bar }Baz + baz = + .attr = { bar }BazAttribute + -bazTerm = Value + .attr = { bar }BazAttribute + placeable-attr = { baz.attr } + # XXX: This is different from JS fixture which + # illegally uses message attribute as selector. + selector-attr = { -bazTerm.attr -> + [FooBarBazAttribute] FooBarBaz + *[other] Other + } + tests: + - + name: can be used as a value + asserts: + - + id: bar + value: FooBar + - + name: can be a value of a message referenced in a placeable + asserts: + - + id: placeable-message + value: FooBarBaz + - + name: can be used as an attribute value + asserts: + - + id: baz + attribute: attr + value: FooBarBazAttribute + - + name: can be a value of an attribute used in a placeable + asserts: + - + id: placeable-attr + value: FooBarBazAttribute + - + name: can be a value of an attribute used as a selector + asserts: + - + id: selector-attr + value: FooBarBaz \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/select_expression.yaml b/fluent-bundle/tests/fixtures/select_expression.yaml new file mode 100644 index 00000000..5614dfe5 --- /dev/null +++ b/fluent-bundle/tests/fixtures/select_expression.yaml @@ -0,0 +1,151 @@ +suites: + - + name: Select expressions + tests: + - + name: missing selector + resources: + - + source: |- + select = {$none -> + [a] A + *[b] B + } + asserts: + - + id: select + value: B + errors: + - + type: Reference + desc: "Unknown variable: $none" + suites: + - + name: string selectors + tests: + - + name: matching selector + resources: + - + source: |- + select = {$selector -> + [a] A + *[b] B + } + asserts: + - + id: select + value: A + args: + selector: a + - + name: non-matching selector + resources: + - + source: |- + select = {$selector -> + [a] A + *[b] B + } + asserts: + - + id: select + value: B + args: + selector: c + - + name: number selectors + tests: + - + name: matching selector + resources: + - + source: |- + select = {$selector -> + [0] A + *[1] B + } + asserts: + - + id: select + value: A + args: + selector: 0 + - + name: non-matching selector + resources: + - + source: |- + select = {$selector -> + [0] A + *[1] B + } + asserts: + - + id: select + value: B + args: + selector: 2 + - + name: plural categories + tests: + - + name: matching number selector + resources: + - + source: |- + select = {$selector -> + [one] A + *[other] B + } + asserts: + - + id: select + value: A + args: + selector: 1 + - + name: matching string selector + resources: + - + source: |- + select = {$selector -> + [one] A + *[other] B + } + asserts: + - + id: select + value: A + args: + selector: one + - + name: non-matching number selector + resources: + - + source: |- + select = {$selector -> + [one] A + *[default] D + } + asserts: + - + id: select + value: D + args: + selector: 2 + - + name: non-matching string selector + resources: + - + source: |- + select = {$selector -> + [one] A + *[default] D + } + asserts: + - + id: select + value: D + args: + selector: other \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/values_format.yaml b/fluent-bundle/tests/fixtures/values_format.yaml new file mode 100644 index 00000000..90688f4b --- /dev/null +++ b/fluent-bundle/tests/fixtures/values_format.yaml @@ -0,0 +1,74 @@ +suites: + - + name: Formatting values + resources: + - + source: |- + 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 + tests: + - + name: returns the value + asserts: + - + id: key1 + value: Value 1 + - + name: returns the default variant + asserts: + - + id: key2 + value: B2 + errors: + - + type: Reference + desc: "Unknown variable: $sel" + - + name: returns the value if it is a pattern + asserts: + - + id: key3 + value: Value 3 + - + name: returns the default variant if it is a pattern + asserts: + - + id: key4 + value: B4 + errors: + - + type: Reference + desc: "Unknown variable: $sel" + - + name: returns {???} when trying to format a null value + skip: true + asserts: + - + id: key5 + value: "{???}" + errors: + - + type: Reference + desc: "Unknown variable: $sel" + - + name: allows to pass traits directly to bundle.formatPattern + asserts: + - + id: key5 + attribute: a + value: "A5" + - + id: key5 + attribute: b + value: "B5" \ No newline at end of file diff --git a/fluent-bundle/tests/fixtures/values_ref.yaml b/fluent-bundle/tests/fixtures/values_ref.yaml new file mode 100644 index 00000000..cd78d871 --- /dev/null +++ b/fluent-bundle/tests/fixtures/values_ref.yaml @@ -0,0 +1,140 @@ +suites: + - + name: Referencing values + resources: + - + source: |- + 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 } + ref12 = { key5.c } + ref13 = { key6 } + ref14 = { key6.a } + ref15 = { -key6 } + ref16 = { -key6.a -> + *[a] A + } + tests: + - + name: references the value + asserts: + - + id: ref1 + value: Value 1 + - + name: references the default variant + asserts: + - + id: ref2 + value: B2 + - + name: references the value if it is a pattern + asserts: + - + id: ref3 + value: Value 3 + - + name: references the default variant if it is a pattern + asserts: + - + id: ref4 + value: B4 + - + name: falls back to id if there is no value + asserts: + - + id: ref5 + value: "{key5}" + errors: + - + type: Reference + desc: "Unknown message: key5" + - + name: references the variants + asserts: + - + id: ref6 + value: A2 + - + id: ref7 + value: B2 + - + name: references the variants which are patterns + asserts: + - + id: ref8 + value: A4 + - + id: ref9 + value: B4 + - + name: references the attributes + asserts: + - + id: ref10 + value: A5 + - + id: ref11 + value: B5 + - + id: ref12 + value: "{key5.c}" + errors: + - + type: Reference + desc: "Unknown attribute: key5.c" + - + name: missing message reference + asserts: + - + id: ref13 + value: "{key6}" + errors: + - + type: Reference + desc: "Unknown message: key6" + - + id: ref14 + value: "{key6.a}" + errors: + - + type: Reference + desc: "Unknown attribute: key6.a" + - + name: missing term reference + asserts: + - + id: ref15 + value: "{-key6}" + errors: + - + type: Reference + desc: "Unknown term: -key6" + - + id: ref16 + value: "A" + errors: + - + type: Reference + desc: "Unknown attribute: -key6.a" \ No newline at end of file diff --git a/fluent-bundle/tests/format.rs b/fluent-bundle/tests/format.rs deleted file mode 100644 index a51a5152..00000000 --- a/fluent-bundle/tests/format.rs +++ /dev/null @@ -1,34 +0,0 @@ -mod helpers; - -use self::helpers::{ - assert_format_no_errors, assert_format_none, assert_get_bundle_no_errors, - assert_get_resource_from_str_no_errors, -}; - -#[test] -fn format() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo - .attr = Attribute --term = Term - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("foo", None), "Foo"); - - assert_format_no_errors(bundle.format("foo.attr", None), "Attribute"); - - assert_format_none(bundle.format("foo.missing", None)); - - assert_format_none(bundle.format("foo.attr.nested", None)); - - assert_format_none(bundle.format("missing", None)); - - assert_format_none(bundle.format("missing.attr", None)); - - assert_format_none(bundle.format("-term", None)); - - assert_format_none(bundle.format("-term.attr", None)); -} diff --git a/fluent-bundle/tests/functions_runtime_test.rs b/fluent-bundle/tests/functions_runtime_test.rs deleted file mode 100644 index ded800a7..00000000 --- a/fluent-bundle/tests/functions_runtime_test.rs +++ /dev/null @@ -1,50 +0,0 @@ -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 deleted file mode 100644 index 256d7515..00000000 --- a/fluent-bundle/tests/functions_test.rs +++ /dev/null @@ -1,74 +0,0 @@ -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 0b0f1651..28c4cf27 100644 --- a/fluent-bundle/tests/helpers/mod.rs +++ b/fluent-bundle/tests/helpers/mod.rs @@ -1,57 +1,192 @@ -use std::borrow::Borrow; -use std::borrow::Cow; +use std::collections::HashMap; +use std::fs::File; +use std::io; +use std::io::Read; -use fluent_bundle::bundle::Message; -use fluent_bundle::errors::FluentError; -use fluent_bundle::{FluentBundle, FluentResource}; +use serde::{Deserialize, Serialize}; -#[allow(dead_code)] -pub fn assert_format_none(result: Option<(Cow, Vec)>) { - assert!(result.is_none()); +#[macro_export] +macro_rules! test { + ($desc:stmt, $closure:block) => {{ + $closure + }}; } -#[allow(dead_code)] -pub fn assert_format_no_errors(result: Option<(Cow, Vec)>, expected: &str) { - assert!(result.is_some()); - assert_eq!(result, Some((expected.into(), vec![]))); +#[macro_export] +macro_rules! assert_format { + ($bundle:expr, $id:expr, $args:expr, $expected:expr) => { + let msg = $bundle.get_message($id).expect("Message doesn't exist"); + let mut errors = vec![]; + assert!(msg.value.is_some()); + assert_eq!( + $bundle.format_pattern(&msg.value.unwrap(), $args, &mut errors), + $expected + ); + assert!(errors.is_empty()); + }; + ($bundle:expr, $id:expr, $args:expr, $expected:expr, $errors:expr) => { + let msg = $bundle.get_message($id).expect("Message doesn't exist."); + let mut errors = vec![]; + assert!(msg.value.is_some()); + assert_eq!( + $bundle.format_pattern(&msg.value.unwrap(), $args, &mut errors), + $expected + ); + assert_eq!(errors, $errors); + }; } -#[allow(dead_code)] -pub fn assert_format( - result: Option<(Cow, Vec)>, - expected: &str, - errors: Vec, -) { - assert!(result.is_some()); - assert_eq!(result, Some((expected.into(), errors))); +#[macro_export] +macro_rules! assert_format_none { + ($bundle:expr, $id:expr) => { + let msg = $bundle.get_message($id).expect("Message doesn't exist"); + assert!(msg.value.is_none()); + }; } -#[allow(dead_code)] -pub fn assert_compound_no_errors(result: Option<(Message, Vec)>, expected: Message) { - assert_eq!(result, Some((expected, vec![]))); +#[macro_export] +macro_rules! assert_format_attr { + ($bundle:expr, $id:expr, $name:expr, $args:expr, $expected:expr) => { + let msg = $bundle.get_message($id).expect("Message doesn't exists"); + let mut errors = vec![]; + let attr = msg.attributes.get($name).expect("Attribute exists"); + assert_eq!($bundle.format_pattern(&attr, $args, &mut errors), $expected); + assert!(errors.is_empty()); + }; } -#[allow(dead_code)] -pub fn assert_compound( - result: Option<(Message, Vec)>, - expected: Message, - errors: Vec, -) { - assert_eq!(result, Some((expected, errors))); +#[macro_export] +macro_rules! assert_get_resource_from_str { + ($source:expr) => { + FluentResource::try_new($source.to_owned()).expect("Failed to parse an FTL resource.") + }; } -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.") +#[macro_export] +macro_rules! assert_get_bundle { + ($res:expr) => {{ + let mut bundle: FluentBundle<&FluentResource> = FluentBundle::new(&["x-testing"]); + bundle.set_use_isolating(false); + bundle + .add_resource($res) + .expect("Failed to add FluentResource to FluentBundle."); + bundle + }}; } -pub fn assert_get_bundle_no_errors<'a, R: Borrow>( - res: R, - locale: Option<&str>, -) -> FluentBundle { - let mut bundle = FluentBundle::new(&[locale.unwrap_or("x-testing")]); - bundle.set_use_isolating(false); - bundle - .add_resource(res) - .expect("Failed to add FluentResource to FluentBundle."); - bundle +pub fn get_fixture(path: &str) -> Result { + let mut f = File::open(path)?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + Ok(serde_yaml::from_str(&s).expect("Parsing YAML failed.")) +} + +pub fn get_defaults(path: &str) -> Result { + let mut f = File::open(path)?; + let mut s = String::new(); + f.read_to_string(&mut s)?; + Ok(serde_yaml::from_str(&s).expect("Parsing YAML failed.")) +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestBundle { + pub name: Option, + pub locales: Option>, + pub resources: Option>, + #[serde(rename = "useIsolating")] + pub use_isolating: Option, + pub functions: Option>, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub errors: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestResource { + pub name: Option, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub errors: Vec, + pub source: String, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestSetup { + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub bundles: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub resources: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestError { + #[serde(rename = "type")] + pub error_type: String, + pub desc: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestAssert { + pub bundle: Option, + pub id: String, + pub attribute: Option, + pub value: Option, + pub args: Option>, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub errors: Vec, + pub missing: Option, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct Test { + pub name: String, + pub skip: Option, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub bundles: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub resources: Vec, + + pub asserts: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestSuite { + pub name: String, + pub skip: Option, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub bundles: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub resources: Vec, + + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub tests: Vec, + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub suites: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestFixture { + pub suites: Vec, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct BundleDefaults { + #[serde(rename = "useIsolating")] + pub use_isolating: Option, + pub locales: Option>, +} + +#[derive(Debug, PartialEq, Serialize, Deserialize, Clone)] +#[serde(deny_unknown_fields)] +pub struct TestDefaults { + pub bundle: BundleDefaults, } diff --git a/fluent-bundle/tests/primitives_test.rs b/fluent-bundle/tests/primitives_test.rs deleted file mode 100644 index 6eb469b4..00000000 --- a/fluent-bundle/tests/primitives_test.rs +++ /dev/null @@ -1,114 +0,0 @@ -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"); -} - -#[test] -fn primitives_placeable() { - let res = assert_get_resource_from_str_no_errors( - r#" -key = { { "Literal" } } - "#, - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("key", None), "Literal"); -} diff --git a/fluent-bundle/tests/resolve_attribute_expression.rs b/fluent-bundle/tests/resolve_attribute_expression.rs deleted file mode 100644 index 6699e67b..00000000 --- a/fluent-bundle/tests/resolve_attribute_expression.rs +++ /dev/null @@ -1,96 +0,0 @@ -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 attribute_expression() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo - .attr = Foo Attr -bar = - .attr = Bar Attr - -use-foo = { foo } -use-foo-attr = { foo.attr } -use-bar = { bar } -use-bar-attr = { bar.attr } - -missing-attr = { foo.missing } -missing-missing = { missing.missing } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("use-foo", None), "Foo"); - - assert_format_no_errors(bundle.format("use-foo-attr", None), "Foo Attr"); - - 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( - 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("foo.label", None), "Foo Bar Baz"); - } -} diff --git a/fluent-bundle/tests/resolve_external_argument.rs b/fluent-bundle/tests/resolve_external_argument.rs deleted file mode 100644 index 79a37142..00000000 --- a/fluent-bundle/tests/resolve_external_argument.rs +++ /dev/null @@ -1,63 +0,0 @@ -mod helpers; - -use std::collections::HashMap; - -use self::helpers::{ - assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, -}; -use fluent_bundle::types::FluentValue; - -#[test] -fn external_argument_string() { - let res = assert_get_resource_from_str_no_errors("hello-world = Hello { $name }"); - let bundle = assert_get_bundle_no_errors(&res, None); - - let mut args = HashMap::new(); - args.insert("name", FluentValue::from("John")); - - assert_format_no_errors(bundle.format("hello-world", Some(&args)), "Hello John"); -} - -#[test] -fn external_argument_number() { - let res = assert_get_resource_from_str_no_errors( - " -unread-emails = You have { $emailsCount } unread emails. -unread-emails-dec = You have { $emailsCountDec } unread emails. - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - let mut args = HashMap::new(); - args.insert("emailsCount", FluentValue::from(5)); - args.insert("emailsCountDec", FluentValue::into_number("5.0")); - - assert_format_no_errors( - bundle.format("unread-emails", Some(&args)), - "You have 5 unread emails.", - ); - - assert_format_no_errors( - bundle.format("unread-emails-dec", Some(&args)), - "You have 5.0 unread emails.", - ); -} - -#[test] -fn reference_message_with_external_argument() { - let res = assert_get_resource_from_str_no_errors( - " -greetings = Hello, { $userName } -click-on = Click on the `{ greetings }` label. - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - let mut args = HashMap::new(); - args.insert("userName", FluentValue::from("Mary")); - - assert_format_no_errors( - bundle.format("click-on", Some(&args)), - "Click on the `Hello, Mary` label.", - ); -} diff --git a/fluent-bundle/tests/resolve_message_reference.rs b/fluent-bundle/tests/resolve_message_reference.rs deleted file mode 100644 index c5a1beff..00000000 --- a/fluent-bundle/tests/resolve_message_reference.rs +++ /dev/null @@ -1,114 +0,0 @@ -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 message_reference() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo -bar = { foo } Bar - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("bar", None), "Foo Bar"); -} - -#[test] -fn term_reference() { - let res = assert_get_resource_from_str_no_errors( - " --foo = Foo -bar = { -foo } Bar - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("bar", None), "Foo Bar"); -} - -#[test] -fn message_reference_nested() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo -bar = { foo } Bar -baz = { bar } Baz - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("baz", None), "Foo Bar Baz"); -} - -#[test] -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( - bundle.format("bar", None), - "foo Bar", - vec![FluentError::ResolverError(ResolverError::Reference( - "Unknown message: foo".into(), - ))], - ); -} - -#[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_format( - bundle.format("foo", None), - "Foo foo Bar", - vec![FluentError::ResolverError(ResolverError::Cyclic)], - ); - } - - { - let res = assert_get_resource_from_str_no_errors( - " -foo = { bar } -bar = { foo } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, 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)], - ); - } -} - -#[test] -fn message_reference_multiple() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo -bar = { foo } Bar { foo } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("bar", None), "Foo Bar Foo"); -} diff --git a/fluent-bundle/tests/resolve_plural_rule.rs b/fluent-bundle/tests/resolve_plural_rule.rs deleted file mode 100644 index 47a850b1..00000000 --- a/fluent-bundle/tests/resolve_plural_rule.rs +++ /dev/null @@ -1,78 +0,0 @@ -mod helpers; - -use std::collections::HashMap; - -use self::helpers::{ - assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, -}; -use fluent_bundle::types::FluentValue; - -#[test] -fn external_argument_number() { - let res = assert_get_resource_from_str_no_errors( - " -unread-emails = - { $emailsCount -> - [one] You have { $emailsCount } unread email. - *[other] You have { $emailsCount } unread emails. - } - -unread-emails-dec = - { $emailsCountDec -> - [one] You have { $emailsCountDec } unread email. - *[other] You have { $emailsCountDec } unread emails. - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, Some("en")); - - let mut args = HashMap::new(); - args.insert("emailsCount", FluentValue::from(1)); - args.insert("emailsCountDec", FluentValue::into_number("1.0")); - - assert_format_no_errors( - bundle.format("unread-emails", Some(&args)), - "You have 1 unread email.", - ); - - assert_format_no_errors( - bundle.format("unread-emails-dec", Some(&args)), - "You have 1.0 unread emails.", - ); -} - -#[test] -fn exact_match() { - let res = assert_get_resource_from_str_no_errors( - " -unread-emails = - { $emailsCount -> - [1] You have one unread email. - [one] You have { $emailsCount } unread email. - *[other] You have { $emailsCount } unread emails. - } - -unread-emails-dec = - { $emailsCountDec -> - [1.0] You have one unread email. - [one] You have { $emailsCountDec } unread email. - *[other] You have { $emailsCountDec } unread emails. - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, Some("en")); - - let mut args = HashMap::new(); - args.insert("emailsCount", FluentValue::from(1)); - args.insert("emailsCountDec", FluentValue::into_number("1.0")); - - assert_format_no_errors( - bundle.format("unread-emails", Some(&args)), - "You have one unread email.", - ); - - assert_format_no_errors( - bundle.format("unread-emails-dec", Some(&args)), - "You have one unread email.", - ); -} diff --git a/fluent-bundle/tests/resolve_select_expression.rs b/fluent-bundle/tests/resolve_select_expression.rs deleted file mode 100644 index 0b489b5b..00000000 --- a/fluent-bundle/tests/resolve_select_expression.rs +++ /dev/null @@ -1,266 +0,0 @@ -mod helpers; -use fluent_bundle::errors::FluentError; -use fluent_bundle::resolve::ResolverError; - -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, -}; -use fluent_bundle::types::FluentValue; - -#[test] -fn select_expression_string_selector() { - let res = assert_get_resource_from_str_no_errors( - " -foo = - { \"genitive\" -> - *[nominative] Foo - [genitive] Foo's - } - -bar = - { \"missing\" -> - *[nominative] Bar - [genitive] Bar's - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("foo", None), "Foo's"); - - assert_format_no_errors(bundle.format("bar", None), "Bar"); -} - -#[test] -fn select_expression_number_selector() { - let res = assert_get_resource_from_str_no_errors( - " -foo = - { 3 -> - *[1] Foo 1 - [3] Foo 3 - } - -bar = - { 3 -> - *[1] Bar 1 - [2] Bar 2 - } - -baz = - { 3.14 -> - *[1] Baz 1 - [3] Baz 3 - [3.14] Baz Pi - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("foo", None), "Foo 3"); - - assert_format_no_errors(bundle.format("bar", None), "Bar 1"); - - assert_format_no_errors(bundle.format("baz", None), "Baz Pi"); -} - -#[test] -fn select_expression_plurals() { - let res = assert_get_resource_from_str_no_errors( - " -foo = - { 3 -> - [one] Foo One - [3] Foo 3 - *[other] Foo Other - } - -bar = - { 1 -> - [one] Bar One - [2] Bar 2 - *[other] Bar Other - } - -baz = - { \"one\" -> - [1] Bar One - [3] Bar 3 - *[other] Bar Other - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, Some("en")); - - assert_format_no_errors(bundle.format("foo", None), "Foo 3"); - - assert_format_no_errors(bundle.format("bar", None), "Bar One"); - - assert_format_no_errors(bundle.format("baz", None), "Bar Other"); -} - -#[test] -fn select_expression_external_argument_selector() { - let res = assert_get_resource_from_str_no_errors( - " -foo-hit = - { $str -> - *[foo] Foo - [qux] Qux - } - -foo-miss = - { $str -> - *[foo] Foo - [bar] Bar - } - -foo-unknown = - { $unknown -> - *[foo] Foo - [bar] Bar - } - -bar-hit = - { $int -> - *[1] Bar 1 - [3] Bar 3 - } - -bar-miss = - { $int -> - *[1] Bar 1 - [2] Bar 2 - } - -bar-unknown = - { $unknown -> - *[1] Bar 1 - [2] Bar 2 - } - -baz-hit = - { $float -> - *[1] Baz 1 - [2.72] Baz E - } - -baz-miss = - { $float -> - *[1] Baz 1 - [2] Baz 2 - } - -baz-unknown = - { $unknown -> - *[1] Baz 1 - [2] Baz 2 - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - let mut args = HashMap::new(); - args.insert("str", FluentValue::from("qux")); - args.insert("int", FluentValue::from(3)); - args.insert("float", FluentValue::from(2.72)); - - assert_format_no_errors(bundle.format("foo-hit", Some(&args)), "Qux"); - - assert_format_no_errors(bundle.format("foo-miss", 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( - 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( - bundle.format("baz-unknown", Some(&args)), - "Baz 1", - vec![FluentError::ResolverError(ResolverError::Reference( - "Unknown variable: $unknown".into(), - ))], - ); -} - -#[test] -fn select_expression_message_selector() { - let res = assert_get_resource_from_str_no_errors( - " --bar = Bar - .attr = attr_val - -use-bar = - { -bar.attr -> - [attr_val] Bar - *[other] Other - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("use-bar", None), "Bar"); -} - -#[test] -fn select_expression_attribute_selector() { - let res = assert_get_resource_from_str_no_errors( - " --foo = Foo - .attr = FooAttr - -use-foo = - { -foo.attr -> - [FooAttr] Foo - *[other] Other - } - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - 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_value.rs b/fluent-bundle/tests/resolve_value.rs deleted file mode 100644 index e0b1c04d..00000000 --- a/fluent-bundle/tests/resolve_value.rs +++ /dev/null @@ -1,30 +0,0 @@ -mod helpers; - -use self::helpers::{ - assert_format_no_errors, assert_get_bundle_no_errors, assert_get_resource_from_str_no_errors, -}; - -#[test] -fn format_message() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("foo", None), "Foo"); -} - -#[test] -fn format_attribute() { - let res = assert_get_resource_from_str_no_errors( - " -foo = Foo - .attr = Foo Attr - ", - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("foo.attr", None), "Foo Attr"); -} diff --git a/fluent-bundle/tests/resolve_variant_expression.rs b/fluent-bundle/tests/resolve_variant_expression.rs deleted file mode 100644 index b5425a6e..00000000 --- a/fluent-bundle/tests/resolve_variant_expression.rs +++ /dev/null @@ -1,56 +0,0 @@ -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 variant_expression() { - let res = assert_get_resource_from_str_no_errors( - r#" --foo = Foo --bar = - { $gender -> - *[nominative] Bar - [genitive] Bar's - } -baz = { -bar } - -use-foo = { -foo } -use-foo-missing = { -foo(gender: "missing") } - -use-bar = { -bar } -use-bar-nominative = { -bar(gender: "nominative") } -use-bar-genitive = { -bar(gender: "genitive") } -use-bar-missing = { -bar(gender: "missing") } - -missing-missing = { -missing(gender: "missing") } - "#, - ); - let bundle = assert_get_bundle_no_errors(&res, None); - - assert_format_no_errors(bundle.format("baz", None), "Bar"); - - assert_format_no_errors(bundle.format("use-foo", None), "Foo"); - - assert_format_no_errors(bundle.format("use-foo-missing", None), "Foo"); - - assert_format_no_errors(bundle.format("use-bar", None), "Bar"); - - assert_format_no_errors(bundle.format("use-bar-nominative", None), "Bar"); - - assert_format_no_errors(bundle.format("use-bar-genitive", None), "Bar's"); - - assert_format_no_errors(bundle.format("use-bar-missing", None), "Bar"); - - assert_format( - bundle.format("missing-missing", None), - "-missing", - vec![FluentError::ResolverError(ResolverError::Reference( - "Unknown term: -missing".into(), - ))], - ); -} diff --git a/fluent-bundle/tests/resolver_fixtures.rs b/fluent-bundle/tests/resolver_fixtures.rs new file mode 100644 index 00000000..3ab27eb5 --- /dev/null +++ b/fluent-bundle/tests/resolver_fixtures.rs @@ -0,0 +1,334 @@ +mod helpers; + +use std::collections::HashMap; +use std::fs; +use std::iter; +use std::path::Path; +use std::str::FromStr; + +use fluent_bundle::errors::FluentError; +use fluent_bundle::resolve::ResolverError; +use fluent_bundle::{FluentBundle as FluentBundleGeneric, FluentResource, FluentValue}; +use rand::distributions::Alphanumeric; +use rand::{thread_rng, Rng}; + +type FluentBundle = FluentBundleGeneric; + +use helpers::*; + +#[derive(Clone)] +struct ScopeLevel { + name: String, + resources: Vec, + bundles: Vec, +} + +#[derive(Clone)] +struct Scope(Vec); + +impl Scope { + fn get_path(&self) -> String { + self.0 + .iter() + .map(|lvl| lvl.name.as_str()) + .collect::>() + .join(" > ") + } + + fn get_bundles(&self, defaults: &Option) -> HashMap { + let mut bundles = HashMap::new(); + + let mut available_resources = vec![]; + + for lvl in self.0.iter() { + for r in lvl.resources.iter() { + available_resources.push(r); + } + + for b in lvl.bundles.iter() { + let name = b + .name + .as_ref() + .cloned() + .unwrap_or_else(|| generate_random_hash()); + let bundle = create_bundle(Some(b), &defaults, &available_resources); + bundles.insert(name, bundle); + } + } + if bundles.is_empty() { + let bundle = create_bundle(None, defaults, &available_resources); + let name = generate_random_hash(); + bundles.insert(name.clone(), bundle); + } + bundles + } +} + +fn generate_random_hash() -> String { + let mut rng = thread_rng(); + let chars: String = iter::repeat(()) + .map(|()| rng.sample(Alphanumeric)) + .take(7) + .collect(); + chars +} + +fn test_fixture(fixture: &TestFixture, defaults: &Option) { + for suite in &fixture.suites { + test_suite(&suite, defaults, Scope(vec![])); + } +} + +fn create_bundle( + b: Option<&TestBundle>, + defaults: &Option, + resources: &Vec<&TestResource>, +) -> FluentBundle { + let mut errors = vec![]; + + let locales = b + .and_then(|b| b.locales.as_ref()) + .or_else(|| { + defaults + .as_ref() + .and_then(|defaults| defaults.bundle.locales.as_ref()) + }) + .expect("Failed to calculate locales."); + let mut bundle = FluentBundle::new(&locales); + let use_isolating = b.and_then(|b| b.use_isolating).or_else(|| { + defaults + .as_ref() + .and_then(|defaults| defaults.bundle.use_isolating) + }); + if let Some(use_isolating) = use_isolating { + bundle.set_use_isolating(use_isolating); + } + if let Some(&TestBundle { + functions: Some(ref fns), + .. + }) = b + { + for f in fns { + let result = match f.as_str() { + "CONCAT" => bundle.add_function(f.as_str(), |args, _name_args| { + args.iter() + .fold(String::new(), |acc, x| acc + &x.to_string()) + .into() + }), + "SUM" => bundle.add_function(f.as_str(), |args, _name_args| { + args.iter() + .fold(0.0, |acc, x| { + if let FluentValue::Number(v) = x { + acc + f64::from_str(v).expect("Failed to format a number") + } else { + panic!("Type cannot be used in SUM"); + } + }) + .into() + }), + "IDENTITY" => bundle.add_function(f.as_str(), |args, _name_args| { + args.get(0).cloned().unwrap_or(FluentValue::None) + }), + "NUMBER" => bundle.add_function(f.as_str(), |args, _name_args| { + args.get(0).expect("Argument must be passed").clone() + }), + _ => unimplemented!("No such function."), + }; + if let Err(err) = result { + errors.push(err); + } + } + } + let res_subset = b.and_then(|b| b.resources.as_ref()); + + for res in resources.iter() { + if let Some(res_subset) = res_subset { + if let Some(ref name) = res.name { + if !res_subset.contains(name) { + continue; + } + } + } + let res = get_resource(res); + if let Err(mut err) = bundle.add_resource(res) { + errors.append(&mut err); + } + } + test_errors(&errors, b.map(|b| b.errors.as_ref())); + bundle +} + +fn get_resource(resource: &TestResource) -> FluentResource { + let res = FluentResource::try_new(resource.source.clone()); + + if resource.errors.is_empty() { + res.expect("Failed to parse an FTL resource.") + } else { + let (res, errors) = match res { + Ok(r) => (r, vec![]), + Err((res, err)) => { + let err = err.into_iter().map(|err| err.into()).collect(); + (res, err) + } + }; + test_errors(&errors, Some(&resource.errors)); + res + } +} + +fn test_suite(suite: &TestSuite, defaults: &Option, mut scope: Scope) { + if suite.skip == Some(true) { + return; + } + + scope.0.push(ScopeLevel { + name: suite.name.clone(), + bundles: suite.bundles.clone(), + resources: suite.resources.clone(), + }); + + for test in &suite.tests { + test_test(test, defaults, scope.clone()); + } + + for sub_suite in &suite.suites { + test_suite(sub_suite, defaults, scope.clone()); + } +} + +fn test_test(test: &Test, defaults: &Option, mut scope: Scope) { + if test.skip == Some(true) { + return; + } + + scope.0.push(ScopeLevel { + name: test.name.clone(), + bundles: test.bundles.clone(), + resources: test.resources.clone(), + }); + + for assert in &test.asserts { + let bundles = scope.get_bundles(defaults); + let bundle = if let Some(ref bundle_name) = assert.bundle { + bundles + .get(bundle_name) + .expect("Failed to retrieve bundle.") + } else if bundles.len() == 1 { + let name = bundles.keys().into_iter().last().unwrap(); + bundles.get(name).expect("Failed to retrieve bundle.") + } else { + panic!(); + }; + let mut errors = vec![]; + + if let Some(expected_missing) = assert.missing { + let missing = if let Some(ref attr) = assert.attribute { + if let Some(msg) = bundle.get_message(&assert.id) { + msg.attributes.contains_key(attr.as_str()) + } else { + false + } + } else { + !bundle.has_message(&assert.id) + }; + assert_eq!( + missing, + expected_missing, + "Expected pattern to be `missing: {}` for {} in {}", + expected_missing, + assert.id, + scope.get_path() + ); + } else { + if let Some(ref expected_value) = assert.value { + let msg = bundle.get_message(&assert.id).expect(&format!( + "Failed to retrieve message `{}` in {}.", + &assert.id, + scope.get_path() + )); + let val = if let Some(ref attr) = assert.attribute { + msg.attributes.get(attr.as_str()).expect(&format!( + "Failed to retrieve an attribute of a message {}.{}.", + assert.id, attr + )) + } else { + msg.value.expect(&format!( + "Failed to retrieve a value of a message {}.", + assert.id + )) + }; + + let args: Option> = assert.args.as_ref().map(|args| { + args.iter() + .map(|(k, v)| { + let val = match f64::from_str(v) { + Ok(_) => FluentValue::Number(v.into()), + Err(_) => FluentValue::String(v.into()), + }; + (k.to_string(), val) + }) + .collect() + }); + let value = bundle.format_pattern(&val, args.as_ref(), &mut errors); + assert_eq!( + &value, + expected_value, + "Values don't match in {}", + scope.get_path() + ); + test_errors(&errors, Some(&assert.errors)); + } else { + panic!("Value field expected."); + } + } + } +} + +fn test_errors(errors: &[FluentError], reference: Option<&[TestError]>) { + let reference = reference.unwrap_or(&[]); + assert_eq!(errors.len(), reference.len()); + for (error, reference) in errors.into_iter().zip(reference) { + match error { + FluentError::ResolverError(err) => match err { + ResolverError::Reference(desc) => { + assert_eq!(reference.desc.as_ref(), Some(desc)); + assert_eq!(reference.error_type, "Reference"); + } + ResolverError::Cyclic => { + assert_eq!(reference.error_type, "Cyclic"); + } + _ => unimplemented!(), + }, + FluentError::ParserError(_) => { + assert_eq!(reference.error_type, "Parser"); + } + FluentError::Overriding { .. } => { + assert_eq!(reference.error_type, "Overriding"); + } + } + } +} + +#[test] +fn resolve_fixtures() { + let dir = "./tests/fixtures/"; + let mut defaults_path = String::from(dir); + defaults_path.push_str("defaults.yaml"); + let defaults = if Path::new(&defaults_path).exists() { + Some(get_defaults(&defaults_path).expect("Failed to read defaults.")) + } else { + None + }; + for entry in fs::read_dir(dir).expect("Failed to read glob pattern.") { + let entry = entry.expect("Entry doesn't exist."); + let path = entry.path(); + let path_str = path.to_str().expect("Failed to convert path to string."); + if path_str.contains("defaults.yaml") { + continue; + } + println!("PATH: {:#?}", path_str); + let fixture = get_fixture(path_str).expect("Loading fixture failed."); + + test_fixture(&fixture, &defaults); + } +} diff --git a/fluent-bundle/tests/resource_test.rs b/fluent-bundle/tests/resource_test.rs deleted file mode 100644 index 9446784a..00000000 --- a/fluent-bundle/tests/resource_test.rs +++ /dev/null @@ -1,10 +0,0 @@ -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 deleted file mode 100644 index 719dfa67..00000000 --- a/fluent-bundle/tests/select_expression_test.rs +++ /dev/null @@ -1,113 +0,0 @@ -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/values_format_test.rs b/fluent-bundle/tests/values_format_test.rs deleted file mode 100644 index 32b63197..00000000 --- a/fluent-bundle/tests/values_format_test.rs +++ /dev/null @@ -1,55 +0,0 @@ -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 deleted file mode 100644 index b3eb762a..00000000 --- a/fluent-bundle/tests/values_ref_test.rs +++ /dev/null @@ -1,68 +0,0 @@ -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"); -} diff --git a/fluent-fallback/examples/simple.rs b/fluent-fallback/examples/simple.rs index 19f78c44..9aaace7f 100644 --- a/fluent-fallback/examples/simple.rs +++ b/fluent-fallback/examples/simple.rs @@ -152,16 +152,16 @@ fn main() { // 7.2. Construct a map of arguments // to format the message. let mut args = HashMap::new(); - args.insert("input", FluentValue::from(i)); - args.insert("value", FluentValue::from(collatz(i))); + args.insert("input".to_string(), FluentValue::from(i)); + args.insert("value".to_string(), FluentValue::from(collatz(i))); // 7.3. Format the message. let value = loc.format_value("response-msg", Some(&args)); println!("{}", value); } Err(err) => { let mut args = HashMap::new(); - args.insert("input", FluentValue::from(input.as_str())); - args.insert("reason", FluentValue::from(err.to_string())); + args.insert("input".to_string(), FluentValue::from(input.as_str())); + args.insert("reason".to_string(), FluentValue::from(err.to_string())); let value = loc.format_value("input-parse-error-msg", Some(&args)); println!("{}", value); } diff --git a/fluent-fallback/src/lib.rs b/fluent-fallback/src/lib.rs index 7bc666e7..c644f1b5 100644 --- a/fluent-fallback/src/lib.rs +++ b/fluent-fallback/src/lib.rs @@ -45,14 +45,16 @@ impl<'loc, R> Localization<'loc, R> { self.bundles = Reiterate::new((self.generate_bundles)(&self.resource_ids)); } - pub fn format_value(&mut self, id: &str, args: Option<&HashMap<&str, FluentValue>>) -> String + pub fn format_value(&mut self, id: &str, args: Option<&HashMap>) -> String where R: Borrow, { for bundle in &self.bundles { - if bundle.has_message(id) { - let res = bundle.format(id, args).unwrap(); - return res.0.to_string(); + if let Some(msg) = bundle.get_message(id) { + if let Some(pattern) = msg.value { + let mut errors = vec![]; + return bundle.format_pattern(pattern, args, &mut errors).to_string(); + } } } id.into() diff --git a/fluent-resmgr/examples/simple.rs b/fluent-resmgr/examples/simple.rs index 57feee96..4787174a 100644 --- a/fluent-resmgr/examples/simple.rs +++ b/fluent-resmgr/examples/simple.rs @@ -104,27 +104,37 @@ fn main() { // 6.2. Construct a map of arguments // to format the message. let mut args = HashMap::new(); - args.insert("input", FluentValue::from(i)); - args.insert("value", FluentValue::from(collatz(i))); + args.insert("input".to_string(), FluentValue::from(i)); + args.insert("value".to_string(), FluentValue::from(collatz(i))); // 6.3. Format the message. - println!("{}", bundle.format("response-msg", Some(&args)).unwrap().0); + let mut errors = vec![]; + let msg = bundle.get_message("response-msg").expect("Message exists"); + let pattern = msg.value.expect("Message has a value"); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + println!("{}", value); } Err(err) => { let mut args = HashMap::new(); - args.insert("input", FluentValue::from(input.to_string())); - args.insert("reason", FluentValue::from(err.to_string())); - println!( - "{}", - bundle - .format("input-parse-error-msg", Some(&args)) - .unwrap() - .0 - ); + args.insert("input".to_string(), FluentValue::from(input.to_string())); + args.insert("reason".to_string(), FluentValue::from(err.to_string())); + let mut errors = vec![]; + let msg = bundle + .get_message("input-parse-error-msg") + .expect("Message exists"); + let pattern = msg.value.expect("Message has a value"); + let value = bundle.format_pattern(&pattern, Some(&args), &mut errors); + println!("{}", value); } } } None => { - println!("{}", bundle.format("missing-arg-error", None).unwrap().0); + let mut errors = vec![]; + let msg = bundle + .get_message("missing-arg-error") + .expect("Message exists"); + let pattern = msg.value.expect("Message has a value"); + let value = bundle.format_pattern(&pattern, None, &mut errors); + println!("{}", value); } } } diff --git a/fluent-resmgr/tests/localization_test.rs b/fluent-resmgr/tests/localization_test.rs index 6888654e..84956d88 100644 --- a/fluent-resmgr/tests/localization_test.rs +++ b/fluent-resmgr/tests/localization_test.rs @@ -27,6 +27,9 @@ fn resmgr_get_bundle() { let bundle = res_mgr.get_bundle(vec!["en-US".into()], vec!["test.ftl".into()]); - let (value, _) = bundle.format("hello-world", None).unwrap(); + let mut errors = vec![]; + let msg = bundle.get_message("hello-world").expect("Message exists"); + let pattern = msg.value.expect("Message has a value"); + let value = bundle.format_pattern(&pattern, None, &mut errors); assert_eq!(value, "Hello World"); } diff --git a/fluent-syntax/Cargo.toml b/fluent-syntax/Cargo.toml index 73a6ff55..213fc110 100644 --- a/fluent-syntax/Cargo.toml +++ b/fluent-syntax/Cargo.toml @@ -17,8 +17,7 @@ keywords = ["localization", "l10n", "i18n", "intl", "internationalization"] categories = ["localization", "internationalization"] [dev-dependencies] -serde = "^1.0" -serde_derive = "^1.0" +serde = { version = "^1.0", features = ["derive"] } serde_json = "^1.0" glob = "^0.3" assert-json-diff = "^1.0" diff --git a/fluent-syntax/tests/ast/mod.rs b/fluent-syntax/tests/ast/mod.rs index 9b6304be..ac501abf 100644 --- a/fluent-syntax/tests/ast/mod.rs +++ b/fluent-syntax/tests/ast/mod.rs @@ -2,7 +2,6 @@ use fluent_syntax::ast; use serde::ser::SerializeMap; use serde::ser::SerializeSeq; use serde::{Serialize, Serializer}; -use serde_derive::Serialize; use std::error::Error; pub fn serialize<'s>(res: &'s ast::Resource) -> Result> {