Skip to content
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

Allow customizing argument resolver #304

Open
wants to merge 3 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
142 changes: 142 additions & 0 deletions fluent-bundle/examples/typesafe_messages.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
// This is an example of an application which adds a custom argument resolver
// to add type safety.
// See the external_arguments example if you are not yet familiar with fluent arguments.
//
// The goal is that we prevent bugs caused by mixing up arguments that belong
// to different messages.
// We can achieve this by defining structs for each message that encode the
// argument types with the corresponding message ID, and then hooking into
// fluent's resolver using a custom fluent_bundle::ArgumentResolver implementation.

use std::borrow::Cow;
alerque marked this conversation as resolved.
Show resolved Hide resolved

use fluent_bundle::{ArgumentResolver, FluentBundle, FluentError, FluentResource, FluentValue};
use unic_langid::langid;

fn main() {
let ftl_string = String::from(
"
hello-world = Hello { $name }
ref = The previous message says { hello-world }
unread-emails =
{ $emailCount ->
[one] You have { $emailCount } unread email
*[other] You have { $emailCount } unread emails
}
",
);
let res = FluentResource::try_new(ftl_string).expect("Could not parse an FTL string.");
let langid_en = langid!("en");
let mut bundle = FluentBundle::new(vec![langid_en]);
bundle
.add_resource(res)
.expect("Failed to add FTL resources to the bundle.");

let hello_world = messages::HelloWorld { name: "John" };
let mut errors = vec![];
let value = bundle.format_message(&hello_world, &mut errors);
println!("{}", value);

let ref_msg = messages::Ref { hello_world };
let mut errors = vec![];
let value = bundle.format_message(&ref_msg, &mut errors);
println!("{}", value);

let unread_emails = messages::UnreadEmails {
email_count: Some(1),
};
let mut errors = vec![];
let value = bundle.format_message(&unread_emails, &mut errors);
println!("{}", value);
}

// these definitions could be generated by a macro or a code generation tool
mod messages {
use super::*;

pub struct HelloWorld<'a> {
pub name: &'a str,
}

impl<'a> Message<'a> for HelloWorld<'a> {
fn id(&self) -> &'static str {
"hello-world"
}

fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
Some(match name {
"name" => self.name.into(),
_ => return None,
})
}
}

pub struct Ref<'a> {
pub hello_world: HelloWorld<'a>,
}

impl<'a> Message<'a> for Ref<'a> {
fn id(&self) -> &'static str {
"ref"
}

fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
self.hello_world.get_arg(name)
}
}

pub struct UnreadEmails {
pub email_count: Option<u32>,
}

impl<'a> Message<'a> for UnreadEmails {
fn id(&self) -> &'static str {
"unread-emails"
}

fn get_arg(&self, name: &str) -> Option<FluentValue<'a>> {
Some(match name {
"emailCount" => self.email_count.into(),
_ => return None,
})
}
}
}

trait Message<'a> {
fn id(&self) -> &'static str;
fn get_arg(&self, name: &str) -> Option<FluentValue<'a>>;
}

// by using &dyn, we prevent monomorphization for each Message struct
// this keeps binary code size in check
impl<'a, 'b> ArgumentResolver<'a> for &'a dyn Message<'b> {
fn resolve(self, name: &str) -> Option<Cow<FluentValue<'a>>> {
let arg = self.get_arg(name)?;
Some(Cow::Owned(arg))
}
}

// allows for method syntax, i.e. bundle.format_message(...)
trait CustomizedBundle {
fn format_message<'b>(
&'b self,
message: &dyn Message,
errors: &mut Vec<FluentError>,
) -> Cow<'b, str>;
}

impl CustomizedBundle for FluentBundle<FluentResource> {
fn format_message<'b>(
&'b self,
message: &dyn Message,
errors: &mut Vec<FluentError>,
) -> Cow<'b, str> {
let msg = self
.get_message(message.id())
.expect("Message doesn't exist.");

let pattern = msg.value().expect("Message has no value.");
self.format_pattern_with_argument_resolver(pattern, message, errors)
}
}
36 changes: 17 additions & 19 deletions fluent-bundle/src/args.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
use std::borrow::Cow;
use std::iter::FromIterator;

