Repository: projectfluent/fluent-rs (rust)

Issue:
Remove MessageContext.get_message
This is similar to https://github.com/projectfluent/fluent.js/issues/208.

I'd like to change `MessageContext.format()` to accept an identifier of a message and format it directly to a string. It would avoid the intermediate step of having to first `get_message` the message from the context, which is a bit clunky and is an artifact of how the JavaScript bindings retrieve translated attributes.

Pull request description:
MessageContext.format now takes an id
- Make `get_message` private.
- Remove `get_term` and `get_function`.
- `format` must now be called with the string id of the desired message:

        ctx.format("foo", None);

    It also accepts dot-separated paths identifying attributes:

        ctx.format("foo.attrname", None);

Fix #60.

Patch:
diff --git a/fluent/examples/external_arguments.rs b/fluent/examples/external_arguments.rs
index 7e9b763..d95fd6e 100644
--- a/fluent/examples/external_arguments.rs
+++ b/fluent/examples/external_arguments.rs
@@ -1,12 +1,11 @@
 extern crate fluent;
 
-use fluent::context::MessageContext;
 use fluent::types::FluentValue;
+use fluent::MessageContext;
 use std::collections::HashMap;
 
 fn main() {
     let mut ctx = MessageContext::new(&["en"]);
-
     ctx.add_messages(
         "
 hello-world = Hello { $name }
@@ -22,18 +21,12 @@ unread-emails =
     let mut args = HashMap::new();
     args.insert("name", FluentValue::from("John"));
 
-    match ctx
-        .get_message("hello-world")
-        .and_then(|msg| ctx.format(msg, Some(&args)))
-    {
+    match ctx.format("hello-world", Some(&args)) {
         Some(value) => println!("{}", value),
         None => println!("None"),
     }
 
-    match ctx
-        .get_message("ref")
-        .and_then(|msg| ctx.format(msg, Some(&args)))
-    {
+    match ctx.format("ref", Some(&args)) {
         Some(value) => println!("{}", value),
         None => println!("None"),
     }
@@ -41,10 +34,7 @@ unread-emails =
     let mut args = HashMap::new();
     args.insert("emailCount", FluentValue::as_number("1.0").unwrap());
 
-    match ctx
-        .get_message("unread-emails")
-        .and_then(|msg| ctx.format(msg, Some(&args)))
-    {
+    match ctx.format("unread-emails", Some(&args)) {
         Some(value) => println!("{}", value),
         None => println!("None"),
     }
diff --git a/fluent/examples/functions.rs b/fluent/examples/functions.rs
index 005f5b3..b44dcf8 100644
--- a/fluent/examples/functions.rs
+++ b/fluent/examples/functions.rs
@@ -38,21 +38,15 @@ fn main() {
     ctx.add_messages("meaning-of-life = { MEANING_OF_LIFE(42) }");
     ctx.add_messages("all-your-base = { BASE_OWNERSHIP(hello, ownership: \"us\") }");
 
-    let value = ctx
-        .get_message("hello-world")
-        .and_then(|message| ctx.format(message, None));
+    let value = ctx.format("hello-world", None);
     assert_eq!(value, Some("Hey there! I'm a function!".to_string()));
 
-    let value = ctx
-        .get_message("meaning-of-life")
-        .and_then(|message| ctx.format(message, None));
+    let value = ctx.format("meaning-of-life", None);
     assert_eq!(
         value,
         Some("The answer to life, the universe, and everything".to_string())
     );
 
-    let value = ctx
-        .get_message("all-your-base")
-        .and_then(|message| ctx.format(message, None));
+    let value = ctx.format("all-your-base", None);
     assert_eq!(value, Some("All your base belong to us".to_string()));
 }
diff --git a/fluent/examples/hello.rs b/fluent/examples/hello.rs
index f9986a8..736d550 100644
--- a/fluent/examples/hello.rs
+++ b/fluent/examples/hello.rs
@@ -6,9 +6,6 @@ fn main() {
     let mut ctx = MessageContext::new(&["en-US"]);
     ctx.add_messages("hello-world = Hello, world!");
 
-    let value = ctx
-        .get_message("hello-world")
-        .and_then(|message| ctx.format(message, None));
-
+    let value = ctx.format("hello-world", None);
     assert_eq!(value, Some("Hello, world!".to_string()));
 }
diff --git a/fluent/examples/message_reference.rs b/fluent/examples/message_reference.rs
index 861702e..1e84e83 100644
--- a/fluent/examples/message_reference.rs
+++ b/fluent/examples/message_reference.rs
@@ -4,7 +4,6 @@ use fluent::context::MessageContext;
 
 fn main() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo = Foo
@@ -13,18 +12,12 @@ bazbar = { baz } Bar
 ",
     );
 
-    match ctx
-        .get_message("foobar")
-        .and_then(|msg| ctx.format(msg, None))
-    {
+    match ctx.format("foobar", None) {
         Some(value) => println!("{}", value),
         None => println!("None"),
     }
 
-    match ctx
-        .get_message("bazbar")
-        .and_then(|msg| ctx.format(msg, None))
-    {
+    match ctx.format("bazbar", None) {
         Some(value) => println!("{}", value),
         None => println!("None"),
     }
diff --git a/fluent/examples/selector.rs b/fluent/examples/selector.rs
index a94dd38..2aed29a 100644
--- a/fluent/examples/selector.rs
+++ b/fluent/examples/selector.rs
@@ -6,7 +6,6 @@ use std::collections::HashMap;
 
 fn main() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 hello-world = Hello {
@@ -21,10 +20,7 @@ hello-world2 = Hello { $name ->
 ",
     );
 
-    match ctx
-        .get_message("hello-world")
-        .and_then(|msg| ctx.format(msg, None))
-    {
+    match ctx.format("hello-world", None) {
         Some(value) => println!("{}", value),
         None => println!("None"),
     }
@@ -32,10 +28,7 @@ hello-world2 = Hello { $name ->
     let mut args = HashMap::new();
     args.insert("name", FluentValue::from("moon"));
 
-    match ctx
-        .get_message("hello-world2")
-        .and_then(|msg| ctx.format(msg, Some(&args)))
-    {
+    match ctx.format("hello-world2", Some(&args)) {
         Some(value) => println!("{}", value),
         None => println!("None"),
     }
diff --git a/fluent/examples/simple.rs b/fluent/examples/simple.rs
index 0b010ad..e42f07d 100644
--- a/fluent/examples/simple.rs
+++ b/fluent/examples/simple.rs
@@ -138,8 +138,8 @@ fn main() {
                     // 6.3. Format the message.
                     println!(
                         "{}",
-                        ctx.get_message("response-msg")
-                            .and_then(|msg| ctx.format(msg, Some(&args)))
+                        ctx
+                            .format("response-msg", Some(&args))
                             .unwrap()
                     );
                 }
@@ -149,8 +149,8 @@ fn main() {
                     args.insert("reason", FluentValue::from(err.to_string()));
                     println!(
                         "{}",
-                        ctx.get_message("input-parse-error")
-                            .and_then(|msg| ctx.format(msg, Some(&args)))
+                        ctx
+                            .format("input-parse-error-msg", Some(&args))
                             .unwrap()
                     );
                 }
@@ -159,8 +159,8 @@ fn main() {
         None => {
             println!(
                 "{}",
-                ctx.get_message("missing-arg-error")
-                    .and_then(|msg| ctx.format(msg, None))
+                ctx
+                    .format("missing-arg-error", None)
                     .unwrap()
             );
         }
diff --git a/fluent/src/context.rs b/fluent/src/context.rs
index 03ec365..fd761a3 100644
--- a/fluent/src/context.rs
+++ b/fluent/src/context.rs
@@ -7,6 +7,7 @@
 use std::cell::RefCell;
 use std::collections::hash_map::{Entry as HashEntry, HashMap};
 
+use super::entry::{Entry, GetEntry};
 use super::errors::FluentError;
 use super::resolve::{Env, ResolveValue};
 use super::types::FluentValue;
@@ -15,16 +16,6 @@ use fluent_syntax::ast;
 use fluent_syntax::parser::parse;
 use intl_pluralrules::{IntlPluralRules, PluralRuleType};
 
-enum Entry<'ctx> {
-    Message(ast::Message),
-    Term(ast::Term),
-    Function(
-        Box<
-            'ctx + Fn(&[Option<FluentValue>], &HashMap<String, FluentValue>) -> Option<FluentValue>,
-        >,
-    ),
-}
-
 /// `MessageContext` is a collection of localization messages which are meant to be used together
 /// in a single view, widget or any other UI abstraction.
 ///
@@ -54,7 +45,7 @@ enum Entry<'ctx> {
 #[allow(dead_code)]
 pub struct MessageContext<'ctx> {
     pub locales: Vec<String>,
-    map: HashMap<String, Entry<'ctx>>,
+    pub entries: HashMap<String, Entry<'ctx>>,
     pub plural_rules: IntlPluralRules,
 }
 
@@ -75,46 +66,20 @@ impl<'ctx> MessageContext<'ctx> {
         let pr = IntlPluralRules::create(&pr_locale, PluralRuleType::CARDINAL).unwrap();
         MessageContext {
             locales,
-            map: HashMap::new(),
+            entries: HashMap::new(),
             plural_rules: pr,
         }
     }
 
     pub fn has_message(&self, id: &str) -> bool {
-        self.map.get(id).map_or(false, |id| {
-            if let Entry::Message(_) = id {
-                true
-            } else {
-                false
-            }
-        })
-    }
-
-    pub fn get_message(&self, id: &str) -> Option<&ast::Message> {
-        self.map.get(id).and_then(|id| {
-            if let Entry::Message(ref msg) = id {
-                Some(msg)
-            } else {
-                None
-            }
-        })
-    }
-
-    pub fn get_term(&self, id: &str) -> Option<&ast::Term> {
-        self.map.get(id).and_then(|id| {
-            if let Entry::Term(ref term) = id {
-                Some(term)
-            } else {
-                None
-            }
-        })
+        self.entries.get_message(id).is_some()
     }
 
     pub fn add_function<F>(&mut self, id: &str, func: F) -> Result<(), FluentError>
     where
         F: 'ctx + Fn(&[Option<FluentValue>], &HashMap<String, FluentValue>) -> Option<FluentValue>,
     {
-        match self.map.entry(id.to_owned()) {
+        match self.entries.entry(id.to_owned()) {
             HashEntry::Vacant(entry) => {
                 entry.insert(Entry::Function(Box::new(func)));
                 Ok(())
@@ -126,23 +91,6 @@ impl<'ctx> MessageContext<'ctx> {
         }
     }
 
-    pub fn get_function(
-        &self,
-        id: &str,
-    ) -> Option<
-        &Box<
-            'ctx + Fn(&[Option<FluentValue>], &HashMap<String, FluentValue>) -> Option<FluentValue>,
-        >,
-    > {
-        self.map.get(id).and_then(|id| {
-            if let Entry::Function(ref func) = id {
-                Some(func)
-            } else {
-                None
-            }
-        })
-    }
-
     pub fn add_messages(&mut self, source: &str) -> Result<(), FluentError> {
         let res = parse(source).unwrap_or_else(|x| x.0);
 
@@ -159,7 +107,7 @@ impl<'ctx> MessageContext<'ctx> {
                 _ => continue,
             };
 
-            match self.map.entry(id.clone()) {
+            match self.entries.entry(id.clone()) {
                 HashEntry::Vacant(empty) => {
                     empty.insert(entry);
                 }
@@ -172,19 +120,38 @@ impl<'ctx> MessageContext<'ctx> {
         Ok(())
     }
 
-    pub fn format<T: ResolveValue>(
-        &self,
-        resolvable: &T,
-        args: Option<&HashMap<&str, FluentValue>>,
-    ) -> Option<String> {
+    pub fn format(&self, path: &str, args: Option<&HashMap<&str, FluentValue>>) -> Option<String> {
         let env = Env {
             ctx: self,
             args,
             travelled: RefCell::new(Vec::new()),
         };
-        resolvable
-            .to_value(&env)
-            .ok()
-            .map(|value| value.format(self))
+
+        // `path` may be a simple message identifier (`identifier`) or a path to
+        // an attribute of a message (`identifier.attrname`).
+        let mut parts = path.split('.');
+
+        // Retrieve the message by id from the context.
+        let message_id = parts.next()?;
+        let message = self.entries.get_message(message_id)?;
+
+        // Check the second and the third part of the path. The second part may
+        // be an attribute name. If the third part is present, the path is
+        // invalid and contains more than one period (e.g. `foo.bar.baz`).
+        match (parts.next(), parts.next()) {
+            (None, None) => message.to_value(&env).map(|value| value.format(self)).ok(),
+            (Some(attr_name), None) => message.attributes.as_ref().and_then(|attributes| {
+                for attribute in attributes {
+                    if attribute.id.name == attr_name {
+                        return attribute
+                            .to_value(&env)
+                            .map(|value| value.format(self))
+                            .ok();
+                    }
+                }
+                None
+            }),
+            _ => None,
+        }
     }
 }
diff --git a/fluent/src/entry.rs b/fluent/src/entry.rs
new file mode 100644
index 0000000..a6a9ca2
--- /dev/null
+++ b/fluent/src/entry.rs
@@ -0,0 +1,43 @@
+//! `Entry` is used to store Messages, Terms and Functions in `MessageContext` instances.
+use std::collections::hash_map::HashMap;
+
+use super::types::FluentValue;
+use fluent_syntax::ast;
+
+type FluentFunction<'ctx> =
+    Box<'ctx + Fn(&[Option<FluentValue>], &HashMap<String, FluentValue>) -> Option<FluentValue>>;
+
+pub enum Entry<'ctx> {
+    Message(ast::Message),
+    Term(ast::Term),
+    Function(FluentFunction<'ctx>),
+}
+
+pub trait GetEntry<'ctx> {
+    fn get_term(&self, id: &str) -> Option<&ast::Term>;
+    fn get_message(&self, id: &str) -> Option<&ast::Message>;
+    fn get_function(&self, id: &str) -> Option<&FluentFunction<'ctx>>;
+}
+
+impl<'ctx> GetEntry<'ctx> for HashMap<String, Entry<'ctx>> {
+    fn get_term(&self, id: &str) -> Option<&ast::Term> {
+        self.get(id).and_then(|entry| match entry {
+            Entry::Term(term) => Some(term),
+            _ => None,
+        })
+    }
+
+    fn get_message(&self, id: &str) -> Option<&ast::Message> {
+        self.get(id).and_then(|entry| match entry {
+            Entry::Message(message) => Some(message),
+            _ => None,
+        })
+    }
+
+    fn get_function(&self, id: &str) -> Option<&FluentFunction<'ctx>> {
+        self.get(id).and_then(|entry| match entry {
+            Entry::Function(function) => Some(function),
+            _ => None,
+        })
+    }
+}
diff --git a/fluent/src/errors.rs b/fluent/src/errors.rs
index 94ccf9a..163cd9a 100644
--- a/fluent/src/errors.rs
+++ b/fluent/src/errors.rs
@@ -1,9 +1,5 @@
 #[derive(Debug, Fail)]
 pub enum FluentError {
-    #[fail(
-        display = "attempted to override an existing {}: {}",
-        kind,
-        id
-    )]
+    #[fail(display = "attempted to override an existing {}: {}", kind, id)]
     Overriding { kind: &'static str, id: String },
 }
diff --git a/fluent/src/lib.rs b/fluent/src/lib.rs
index e576f63..fda521a 100644
--- a/fluent/src/lib.rs
+++ b/fluent/src/lib.rs
@@ -16,7 +16,6 @@
 //! use std::collections::HashMap;
 //!
 //! let mut ctx = MessageContext::new(&["en-US"]);
-//!
 //! ctx.add_messages(
 //!     "
 //! hello-world = Hello, world!
@@ -24,17 +23,13 @@
 //! "
 //!     );
 //!
-//! let msg = ctx.get_message("hello-world").unwrap();
-//! let value = ctx.format(msg, None).unwrap();
-//!
+//! let value = ctx.format("hello-world", None).unwrap();
 //! assert_eq!(value, "Hello, world!");
 //!
 //! let mut args = HashMap::new();
 //! args.insert("name", FluentValue::from("John"));
 //!
-//! let msg = ctx.get_message("intro").unwrap();
-//! let value = ctx.format(msg, Some(&args)).unwrap();
-//!
+//! let value = ctx.format("intro", Some(&args)).unwrap();
 //! assert_eq!(value, "Welcome, John.");
 //! ```
 
@@ -46,6 +41,7 @@ extern crate fluent_syntax;
 extern crate intl_pluralrules;
 
 pub mod context;
+pub mod entry;
 pub mod errors;
 pub mod resolve;
 pub mod types;
diff --git a/fluent/src/resolve.rs b/fluent/src/resolve.rs
index ae38359..2623065 100644
--- a/fluent/src/resolve.rs
+++ b/fluent/src/resolve.rs
@@ -12,6 +12,7 @@ use std::collections::HashMap;
 use std::hash::{Hash, Hasher};
 
 use super::context::MessageContext;
+use super::entry::GetEntry;
 use super::types::FluentValue;
 use fluent_syntax::ast;
 
@@ -131,11 +132,13 @@ impl ResolveValue for ast::Expression {
             ast::Expression::NumberExpression { value } => value.to_value(env),
             ast::Expression::MessageReference { ref id } if id.name.starts_with('-') => env
                 .ctx
+                .entries
                 .get_term(&id.name)
                 .ok_or(FluentError::None)?
                 .to_value(env),
             ast::Expression::MessageReference { ref id } => env
                 .ctx
+                .entries
                 .get_message(&id.name)
                 .ok_or(FluentError::None)?
                 .to_value(env),
@@ -185,12 +188,14 @@ impl ResolveValue for ast::Expression {
             ast::Expression::AttributeExpression { id, name } => {
                 let attributes = if id.name.starts_with('-') {
                     env.ctx
+                        .entries
                         .get_term(&id.name)
                         .ok_or(FluentError::None)?
                         .attributes
                         .as_ref()
                 } else {
                     env.ctx
+                        .entries
                         .get_message(&id.name)
                         .ok_or(FluentError::None)?
                         .attributes
@@ -206,7 +211,7 @@ impl ResolveValue for ast::Expression {
                 Err(FluentError::None)
             }
             ast::Expression::VariantExpression { id, key } if id.name.starts_with('-') => {
-                let term = env.ctx.get_term(&id.name).ok_or(FluentError::None)?;
+                let term = env.ctx.entries.get_term(&id.name).ok_or(FluentError::None)?;
                 let variants = match term.value.elements.as_slice() {
                     [ast::PatternElement::Placeable(ast::Expression::SelectExpression {
                         expression: None,
@@ -256,6 +261,7 @@ impl ResolveValue for ast::Expression {
                 }
 
                 env.ctx
+                    .entries
                     .get_function(&callee.name)
                     .ok_or(FluentError::None)
                     .and_then(|func| {


Test patch:
diff --git a/fluent/tests/format.rs b/fluent/tests/format.rs
new file mode 100644
index 0000000..6a31c3b
--- /dev/null
+++ b/fluent/tests/format.rs
@@ -0,0 +1,39 @@
+extern crate fluent;
+
+use self::fluent::context::MessageContext;
+
+#[test]
+fn format() {
+    let mut ctx = MessageContext::new(&["x-testing"]);
+    ctx.add_messages(
+        "
+foo = Foo
+    .attr = Attribute
+-term = Term
+",
+    );
+
+    let value = ctx.format("foo", None);
+    assert_eq!(value, Some("Foo".to_string()));
+
+    let value = ctx.format("foo.attr", None);
+    assert_eq!(value, Some("Attribute".to_string()));
+
+    let value = ctx.format("foo.missing", None);
+    assert_eq!(value, None);
+
+    let value = ctx.format("foo.attr.nested", None);
+    assert_eq!(value, None);
+
+    let value = ctx.format("missing", None);
+    assert_eq!(value, None);
+
+    let value = ctx.format("missing.attr", None);
+    assert_eq!(value, None);
+
+    let value = ctx.format("-term", None);
+    assert_eq!(value, None);
+
+    let value = ctx.format("-term.attr", None);
+    assert_eq!(value, None);
+}
diff --git a/fluent/tests/resolve_attribute_expression.rs b/fluent/tests/resolve_attribute_expression.rs
index 9356095..188f725 100644
--- a/fluent/tests/resolve_attribute_expression.rs
+++ b/fluent/tests/resolve_attribute_expression.rs
@@ -23,33 +23,21 @@ missing-missing = { missing.missing }
 ",
     );
 
-    let value = ctx
-        .get_message("use-foo")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-foo", None);
     assert_eq!(value, Some("Foo".to_string()));
 
-    let value = ctx
-        .get_message("use-foo-attr")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-foo-attr", None);
     assert_eq!(value, Some("Foo Attr".to_string()));
 
-    let value = ctx
-        .get_message("use-bar")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-bar", None);
     assert_eq!(value, Some("___".to_string()));
 
-    let value = ctx
-        .get_message("use-bar-attr")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-bar-attr", None);
     assert_eq!(value, Some("Bar Attr".to_string()));
 
-    let value = ctx
-        .get_message("missing-attr")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("missing-attr", None);
     assert_eq!(value, Some("___".to_string()));
 
-    let value = ctx
-        .get_message("missing-missing")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("missing-missing", None);
     assert_eq!(value, Some("___".to_string()));
 }
diff --git a/fluent/tests/resolve_external_argument.rs b/fluent/tests/resolve_external_argument.rs
index 5181589..9f0b1e0 100644
--- a/fluent/tests/resolve_external_argument.rs
+++ b/fluent/tests/resolve_external_argument.rs
@@ -14,10 +14,7 @@ fn external_argument_string() {
     let mut args = HashMap::new();
     args.insert("name", FluentValue::from("John"));
 
-    let value = ctx
-        .get_message("hello-world")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("hello-world", Some(&args));
     assert_eq!(value, Some("Hello John".to_string()));
 }
 
@@ -32,16 +29,10 @@ fn external_argument_number() {
     args.insert("emailsCount", FluentValue::from(5));
     args.insert("emailsCountDec", FluentValue::as_number("5.0").unwrap());
 
-    let value = ctx
-        .get_message("unread-emails")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("unread-emails", Some(&args));
     assert_eq!(value, Some("You have 5 unread emails.".to_string()));
 
-    let value = ctx
-        .get_message("unread-emails-dec")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("unread-emails-dec", Some(&args));
     assert_eq!(value, Some("You have 5.0 unread emails.".to_string()));
 }
 
@@ -55,9 +46,6 @@ fn reference_message_with_external_argument() {
     let mut args = HashMap::new();
     args.insert("userName", FluentValue::from("Mary"));
 
-    let value = ctx
-        .get_message("click-on")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("click-on", Some(&args));
     assert_eq!(value, Some("Click on the `Hello, Mary` label.".to_string()));
 }
diff --git a/fluent/tests/resolve_message_reference.rs b/fluent/tests/resolve_message_reference.rs
index 4b4328b..3a5192f 100644
--- a/fluent/tests/resolve_message_reference.rs
+++ b/fluent/tests/resolve_message_reference.rs
@@ -5,7 +5,6 @@ use self::fluent::context::MessageContext;
 #[test]
 fn message_reference() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo = Foo
@@ -13,14 +12,13 @@ bar = { foo } Bar
 ",
     );
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Foo Bar".to_string()));
 }
 
 #[test]
 fn term_reference() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 -foo = Foo
@@ -28,14 +26,13 @@ bar = { -foo } Bar
 ",
     );
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Foo Bar".to_string()));
 }
 
 #[test]
 fn message_reference_nested() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo = Foo
@@ -44,17 +41,16 @@ baz = { bar } Baz
 ",
     );
 
-    let value = ctx.get_message("baz").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("baz", None);
     assert_eq!(value, Some("Foo Bar Baz".to_string()));
 }
 
 #[test]
 fn message_reference_missing() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages("bar = { foo } Bar");
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("___ Bar".to_string()));
 }
 
@@ -62,7 +58,6 @@ fn message_reference_missing() {
 fn message_reference_cyclic() {
     {
         let mut ctx = MessageContext::new(&["x-testing"]);
-
         ctx.add_messages(
             "
 foo = Foo { bar }
@@ -70,15 +65,14 @@ bar = { foo } Bar
 ",
         );
 
-        let value = ctx.get_message("foo").and_then(|msg| ctx.format(msg, None));
+        let value = ctx.format("foo", None);
         assert_eq!(value, Some("Foo ___".to_string()));
-        let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+        let value = ctx.format("bar", None);
         assert_eq!(value, Some("___ Bar".to_string()));
     }
 
     {
         let mut ctx = MessageContext::new(&["x-testing"]);
-
         ctx.add_messages(
             "
 foo = { bar }
@@ -86,9 +80,9 @@ bar = { foo }
 ",
         );
 
-        let value = ctx.get_message("foo").and_then(|msg| ctx.format(msg, None));
+        let value = ctx.format("foo", None);
         assert_eq!(value, Some("___".to_string()));
-        let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+        let value = ctx.format("bar", None);
         assert_eq!(value, Some("___".to_string()));
     }
 }
@@ -96,7 +90,6 @@ bar = { foo }
 #[test]
 fn message_reference_multiple() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo = Foo
@@ -104,6 +97,6 @@ bar = { foo } Bar { foo }
 ",
     );
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Foo Bar Foo".to_string()));
 }
diff --git a/fluent/tests/resolve_plural_rule.rs b/fluent/tests/resolve_plural_rule.rs
index 7cb26f9..9f0ad55 100644
--- a/fluent/tests/resolve_plural_rule.rs
+++ b/fluent/tests/resolve_plural_rule.rs
@@ -8,7 +8,6 @@ use self::fluent::types::FluentValue;
 #[test]
 fn external_argument_number() {
     let mut ctx = MessageContext::new(&["en"]);
-
     ctx.add_messages(
         "
 unread-emails =
@@ -30,23 +29,16 @@ unread-emails-dec =
     args.insert("emailsCount", FluentValue::from(1));
     args.insert("emailsCountDec", FluentValue::as_number("1.0").unwrap());
 
-    let value = ctx
-        .get_message("unread-emails")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("unread-emails", Some(&args));
     assert_eq!(value, Some("You have 1 unread email.".to_string()));
 
-    let value = ctx
-        .get_message("unread-emails-dec")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("unread-emails-dec", Some(&args));
     assert_eq!(value, Some("You have 1.0 unread emails.".to_string()));
 }
 
 #[test]
 fn exact_match() {
     let mut ctx = MessageContext::new(&["en"]);
-
     ctx.add_messages(
         "
 unread-emails =
@@ -70,15 +62,9 @@ unread-emails-dec =
     args.insert("emailsCount", FluentValue::from(1));
     args.insert("emailsCountDec", FluentValue::as_number("1.0").unwrap());
 
-    let value = ctx
-        .get_message("unread-emails")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("unread-emails", Some(&args));
     assert_eq!(value, Some("You have one unread email.".to_string()));
 
-    let value = ctx
-        .get_message("unread-emails-dec")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
-
+    let value = ctx.format("unread-emails-dec", Some(&args));
     assert_eq!(value, Some("You have one unread email.".to_string()));
 }
diff --git a/fluent/tests/resolve_select_expression.rs b/fluent/tests/resolve_select_expression.rs
index aa54a02..495bc60 100644
--- a/fluent/tests/resolve_select_expression.rs
+++ b/fluent/tests/resolve_select_expression.rs
@@ -25,17 +25,16 @@ bar =
 ",
     );
 
-    let value = ctx.get_message("foo").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("foo", None);
     assert_eq!(value, Some("Foo".to_string()));
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Bar".to_string()));
 }
 
 #[test]
 fn select_expression_string_selector() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo =
@@ -52,17 +51,16 @@ bar =
 ",
     );
 
-    let value = ctx.get_message("foo").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("foo", None);
     assert_eq!(value, Some("Foo's".to_string()));
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Bar".to_string()));
 }
 
 #[test]
 fn select_expression_number_selector() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo =
@@ -86,20 +84,19 @@ baz =
 ",
     );
 
-    let value = ctx.get_message("foo").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("foo", None);
     assert_eq!(value, Some("Foo 3".to_string()));
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Bar 1".to_string()));
 
-    let value = ctx.get_message("baz").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("baz", None);
     assert_eq!(value, Some("Baz Pi".to_string()));
 }
 
 #[test]
 fn select_expression_plurals() {
     let mut ctx = MessageContext::new(&["en"]);
-
     ctx.add_messages(
         "
 foo =
@@ -125,20 +122,19 @@ baz =
 ",
     );
 
-    let value = ctx.get_message("foo").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("foo", None);
     assert_eq!(value, Some("Foo 3".to_string()));
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Bar One".to_string()));
 
-    let value = ctx.get_message("baz").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("baz", None);
     assert_eq!(value, Some("Bar Other".to_string()));
 }
 
 #[test]
 fn select_expression_external_argument_selector() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo-hit =
@@ -202,56 +198,37 @@ baz-unknown =
     args.insert("int", FluentValue::from(3));
     args.insert("float", FluentValue::from(2.72));
 
-    let value = ctx
-        .get_message("foo-hit")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("foo-hit", Some(&args));
     assert_eq!(value, Some("Qux".to_string()));
 
-    let value = ctx
-        .get_message("foo-miss")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("foo-miss", Some(&args));
     assert_eq!(value, Some("Foo".to_string()));
 
-    let value = ctx
-        .get_message("foo-unknown")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("foo-unknown", Some(&args));
     assert_eq!(value, Some("Foo".to_string()));
 
-    let value = ctx
-        .get_message("bar-hit")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("bar-hit", Some(&args));
     assert_eq!(value, Some("Bar 3".to_string()));
 
-    let value = ctx
-        .get_message("bar-miss")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("bar-miss", Some(&args));
     assert_eq!(value, Some("Bar 1".to_string()));
 
-    let value = ctx
-        .get_message("bar-unknown")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("bar-unknown", Some(&args));
     assert_eq!(value, Some("Bar 1".to_string()));
 
-    let value = ctx
-        .get_message("baz-hit")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("baz-hit", Some(&args));
     assert_eq!(value, Some("Baz E".to_string()));
 
-    let value = ctx
-        .get_message("baz-miss")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("baz-miss", Some(&args));
     assert_eq!(value, Some("Baz 1".to_string()));
 
-    let value = ctx
-        .get_message("baz-unknown")
-        .and_then(|msg| ctx.format(msg, Some(&args)));
+    let value = ctx.format("baz-unknown", Some(&args));
     assert_eq!(value, Some("Baz 1".to_string()));
 }
 
 #[test]
 fn select_expression_message_selector() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 -bar = Bar
@@ -265,16 +242,13 @@ use-bar =
 ",
     );
 
-    let value = ctx
-        .get_message("use-bar")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-bar", None);
     assert_eq!(value, Some("Bar".to_string()));
 }
 
 #[test]
 fn select_expression_attribute_selector() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 -foo = Foo
@@ -288,8 +262,6 @@ use-foo =
 ",
     );
 
-    let value = ctx
-        .get_message("use-foo")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-foo", None);
     assert_eq!(value, Some("Foo".to_string()));
 }
diff --git a/fluent/tests/resolve_value.rs b/fluent/tests/resolve_value.rs
index fd95374..0bbe080 100644
--- a/fluent/tests/resolve_value.rs
+++ b/fluent/tests/resolve_value.rs
@@ -5,22 +5,19 @@ use self::fluent::context::MessageContext;
 #[test]
 fn format_message() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo = Foo
 ",
     );
 
-    let value = ctx.get_message("foo").and_then(|msg| ctx.format(msg, None));
-
+    let value = ctx.format("foo", None);
     assert_eq!(value, Some("Foo".to_string()));
 }
 
 #[test]
 fn format_attribute() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 foo = Foo
