From 7c2d19927215f338347f9ee311794f44bdb95e50 Mon Sep 17 00:00:00 2001 From: JasperDeSutter Date: Fri, 16 Dec 2022 14:29:15 +0100 Subject: [PATCH] docs(fluent-bundle): Add typesafe message arguments example --- fluent-bundle/examples/typesafe_messages.rs | 142 ++++++++++++++++++++ fluent-bundle/src/resolver/scope.rs | 5 + 2 files changed, 147 insertions(+) create mode 100644 fluent-bundle/examples/typesafe_messages.rs diff --git a/fluent-bundle/examples/typesafe_messages.rs b/fluent-bundle/examples/typesafe_messages.rs new file mode 100644 index 00000000..b0c7bf36 --- /dev/null +++ b/fluent-bundle/examples/typesafe_messages.rs @@ -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; + +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> { + 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> { + self.hello_world.get_arg(name) + } + } + + pub struct UnreadEmails { + pub email_count: Option, + } + + impl<'a> Message<'a> for UnreadEmails { + fn id(&self) -> &'static str { + "unread-emails" + } + + fn get_arg(&self, name: &str) -> Option> { + 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>; +} + +// 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>> { + 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, + ) -> Cow<'b, str>; +} + +impl CustomizedBundle for FluentBundle { + fn format_message<'b>( + &'b self, + message: &dyn Message, + errors: &mut Vec, + ) -> 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) + } +} diff --git a/fluent-bundle/src/resolver/scope.rs b/fluent-bundle/src/resolver/scope.rs index 4a733d64..1c4f5f5b 100644 --- a/fluent-bundle/src/resolver/scope.rs +++ b/fluent-bundle/src/resolver/scope.rs @@ -146,6 +146,11 @@ impl<'bundle, 'ast, 'args, 'errors, R, M, Args: ArgumentResolver<'args>> } } +/// Determines how to retrieve argument values when resolving fluent messages. +/// This trait can be used to implement an alternative to [`FluentArgs`]. +/// +/// One example usage is for argument type safety that [`FluentArgs`] can't provide due to its +/// flexible nature. See `fluent-bundle/examples/typesafe_messages.rs` for an example of this. pub trait ArgumentResolver<'a>: Copy { fn resolve(self, name: &str) -> Option>>; }