From 1e78103c1388a3ff1d90e4dc6785642aacf86a3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C4=93teris=20Pakalns?= Date: Wed, 5 Jun 2024 14:25:20 +0300 Subject: [PATCH] Support more complex user defined functions. --- fluent-bundle/src/args.rs | 2 +- fluent-bundle/src/bundle.rs | 8 + fluent-bundle/src/entry.rs | 41 +++++- fluent-bundle/src/lib.rs | 1 + .../src/resolver/inline_expression.rs | 12 +- fluent-bundle/src/resolver/scope.rs | 29 +++- fluent-bundle/tests/function.rs | 138 ++++++++++++++++++ .../tests/terms-references-with-arguments.rs | 3 +- 8 files changed, 225 insertions(+), 9 deletions(-) diff --git a/fluent-bundle/src/args.rs b/fluent-bundle/src/args.rs index 58a560e0..f301d378 100644 --- a/fluent-bundle/src/args.rs +++ b/fluent-bundle/src/args.rs @@ -52,7 +52,7 @@ use crate::types::FluentValue; /// "Hello, John. You have 5 messages." /// ); /// ``` -#[derive(Debug, Default)] +#[derive(Debug, Default, Clone)] pub struct FluentArgs<'args>(Vec<(Cow<'args, str>, FluentValue<'args>)>); impl<'args> FluentArgs<'args> { diff --git a/fluent-bundle/src/bundle.rs b/fluent-bundle/src/bundle.rs index 41a00e24..66eb08bd 100644 --- a/fluent-bundle/src/bundle.rs +++ b/fluent-bundle/src/bundle.rs @@ -24,6 +24,7 @@ use crate::message::FluentMessage; use crate::resolver::{ResolveValue, Scope, WriteValue}; use crate::resource::FluentResource; use crate::types::FluentValue; +use crate::FluentFunctionObject; /// A collection of localization messages for a single locale, which are meant /// to be used together in a single view, widget or any other UI abstraction. @@ -535,6 +536,13 @@ impl FluentBundle { pub fn add_function(&mut self, id: &str, func: F) -> Result<(), FluentError> where F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static, + { + self.add_function_with_scope(id, func) + } + + pub fn add_function_with_scope(&mut self, id: &str, func: F) -> Result<(), FluentError> + where + F: FluentFunctionObject + Sync + Send + 'static, { match self.entries.entry(id.to_owned()) { HashEntry::Vacant(entry) => { diff --git a/fluent-bundle/src/entry.rs b/fluent-bundle/src/entry.rs index 37b3eccc..7deab1ca 100644 --- a/fluent-bundle/src/entry.rs +++ b/fluent-bundle/src/entry.rs @@ -9,9 +9,7 @@ use crate::args::FluentArgs; use crate::bundle::FluentBundle; use crate::resource::FluentResource; use crate::types::FluentValue; - -pub type FluentFunction = - Box Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Send + Sync>; +use crate::FluentMessage; type ResourceIdx = usize; type EntryIdx = usize; @@ -71,3 +69,40 @@ impl, M> GetEntry for FluentBundle { }) } } + +pub type FluentFunction = Box; + +pub trait FluentFunctionScope<'bundle> { + fn get_message(&self, id: &str) -> Option>; + + fn format_message( + &mut self, + pattern: &'bundle ast::Pattern<&'bundle str>, + args: Option>, + ) -> FluentValue<'bundle>; +} + +/// Implement custom function that retrieves execution scope information +pub trait FluentFunctionObject { + fn call<'bundle>( + &self, + scope: &mut dyn FluentFunctionScope<'bundle>, + positional: &[FluentValue<'bundle>], + named: &FluentArgs<'bundle>, + ) -> FluentValue<'bundle>; +} + +impl FluentFunctionObject for F +where + F: for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Sync + Send + 'static, +{ + fn call<'bundle>( + &self, + scope: &mut dyn FluentFunctionScope, + positional: &[FluentValue<'bundle>], + named: &FluentArgs, + ) -> FluentValue<'bundle> { + let _ = scope; + self(positional, named) + } +} diff --git a/fluent-bundle/src/lib.rs b/fluent-bundle/src/lib.rs index 93d7ea53..5415315f 100644 --- a/fluent-bundle/src/lib.rs +++ b/fluent-bundle/src/lib.rs @@ -121,6 +121,7 @@ pub use args::FluentArgs; /// The concurrent specialization can be constructed with /// [`FluentBundle::new_concurrent`](crate::concurrent::FluentBundle::new_concurrent). pub type FluentBundle = bundle::FluentBundle; +pub use entry::{FluentFunctionObject, FluentFunctionScope}; pub use errors::FluentError; pub use message::{FluentAttribute, FluentMessage}; pub use resource::FluentResource; diff --git a/fluent-bundle/src/resolver/inline_expression.rs b/fluent-bundle/src/resolver/inline_expression.rs index c793fe82..da0a477d 100644 --- a/fluent-bundle/src/resolver/inline_expression.rs +++ b/fluent-bundle/src/resolver/inline_expression.rs @@ -90,7 +90,11 @@ impl<'bundle> WriteValue<'bundle> for ast::InlineExpression<&'bundle str> { 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); + let result = func.call( + scope, + resolved_positional_args.as_slice(), + &resolved_named_args, + ); if let FluentValue::Error = result { self.write_error(w) } else { @@ -185,7 +189,11 @@ impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> { 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); + let result = func.call( + scope, + resolved_positional_args.as_slice(), + &resolved_named_args, + ); return result; } else { return FluentValue::Error; diff --git a/fluent-bundle/src/resolver/scope.rs b/fluent-bundle/src/resolver/scope.rs index 1ddff1a4..f38cb410 100644 --- a/fluent-bundle/src/resolver/scope.rs +++ b/fluent-bundle/src/resolver/scope.rs @@ -2,7 +2,7 @@ use crate::bundle::FluentBundle; use crate::memoizer::MemoizerKind; use crate::resolver::{ResolveValue, ResolverError, WriteValue}; use crate::types::FluentValue; -use crate::{FluentArgs, FluentError, FluentResource}; +use crate::{FluentArgs, FluentError, FluentFunctionScope, FluentMessage, FluentResource}; use fluent_syntax::ast; use std::borrow::Borrow; use std::fmt; @@ -138,3 +138,30 @@ impl<'bundle, 'ast, 'args, 'errors, R, M> Scope<'bundle, 'ast, 'args, 'errors, R } } } + +impl<'bundle, 'ast, 'args, 'errors, R, M> FluentFunctionScope<'bundle> + for Scope<'bundle, 'ast, 'args, 'errors, R, M> +where + R: Borrow, + M: MemoizerKind, +{ + fn get_message(&self, id: &str) -> Option> { + self.bundle.get_message(id) + } + + fn format_message( + &mut self, + pattern: &'bundle ast::Pattern<&'bundle str>, + mut args: Option>, + ) -> FluentValue<'bundle> { + // Setup scope + std::mem::swap(&mut self.local_args, &mut args); + + let value = pattern.resolve(self); + + // Restore scope + std::mem::swap(&mut self.local_args, &mut args); + + value + } +} diff --git a/fluent-bundle/tests/function.rs b/fluent-bundle/tests/function.rs index 1d403e2f..a98c0dbb 100644 --- a/fluent-bundle/tests/function.rs +++ b/fluent-bundle/tests/function.rs @@ -102,3 +102,141 @@ liked-count2 = { NUMBER($num) -> let value = bundle.format_pattern(pattern, Some(&args), &mut errors); assert_eq!("One person liked your message", &value); } + +#[test] +fn test_extended_function() { + struct ManualMessageReference; + + impl fluent_bundle::FluentFunctionObject for ManualMessageReference { + fn call<'bundle>( + &self, + scope: &mut dyn fluent_bundle::FluentFunctionScope<'bundle>, + positional: &[FluentValue<'bundle>], + named: &FluentArgs<'bundle>, + ) -> FluentValue<'bundle> { + let Some(FluentValue::String(name)) = positional.first().cloned() else { + return FluentValue::Error; + }; + + let Some(msg) = scope.get_message(&name) else { + return FluentValue::Error; + }; + + let pattern = if let Some(FluentValue::String(attribute)) = positional.get(1) { + let Some(pattern) = msg.get_attribute(attribute) else { + return FluentValue::Error; + }; + Some(pattern.value()) + } else { + msg.value() + }; + + let Some(pattern) = pattern else { + return FluentValue::Error; + }; + + scope.format_message(pattern, Some(named.clone())) + } + } + + // Create bundle + let ftl_string = String::from( + r#" +hero-1 = Aurora + .gender = feminine + +hero-2 = Rick + .gender = masculine + +creature-horse = { $count -> + *[one] a horse + [other] { $count } horses +} + +creature-rabbit = { $count -> + *[one] a rabbit + [other] { $count } rabbits +} + +annotation = Beautiful! { MSGREF($creature, count: $count) } + +hero-owns-creature = + { MSGREF($hero) } arrived! + { MSGREF($hero, "gender") -> + [feminine] She owns + [masculine] He owns + *[other] They own + } + { MSGREF($creature, count: $count) } + +"#, + ); + + 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.first() { + Some(FluentValue::Number(n)) => { + let mut num = n.clone(); + num.options.merge(named); + + FluentValue::Number(num) + } + _ => FluentValue::Error, + }) + .expect("Failed to add a function."); + + bundle + .add_function_with_scope("MSGREF", ManualMessageReference) + .expect("Failed to add a function"); + + bundle + .add_resource(res) + .expect("Failed to add FTL resources to the bundle."); + + // Examples with passing message reference to a function + let mut args = FluentArgs::new(); + args.set("creature", FluentValue::from("creature-horse")); + args.set("count", FluentValue::from(1)); + + let msg = bundle + .get_message("annotation") + .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!("Beautiful! \u{2068}a horse\u{2069}", &value); + + let mut args = FluentArgs::new(); + args.set("creature", FluentValue::from("creature-rabbit")); + args.set("count", FluentValue::from(5)); + + let msg = bundle + .get_message("annotation") + .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!( + "Beautiful! \u{2068}\u{2068}5\u{2069} rabbits\u{2069}", + &value + ); + + // Example with accessing message attributes + let mut args = FluentArgs::new(); + args.set("hero", FluentValue::from("hero-2")); + args.set("creature", FluentValue::from("creature-rabbit")); + args.set("count", FluentValue::from(3)); + + let msg = bundle + .get_message("hero-owns-creature") + .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!( + "\u{2068}Rick\u{2069} arrived! \n\u{2068}He owns\u{2069}\n\u{2068}\u{2068}3\u{2069} rabbits\u{2069}", + &value + ); +} diff --git a/fluent-bundle/tests/terms-references-with-arguments.rs b/fluent-bundle/tests/terms-references-with-arguments.rs index 185abee0..6411e6f2 100644 --- a/fluent-bundle/tests/terms-references-with-arguments.rs +++ b/fluent-bundle/tests/terms-references-with-arguments.rs @@ -1,8 +1,7 @@ -use fluent_bundle::types::FluentNumber; use fluent_bundle::{FluentArgs, FluentBundle, FluentResource, FluentValue}; #[test] -fn test_function_resolve() { +fn test_term_argument_resolve() { // 1. Create bundle let ftl_string = String::from( "