Skip to content

Custom functions with scope access #364

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
43 changes: 31 additions & 12 deletions fluent-bundle/src/resolver/inline_expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use super::{ResolveValue, ResolverError, WriteValue};

use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;

use fluent_syntax::ast;
use fluent_syntax::unicode::{unescape_unicode, unescape_unicode_to_string};
Expand Down Expand Up @@ -89,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 @@ -157,8 +162,12 @@ impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> {
M: MemoizerKind,
{
match self {
Self::StringLiteral { value } => unescape_unicode_to_string(value).into(),
Self::NumberLiteral { value } => FluentValue::try_number(value),
Self::StringLiteral { value } => {
return unescape_unicode_to_string(value).into();
}
Self::NumberLiteral { value } => {
return FluentValue::try_number(value);
}
Self::VariableReference { id } => {
if let Some(local_args) = &scope.local_args {
if let Some(arg) = local_args.get(id.name) {
Expand All @@ -171,7 +180,7 @@ impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> {
if scope.local_args.is_none() {
scope.add_error(self.into());
}
FluentValue::Error
return FluentValue::Error;
}
Self::FunctionReference { id, arguments } => {
let (resolved_positional_args, resolved_named_args) =
Expand All @@ -180,17 +189,27 @@ 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);
result
let result = func.call(
scope,
resolved_positional_args.as_slice(),
&resolved_named_args,
);
return result;
} else {
FluentValue::Error
return FluentValue::Error;
}
}
_ => {
let mut result = String::new();
self.write(&mut result, scope).expect("Failed to write");
result.into()
Self::Placeable { expression } => {
if let ast::Expression::Inline(expression) = expression.deref() {
return expression.resolve(scope);
}
}
}
_ => {}
};

// Fallback to text serialization
let mut result = String::new();
self.write(&mut result, scope).expect("Failed to write");
result.into()
}
}
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
);
}
Loading