Repository: projectfluent/fluent-rs (rust)

Issue:
[Bug] Incorrect behaviour with following function invocation
Fluent Syntax book shows the following snippet.
```fluent
liked-count = { $num ->
        [0]     No likes yet.
        [one]   One person liked your message
       *[other] { $num } people liked your message
    }

liked-count2 = { NUMBER($num) ->
        [0]     No likes yet.
        [one]   One person liked your message
       *[other] { $num } people liked your message
    }
```
The quote is, "the following examples are equivalent and will both work.".
In reality, things are somewhat different.
Firstly, the first snippet depends on the type of argument passed as $num. If $num is a string, like "1", the result will be "1 people liked your message". If $num is number 1, the result is "One person liked your message". This is mostly ok.
But with the second snippet here is a real problem.
Considering we want to cast our number "1" to FluentValue::Number, we can call it to make the conversion. And it works on the playground.
https://projectfluent.org/play/?id=c9aca30ef4ba09384df54def5b5ead01

In fluent-bundle, it doesn't work. Even worse, it doesn't work even if we call NUMBER if $num is already a number (for example to add more options).
I created a gist that shows those 4 cases.
https://gist.github.com/juliancoffee/ab6f8bf691415f20942debfdfe50cc4b

Results are following:
1 people liked your message ("1")
One person liked your message (1)
1 people liked your message (NUMBER("1"))
1 people liked your message (NUMBER(1))

The problem I found is that we don't have functions properly in `impl ResolveValue` for `ast::InlineExpression`, and the fix is quite simple. I would gladly make a PR if that's appreciated.

Pull request:
Resolve function instead in impl ResolveValue
Before this change, we were using the version from WriteValue, which
basically ignored the type of function and always converted it to string,
which made NUMBER function useless when used on strings or even wrong
when used on numbers.
Fixes #261 

Patch:
diff --git a/fluent-bundle/src/resolver/inline_expression.rs b/fluent-bundle/src/resolver/inline_expression.rs
index b9e89b6..6b4b6c2 100644
--- a/fluent-bundle/src/resolver/inline_expression.rs
+++ b/fluent-bundle/src/resolver/inline_expression.rs
@@ -171,6 +171,19 @@ impl<'p> ResolveValue for ast::InlineExpression<&'p str> {
                     FluentValue::Error
                 }
             }
+            Self::FunctionReference { id, arguments } => {
+                let (resolved_positional_args, resolved_named_args) =
+                    scope.get_arguments(Some(arguments));
+
+                let func = scope.bundle.get_entry_function(id.name);
+
+                if let Some(func) = func {
+                    let result = func(resolved_positional_args.as_slice(), &resolved_named_args);
+                    result
+                } else {
+                    FluentValue::Error
+                }
+            }
             _ => {
                 let mut result = String::new();
                 self.write(&mut result, scope).expect("Failed to write");
diff --git a/fluent/src/lib.rs b/fluent/src/lib.rs
index caa28db..1cbefc0 100644
--- a/fluent/src/lib.rs
+++ b/fluent/src/lib.rs
@@ -86,13 +86,13 @@ pub use fluent_bundle::*;
 ///
 /// let mut args = fluent_args![
 ///     "name" => "John",
-///     "emailCount" => 5
+///     "emailCount" => 5,
 /// ];
 ///
 /// ```
 #[macro_export]
 macro_rules! fluent_args {
-    ( $($key:expr => $value:expr),* ) => {
+    ( $($key:expr => $value:expr),* $(,)? ) => {
         {
             let mut args: $crate::FluentArgs = $crate::FluentArgs::new();
             $(


Test patch:
diff --git a/fluent-bundle/tests/function.rs b/fluent-bundle/tests/function.rs
new file mode 100644
index 0000000..6afab73
--- /dev/null
+++ b/fluent-bundle/tests/function.rs
@@ -0,0 +1,104 @@
+use fluent_bundle::types::FluentNumber;
+use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue};
+
+#[test]
+fn test_function_resolve() {
+    // 1. Create bundle
+    let ftl_string = String::from(
+        "
+liked-count = { $num ->
+        [0]     No likes yet.
+        [one]   One person liked your message
+       *[other] { $num } people liked your message
+    }
+
+liked-count2 = { NUMBER($num) ->
+        [0]     No likes yet.
+        [one]   One person liked your message
+       *[other] { $num } people liked your message
+    }
+    ",
+    );
+
+    let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
+    let mut bundle = FluentBundle::default();
+
+    bundle
+        .add_function("NUMBER", |positional, named| match positional.get(0) {
+            Some(FluentValue::Number(n)) => {
+                let mut num = n.clone();
+                num.options.merge(named);
+
+                FluentValue::Number(num)
+            }
+            Some(FluentValue::String(s)) => {
+                let num: f64 = if let Ok(n) = s.to_owned().parse() {
+                    n
+                } else {
+                    return FluentValue::Error;
+                };
+                let mut num = FluentNumber {
+                    value: num,
+                    options: Default::default(),
+                };
+                num.options.merge(named);
+
+                FluentValue::Number(num)
+            }
+            _ => FluentValue::Error,
+        })
+        .expect("Failed to add a function.");
+
+    bundle
+        .add_resource(res)
+        .expect("Failed to add FTL resources to the bundle.");
+
+    // 2. Example without NUMBER call
+    let msg = bundle
+        .get_message("liked-count")
+        .expect("Message doesn't exist.");
+
+    let mut args = FluentArgs::new();
+    args.set("num", FluentValue::from("1"));
+
+    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!("\u{2068}1\u{2069} people liked your message", &value);
+
+    // 3. Example with passing number, but without NUMBER call
+    let mut args = FluentArgs::new();
+    args.set("num", FluentValue::from(1));
+
+    let msg = bundle
+        .get_message("liked-count")
+        .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!("One person liked your message", &value);
+
+    // 4. Example with NUMBER call
+    let mut args = FluentArgs::new();
+    args.set("num", FluentValue::from("1"));
+
+    let msg = bundle
+        .get_message("liked-count2")
+        .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!("One person liked your message", &value);
+
+    // 5. Example with NUMBER call from number
+    let mut args = FluentArgs::new();
+    args.set("num", FluentValue::from(1));
+
+    let msg = bundle
+        .get_message("liked-count2")
+        .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!("One person liked your message", &value);
+}
diff --git a/fluent/tests/macro.rs b/fluent/tests/macro.rs
index d362bc8..bff6d6a 100644
--- a/fluent/tests/macro.rs
+++ b/fluent/tests/macro.rs
@@ -7,7 +7,7 @@ fn test_fluent_args() {
     let args = fluent_args![
         "name" => "John",
         "emailCount" => 5,
-        "customValue" => FluentValue::from("My Value")
+        "customValue" => FluentValue::from("My Value"),
     ];
     assert_eq!(
         args.get("name"),