@@ -28,14 +25,6 @@ foo = Foo
 ",
     );
 
-    if let Some(attributes) = ctx
-        .get_message("foo")
-        .and_then(|message| message.attributes.as_ref())
-    {
-        let value = attributes
-            .first()
-            .and_then(|attribute| ctx.format(attribute, None));
-
-        assert_eq!(value, Some("Foo Attr".to_string()));
-    }
+    let value = ctx.format("foo.attr", None);
+    assert_eq!(value, Some("Foo Attr".to_string()));
 }
diff --git a/fluent/tests/resolve_variant_expression.rs b/fluent/tests/resolve_variant_expression.rs
index d5133a8..f023794 100644
--- a/fluent/tests/resolve_variant_expression.rs
+++ b/fluent/tests/resolve_variant_expression.rs
@@ -5,7 +5,6 @@ use self::fluent::context::MessageContext;
 #[test]
 fn variant_expression() {
     let mut ctx = MessageContext::new(&["x-testing"]);
-
     ctx.add_messages(
         "
 -foo = Foo
@@ -28,41 +27,27 @@ missing-missing = { -missing[missing] }
 ",
     );
 
-    let value = ctx.get_message("bar").and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("bar", None);
     assert_eq!(value, Some("Bar".to_string()));
 
-    let value = ctx
-        .get_message("use-foo")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-foo", None);
     assert_eq!(value, Some("Foo".to_string()));
 
-    let value = ctx
-        .get_message("use-foo-missing")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-foo-missing", None);
     assert_eq!(value, Some("Foo".to_string()));
 
-    let value = ctx
-        .get_message("use-bar")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-bar", None);
     assert_eq!(value, Some("Bar".to_string()));
 
-    let value = ctx
-        .get_message("use-bar-nominative")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-bar-nominative", None);
     assert_eq!(value, Some("Bar".to_string()));
 
-    let value = ctx
-        .get_message("use-bar-genitive")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-bar-genitive", None);
     assert_eq!(value, Some("Bar's".to_string()));
 
-    let value = ctx
-        .get_message("use-bar-missing")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("use-bar-missing", None);
     assert_eq!(value, Some("Bar".to_string()));
 
-    let value = ctx
-        .get_message("missing-missing")
-        .and_then(|msg| ctx.format(msg, None));
+    let value = ctx.format("missing-missing", None);
     assert_eq!(value, Some("___".to_string()));
 }