Skip to content

Commit

Permalink
Support more complex user defined functions.
Browse files Browse the repository at this point in the history
  • Loading branch information
PPakalns committed Jun 5, 2024
1 parent 8be41e6 commit 1e78103
Show file tree
Hide file tree
Showing 8 changed files with 225 additions and 9 deletions.
2 changes: 1 addition & 1 deletion fluent-bundle/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down
8 changes: 8 additions & 0 deletions fluent-bundle/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -535,6 +536,13 @@ impl<R, M> FluentBundle<R, M> {
pub fn add_function<F>(&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<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
where
F: FluentFunctionObject + Sync + Send + 'static,
{
match self.entries.entry(id.to_owned()) {
HashEntry::Vacant(entry) => {
Expand Down
41 changes: 38 additions & 3 deletions fluent-bundle/src/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ use crate::args::FluentArgs;
use crate::bundle::FluentBundle;
use crate::resource::FluentResource;
use crate::types::FluentValue;

pub type FluentFunction =
Box<dyn for<'a> Fn(&[FluentValue<'a>], &FluentArgs) -> FluentValue<'a> + Send + Sync>;
use crate::FluentMessage;

type ResourceIdx = usize;
type EntryIdx = usize;
Expand Down Expand Up @@ -71,3 +69,40 @@ impl<R: Borrow<FluentResource>, M> GetEntry for FluentBundle<R, M> {
})
}
}

pub type FluentFunction = Box<dyn FluentFunctionObject + Send + Sync>;

pub trait FluentFunctionScope<'bundle> {
fn get_message(&self, id: &str) -> Option<FluentMessage<'bundle>>;

fn format_message(
&mut self,
pattern: &'bundle ast::Pattern<&'bundle str>,
args: Option<FluentArgs<'bundle>>,
) -> 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<F> 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)
}
}
1 change: 1 addition & 0 deletions fluent-bundle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R> = bundle::FluentBundle<R, intl_memoizer::IntlLangMemoizer>;
pub use entry::{FluentFunctionObject, FluentFunctionScope};
pub use errors::FluentError;
pub use message::{FluentAttribute, FluentMessage};
pub use resource::FluentResource;
Expand Down
12 changes: 10 additions & 2 deletions fluent-bundle/src/resolver/inline_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
Expand Down
29 changes: 28 additions & 1 deletion fluent-bundle/src/resolver/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<FluentResource>,
M: MemoizerKind,
{
fn get_message(&self, id: &str) -> Option<FluentMessage<'bundle>> {
self.bundle.get_message(id)
}

fn format_message(
&mut self,
pattern: &'bundle ast::Pattern<&'bundle str>,
mut args: Option<FluentArgs<'bundle>>,
) -> 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
}
}
138 changes: 138 additions & 0 deletions fluent-bundle/tests/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
);
}
3 changes: 1 addition & 2 deletions fluent-bundle/tests/terms-references-with-arguments.rs
Original file line number Diff line number Diff line change
@@ -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(
"
Expand Down

0 comments on commit 1e78103

Please sign in to comment.