use crate::types::FluentValue;
Expand Down Expand Up @@ -53,7 +52,7 @@ use crate::types::FluentValue;
/// );
/// ```
#[derive(Debug, Default)]
pub struct FluentArgs<'args>(Vec<(Cow<'args, str>, FluentValue<'args>)>);
pub struct FluentArgs<'args>(Vec<(&'args str, FluentValue<'args>)>);

impl<'args> FluentArgs<'args> {
/// Creates a new empty argument map.
Expand All @@ -67,45 +66,42 @@ impl<'args> FluentArgs<'args> {
}

/// Gets the [`FluentValue`] at the `key` if it exists.
pub fn get<K>(&self, key: K) -> Option<&FluentValue<'args>>
where
K: Into<Cow<'args, str>>,
{
let key = key.into();
if let Ok(idx) = self.0.binary_search_by_key(&&key, |(k, _)| k) {
pub fn get<'s>(&'s self, key: &str) -> Option<&'s FluentValue<'args>> {
if let Ok(idx) = self.0.binary_search_by_key(&key, |(k, _)| k) {
Some(&self.0[idx].1)
} else {
None
}
}

/// Sets the key value pair.
pub fn set<K, V>(&mut self, key: K, value: V)
pub fn set<V>(&mut self, key: &'args str, value: V)
where
K: Into<Cow<'args, str>>,
V: Into<FluentValue<'args>>,
{
let key = key.into();
self.set_inner(key, value.into());
}

fn set_inner(&mut self, key: &'args str, value: FluentValue<'args>) {
match self.0.binary_search_by_key(&&key, |(k, _)| k) {
Ok(idx) => self.0[idx] = (key, value.into()),
Err(idx) => self.0.insert(idx, (key, value.into())),
Ok(idx) => self.0[idx] = (key, value),
Err(idx) => self.0.insert(idx, (key, value)),
};
}

/// Iterate over a tuple of the key an [`FluentValue`].
pub fn iter(&self) -> impl Iterator<Item = (&str, &FluentValue)> {
self.0.iter().map(|(k, v)| (k.as_ref(), v))
pub fn iter(&self) -> impl Iterator<Item = (&'args str, &FluentValue<'args>)> {
self.0.iter().map(|(k, v)| (*k, v))
}
}

impl<'args, K, V> FromIterator<(K, V)> for FluentArgs<'args>
impl<'args, V> FromIterator<(&'args str, V)> for FluentArgs<'args>
where
K: Into<Cow<'args, str>>,
V: Into<FluentValue<'args>>,
{
fn from_iter<I>(iter: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
I: IntoIterator<Item = (&'args str, V)>,
{
let iter = iter.into_iter();
let mut args = if let Some(size) = iter.size_hint().1 {
Expand All @@ -123,7 +119,7 @@ where
}

impl<'args> IntoIterator for FluentArgs<'args> {
type Item = (Cow<'args, str>, FluentValue<'args>);
type Item = (&'args str, FluentValue<'args>);
type IntoIter = std::vec::IntoIter<Self::Item>;

fn into_iter(self) -> Self::IntoIter {
Expand All @@ -133,6 +129,8 @@ impl<'args> IntoIterator for FluentArgs<'args> {

#[cfg(test)]
mod tests {
use std::borrow::Cow;

use super::*;

#[test]
Expand Down
16 changes: 16 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::ArgumentResolver;

/// 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 @@ -488,6 +489,21 @@ impl<R, M> FluentBundle<R, M> {
args: Option<&FluentArgs>,
errors: &mut Vec<FluentError>,
) -> Cow<'bundle, str>
where
R: Borrow<FluentResource>,
M: MemoizerKind,
{
self.format_pattern_with_argument_resolver(pattern, args, errors)
}

/// Formats a pattern which comes from a `FluentMessage`.
/// Works the same as [`FluentBundle::format_pattern`], but allows passing a custom argument resolver.
pub fn format_pattern_with_argument_resolver<'bundle, 'args>(
&'bundle self,
pattern: &'bundle ast::Pattern<&'bundle str>,
args: impl ArgumentResolver<'args>,
errors: &mut Vec<FluentError>,
) -> Cow<'bundle, str>
where
R: Borrow<FluentResource>,
M: MemoizerKind,
Expand Down
1 change: 1 addition & 0 deletions fluent-bundle/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ pub use args::FluentArgs;
pub type FluentBundle<R> = bundle::FluentBundle<R, intl_memoizer::IntlLangMemoizer>;
pub use errors::FluentError;
pub use message::{FluentAttribute, FluentMessage};
pub use resolver::ArgumentResolver;
pub use resource::FluentResource;
#[doc(inline)]
pub use types::FluentValue;
7 changes: 4 additions & 3 deletions fluent-bundle/src/resolver/expression.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::scope::Scope;
use super::scope::{ArgumentResolver, Scope};
use super::WriteValue;

use std::borrow::Borrow;
Expand All @@ -12,15 +12,16 @@ use crate::resource::FluentResource;
use crate::types::FluentValue;

impl<'bundle> WriteValue<'bundle> for ast::Expression<&'bundle str> {
fn write<'ast, 'args, 'errors, W, R, M>(
fn write<'ast, 'args, 'errors, W, R, M, Args>(
&'ast self,
w: &mut W,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M, Args>,
) -> fmt::Result
where
W: fmt::Write,
R: Borrow<FluentResource>,
M: MemoizerKind,
Args: ArgumentResolver<'args>,
{
match self {
Self::Inline(exp) => exp.write(w, scope),
Expand Down
26 changes: 17 additions & 9 deletions fluent-bundle/src/resolver/inline_expression.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use super::scope::Scope;
use super::scope::{ArgumentResolver, Scope};
use super::{ResolveValue, ResolverError, WriteValue};

use std::borrow::Borrow;
Expand All @@ -13,15 +13,16 @@ use crate::resource::FluentResource;
use crate::types::FluentValue;

impl<'bundle> WriteValue<'bundle> for ast::InlineExpression<&'bundle str> {
fn write<'ast, 'args, 'errors, W, R, M>(
fn write<'ast, 'args, 'errors, W, R, M, Args>(
&'ast self,
w: &mut W,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M, Args>,
) -> fmt::Result
where
W: fmt::Write,
R: Borrow<FluentResource>,
M: MemoizerKind,
Args: ArgumentResolver<'args>,
{
match self {
Self::StringLiteral { value } => unescape_unicode(w, value),
Expand Down Expand Up @@ -100,9 +101,15 @@ impl<'bundle> WriteValue<'bundle> for ast::InlineExpression<&'bundle str> {
}
}
Self::VariableReference { id } => {
let args = scope.local_args.as_ref().or(scope.args);
let resolved_arg;
let opt_arg = if let Some(args) = scope.local_args.as_ref() {
args.get(id.name)
} else {
resolved_arg = scope.args.resolve(id.name);
resolved_arg.as_ref().map(|it| it.as_ref())
};

if let Some(arg) = args.and_then(|args| args.get(id.name)) {
if let Some(arg) = opt_arg {
arg.write(w, scope)
} else {
if scope.local_args.is_none() {
Expand Down Expand Up @@ -148,13 +155,14 @@ impl<'bundle> WriteValue<'bundle> for ast::InlineExpression<&'bundle str> {
}

impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> {
fn resolve<'ast, 'args, 'errors, R, M>(
fn resolve<'ast, 'args, 'errors, R, M, Args>(
&'ast self,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M>,
scope: &mut Scope<'bundle, 'ast, 'args, 'errors, R, M, Args>,
) -> FluentValue<'bundle>
where
R: Borrow<FluentResource>,
M: MemoizerKind,
Args: ArgumentResolver<'args>,
{
match self {
Self::StringLiteral { value } => unescape_unicode_to_string(value).into(),
Expand All @@ -164,8 +172,8 @@ impl<'bundle> ResolveValue<'bundle> for ast::InlineExpression<&'bundle str> {
if let Some(arg) = local_args.get(id.name) {
return arg.clone();
}
} else if let Some(arg) = scope.args.and_then(|args| args.get(id.name)) {
return arg.into_owned();
} else if let Some(arg) = scope.args.resolve(id.name) {
return arg.as_ref().into_owned();
}

if scope.local_args.is_none() {
Expand Down
Loading