human-string-filler 1.0.0

A tiny template language for human-friendly string substitutions
Documentation
#![cfg_attr(not(feature = "std"), no_std)]
#![deny(missing_docs)]
#![cfg_attr(docsrs, feature(doc_cfg))]
//! A tiny template language for human-friendly string substitutions.
//!
//! This crate is intended for situations where you need the user to be able to write simple
//! templated strings, and conveniently evaluate them. It’s deliberately simple so that there are
//! no surprises in its performance or functionality, and so that it’s not accidentally tied to
//! Rust (e.g. you can readily implement it in a JavaScript-powered web app), which would happen
//! if things like number formatting specifiers were included out of the box—instead, if you want
//! that sort of thing, you’ll have to implement it yourself (don’t worry, it won’t be hard).
//!
//! No logic is provided in this template language, only simple string formatting: `{…}` template
//! regions get replaced in whatever way you decide, curly braces get escaped by doubling them
//! (`{{` and `}}`), and *that’s it*.
//!
//! ## Sample usage
//!
//! The **lowest-level** handling looks like this:
//!
//! ```rust
//! use human_string_filler::{StrExt, SimpleFillerError};
//!
//! let mut output = String::new();
//! "Hello, {name}!".fill_into(&mut output, |output: &mut String, key: &str| {
//!     match key {
//!         "name" => output.push_str("world"),
//!         _ => return Err(SimpleFillerError::NoSuchKey),
//!     }
//!     Ok(())
//! }).unwrap();
//!
//! assert_eq!(output, "Hello, world!");
//! ```
//!
//! `template.fill_into(output, filler)` (provided by `StrExt`) can also be spelled
//! `fill(template, filler, output)` if you prefer a function to a method
//! (I reckon the method syntax is clearer, but opinions will differ so I provided both).
//!
//! The filler function appends to the string directly for efficiency in case of computed values,
//! and returns `Result<(), E>`; any error will become `Err(Error::BadReplacement { error, .. })`
//! on the fill call. (In this example I’ve used `SimpleFillerError::NoSuchKey`, but `()` would
//! work almost as well, or you can write your own error type altogether.)
//!
//! This example showed a closure that took `&mut String` and used `.push_str(…)`, but this crate
//! is not tied to `String` in any way: for greater generality you would use a function generic
//! over a type that implements `std::fmt::Write`, and use `.write_str(…)?` inside (`?` works there
//! because `SimpleFillerError` implements `From<std::fmt::Error>`).
//!
//! At a **higher level**, you can use a string-string map as a filler, and you can also fill
//! directly to a `String` with `.fill_to_string()` (also available as a standalone function
//! `fill_to_string`):
//!
//! ```rust
//! # #[cfg(feature = "std")] {
//! use std::collections::HashMap;
//! use human_string_filler::StrExt;
//!
//! let mut map = HashMap::new();
//! map.insert("name", "world");
//!
//! let s = "Hello, {name}!".fill_to_string(&map);
//!
//! assert_eq!(s.unwrap(), "Hello, world!");
//! # }
//! ```
//!
//! Or you can implement the [`Filler`] trait for some other type of your own if you like.
//!
//! ## Cargo features
//!
#![cfg_attr(
    feature = "std",
    doc = " \
    - **std** (enabled by default, enabled in this build): remove for `#![no_std]` operation. \
      Implies *alloc*.\
"
)]
#![cfg_attr(
    not(feature = "std"),
    doc = " \
    - **std** (enabled by default, *disabled* in this build): remove for `#![no_std]` operation. \
      Implies *alloc*.\
"
)]
//!     - Implementation of `std::error::Error` for `Error`;
//!     - Implementation of `Filler` for `&HashMap`.
//!
#![cfg_attr(
    feature = "alloc",
    doc = " \
    - **alloc** (enabled by default via *std*, enabled in this build):\
"
)]
#![cfg_attr(
    not(feature = "alloc"),
    doc = " \
    - **alloc** (enabled by default via *std*, disabled in this build):\
"
)]
//!     - Implementation of `Filler` for `&BTreeMap`.
//!     - `fill_to_string` and `StrExt::fill_to_string`.
//!
//! ## The template language
//!
//! This is the grammar of the template language in [ABNF](https://tools.ietf.org/html/rfc5234):
//!
//! ```abnf
//! unescaped-normal-char = %x00-7A / %x7C / %x7E-D7FF / %xE000-10FFFF
//!                       ; any Unicode scalar value except for "{" and "}"
//!
//! normal-char           = unescaped-normal-char / "{{" / "}}"
//!
//! template-region       = "{" *unescaped-normal-char "}"
//!
//! template-string       = *( normal-char / template-region )
//! ```
//!
//! This regular expression will validate a template string:
//!
//! ```text
//! ^([^{}]|\{\{|\}\}|\{[^{}]*\})*$
//! ```
//!
//! Sample legal template strings:
//!
//! - The empty string
//! - `Hello, {name}!`: one template region with key "name".
//! - `Today is {date:short}`: one template region with key "date:short". (Although there’s no
//!   format specification like with the `format!()` macro, a colon convention is one reasonable
//!   option—see the next section.)
//! - `Hello, {}!`: one template region with an empty key, not recommended but allowed.
//! - `Escaped {{ braces {and replacements} for {fun}!`: string "Escaped { braces ", followed by a
//!   template region with key "and replacements", followed by string " for ", followed by a
//!   template region with key "fun", followed by string "!".
//!
//! Sample illegal template strings:
//!
//! - `hello, {world}foo}`: opening and closing curlies must match; any others (specifically, the
//!   last character of the string) must be escaped by doubling.
//! - `{{thing}`: the `{{` is an escaped opening curly, so the `}` is unmatched.
//! - `{thi{{n}}g}`: no curlies of any form inside template region keys. (It’s possible that a
//!   future version may make it possible to escape curlies inside template regions, if it proves
//!   to be useful in something like format specifiers; but not at this time.)
//!
//! ## Conventions on key semantics
//!
//! The key is an arbitrary string (except that it can’t contain `{` or `}`) with explicitly no
//! defined semantics, but here are some suggestions, including helper functions:
//!
//! 1. If it makes sense to have a format specifier (e.g. to specify a date format to use, or
//!    whether to pad numbers with leading zeroes, *&c.*), split once on a character like `:`.
//!    To do this most conveniently, a function [`split_on`] is provided.
//!
//! 2. For more advanced formatting where you have multiple properties you could wish to set,
//!    [`split_propertied`] offers some sound and similarly simple semantics for such strings as
//!    `{key prop1 prop2=val2}` and `{key:prop1,prop2=val2}`.
//!
//! 3. If it makes sense to have nested property access, split on `.` with the `key.split('.')`
//!    iterator. (If you’re using `split_on` or `split_propertied` as mentioned above, you
//!    probably want to apply them first to separate out the key part.)
//!
//! 4. Only use [UAX #31 identifiers](https://www.unicode.org/reports/tr31/) for the key
//!    (or keys, if supporting nested property access). Most of the time, empty strings and
//!    numbers are probably not a good idea.
//!
//! With these suggestions, you might end up with the key `foo.bar:baz` being interpreted as
//! retrieving the “bar” property from the “foo” object, and formatting it according to “baz”; or
//! `aleph.beth.gimmel|alpha beta=5` as retrieving “gimmel” from “beth” of “aleph”, and formatting
//! it with properties “alpha” set to true and “beta” set to 5. What those things actually *mean*
//! is up to you to decide. *I* certainly haven’t a clue.

use core::fmt;
use core::iter::FusedIterator;
use core::ops::Range;

#[cfg(feature = "alloc")]
extern crate alloc;

#[cfg(feature = "alloc")]
use alloc::string::String;

#[cfg(feature = "alloc")]
use alloc::collections::BTreeMap;
#[cfg(feature = "alloc")]
use core::borrow::Borrow;
#[cfg(feature = "std")]
use std::collections::HashMap;
#[cfg(feature = "std")]
use std::hash::Hash;

/// Any error that occurs when filling a template string.
///
/// Template parsing and filling is all done in a single pass; so a failed replacement due to an
/// unknown key will shadow a syntax error later in the string.
#[derive(Debug, PartialEq, Eq)]
pub enum Error<'a, E> {
    /// A template region was not closed.
    /// That is, an opening curly brace (`{`) with no matching closing curly brace (`}`).
    ///
    /// Example:
    ///
    /// ```rust
    /// # #[cfg(feature = "alloc")] {
    /// # use human_string_filler::{StrExt, Error};
    /// # assert_eq!(
    /// "Hello, {thing"
    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
    /// # Err(Error::UnclosedRegion { source: "{thing", range: 7..13 }),
    /// # );
    /// # }
    /// ```
    UnclosedRegion {
        /// The text of the unclosed region, which will start with `{` and contain no other curly
        /// braces.
        source: &'a str,
        /// The indexes of `source` within the template string.
        range: Range<usize>,
    },

    /// An unescaped closing curly brace (`}`) was found, outside a template region.
    ///
    /// Examples:
    ///
    /// ```rust
    /// # #[cfg(feature = "alloc")] {
    /// # use human_string_filler::{StrExt, Error};
    /// # assert_eq!(
    /// "Hello, thing}!"
    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
    /// # Err(Error::UnexpectedClosingBrace { index: 12 }),
    /// # );
    /// # assert_eq!(
    /// "Hello, {name}, look at my magnificent moustache: (}-:"
    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
    /// # Err(Error::UnexpectedClosingBrace { index: 50 }),
    /// # );
    /// # assert_eq!(
    /// "Hello, {name}}!"
    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
    /// # Err(Error::UnexpectedClosingBrace { index: 13 }),
    /// # );
    /// # }
    /// ```
    UnexpectedClosingBrace {
        /// The index of the closing brace within the template string.
        index: usize,
    },

    /// An opening curly brace (`{`) was found within a template region.
    ///
    /// Examples:
    ///
    /// ```rust
    /// # #[cfg(feature = "alloc")] {
    /// # use human_string_filler::{StrExt, Error};
    /// # assert_eq!(
    /// "Hello, {thing{{sadness}}}"
    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
    /// # Err(Error::UnexpectedOpeningBrace { index: 13 }),
    /// # );
    /// # }
    /// ```
    UnexpectedOpeningBrace {
        /// The index of the opening brace within the template string.
        index: usize,
    },

    /// The filler returned an error for the specified key.
    BadReplacement {
        /// The key on which the filler failed. Curly braces not included.
        key: &'a str,
        /// The indexes of `key` within the template string.
        range: Range<usize>,
        /// The error value returned by the filler.
        error: E,
    },

    /// Writing to the output failed.
    WriteFailed(fmt::Error),
}

impl<'a, E> From<fmt::Error> for Error<'a, E> {
    fn from(e: fmt::Error) -> Self {
        Error::WriteFailed(e)
    }
}

impl<'a, E> fmt::Display for Error<'a, E>
where
    E: fmt::Display,
{
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Error::UnclosedRegion { source, .. } => {
                write!(f, "Unclosed template region at \"{}\"", source)
            }

            Error::UnexpectedClosingBrace { index } => {
                write!(f, "Unexpected closing brace at index {}", index)
            }

            Error::UnexpectedOpeningBrace { index } => {
                write!(
                    f,
                    "Unexpected curly brace within template region at index {}",
                    index
                )
            }

            Error::BadReplacement { key, error, .. } => {
                write!(f, "Error in template string at \"{{{}}}\": {}", key, error)
            }

            Error::WriteFailed(fmt::Error) => f.write_str("Error in writing output"),
        }
    }
}

#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl<'a, E> std::error::Error for Error<'a, E>
where
    E: std::error::Error + 'static,
{
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            Error::BadReplacement { error, .. } => Some(error),
            Error::WriteFailed(error) => Some(error),
            _ => None,
        }
    }
}

/// Implementers of this trait have the ability to fill template strings.
///
/// It is extremely strongly recommended that fillers only push to the output, and do not perform
/// any other modifications of it.
///
/// I mean, if you implement `Filler<String, _>`, you get a `&mut String` and it’s *possible* to do
/// other things with it, but that’s a terrible idea. I’m almost ashamed of ideas like making `{␡}`
/// pop the last character, and `{←rot13}` ROT-13-encode what precedes it in the string.
pub trait Filler<W, E>
where
    W: fmt::Write,
{
    /// Fill the value for the given key into the output string.
    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E>;
}

impl<F, W, E> Filler<W, E> for F
where
    F: FnMut(&mut W, &str) -> Result<(), E>,
    W: fmt::Write,
{
    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E> {
        self(output, key)
    }
}

#[cfg_attr(not(feature = "std"), allow(rustdoc::broken_intra_doc_links))]
/// A convenient error type for fillers; you might even like to use it yourself.
///
/// You could also use `()`, but this gives you
/// <code>[From](core::convert::From)&lt;[core::fmt::Error]></code> so that you can use
/// `write!(out, …)?`, and sane [`core::fmt::Display`] and [`std::error::Error`] implementations.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum SimpleFillerError {
    /// The map didn’t contain the requested key.
    NoSuchKey,
    /// Some fmt::Write operation returned an error.
    WriteFailed(fmt::Error),
}

impl From<fmt::Error> for SimpleFillerError {
    fn from(e: fmt::Error) -> Self {
        SimpleFillerError::WriteFailed(e)
    }
}

impl fmt::Display for SimpleFillerError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            SimpleFillerError::NoSuchKey => f.write_str("no such key"),
            SimpleFillerError::WriteFailed(fmt::Error) => f.write_str("write failed"),
        }
    }
}

#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl std::error::Error for SimpleFillerError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            SimpleFillerError::WriteFailed(error) => Some(error),
            _ => None,
        }
    }
}

#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl<K, V, W> Filler<W, SimpleFillerError> for &HashMap<K, V>
where
    K: Borrow<str> + Eq + Hash,
    V: AsRef<str>,
    W: fmt::Write,
{
    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
        self.get(key)
            .ok_or(SimpleFillerError::NoSuchKey)
            .and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
    }
}

#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
impl<K, V, W> Filler<W, SimpleFillerError> for &BTreeMap<K, V>
where
    K: Borrow<str> + Ord,
    V: AsRef<str>,
    W: fmt::Write,
{
    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
        self.get(key)
            .ok_or(SimpleFillerError::NoSuchKey)
            .and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
    }
}

/// String extension methods for the template string.
///
/// This is generally how I recommend using this library, because I find that the method receiver
/// makes code clearer: that `template.fill_into(output, filler)` is easier to understand than
/// `fill(template, filler, output)`.
pub trait StrExt {
    /// Fill this template, producing a new string.
    ///
    /// This is a convenience method for ergonomics in the case where you aren’t fussed about
    /// allocations and are using the standard `String` type.
    ///
    #[cfg_attr(feature = "std", doc = " Example, using a hash map:")]
    #[cfg_attr(
        not(feature = "std"),
        doc = " Example, using a hash map (requires the *std* feature):"
    )]
    ///
    /// ```rust
    /// # #[cfg(feature = "std")] {
    /// # use human_string_filler::StrExt;
    /// # use std::collections::HashMap;
    /// let map = [("name", "world")].into_iter().collect::<HashMap<_, _>>();
    /// assert_eq!(
    ///     "Hello, {name}!".fill_to_string(&map).unwrap(),
    ///     "Hello, world!",
    /// );
    /// # }
    /// ```
    #[cfg(feature = "alloc")]
    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
    fn fill_to_string<F, E>(&self, filler: F) -> Result<String, Error<E>>
    where
        F: Filler<String, E>,
    {
        let mut out = String::new();
        self.fill_into(&mut out, filler).map(|()| out)
    }

    /// Fill this template string into the provided string, with the provided filler.
    ///
    /// Uses an existing string, which is more efficient if you want to push to an existing string
    /// or can reuse a string allocation.
    ///
    /// Example, using a closure:
    ///
    /// ```rust
    /// # use human_string_filler::StrExt;
    /// let filler = |output: &mut String, key: &str| {
    ///     match key {
    ///         "name" => output.push_str("world"),
    ///         _ => return Err(()),
    ///     }
    ///     Ok(())
    /// };
    /// let mut string = String::new();
    /// assert!("Hello, {name}!".fill_into(&mut string, filler).is_ok());
    /// assert_eq!(string, "Hello, world!");
    /// ```
    fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
    where
        F: Filler<W, E>,
        W: fmt::Write;
}

impl StrExt for str {
    #[inline]
    fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
    where
        F: Filler<W, E>,
        W: fmt::Write,
    {
        fill(self, filler, output)
    }
}

/// The lowest-level form, as a function: fill the template string, into a provided writer.
///
/// This is the most efficient form. It splits a string by `{…}` sections, adding anything outside
/// them to the output string (with escaped curlies dedoubled) and passing template regions through
/// the filler, which handles pushing to the output string itself.
///
/// See also [`StrExt::fill_into`] which respells `fill(template, filler, output)` as
/// `template.fill_into(output, filler)`.
pub fn fill<'a, F, W, E>(
    mut template: &'a str,
    mut filler: F,
    output: &mut W,
) -> Result<(), Error<'a, E>>
where
    F: Filler<W, E>,
    W: fmt::Write,
{
    let mut index = 0;
    loop {
        if let Some(i) = template.find(|c| c == '{' || c == '}') {
            #[allow(clippy::wildcard_in_or_patterns)]
            match template.as_bytes()[i] {
                c @ b'}' | c @ b'{' if template.as_bytes().get(i + 1) == Some(&c) => {
                    output.write_str(&template[0..i + 1])?;
                    template = &template[i + 2..];
                    index += i + 2;
                }
                b'}' => return Err(Error::UnexpectedClosingBrace { index: index + i }),
                b'{' | _ => {
                    // (_ here just to lazily skip an unreachable!().)
                    output.write_str(&template[0..i])?;
                    template = &template[i..];
                    index += i;
                    if let Some(i) = template[1..].find(|c| c == '{' || c == '}') {
                        match template.as_bytes()[i + 1] {
                            b'}' => {
                                if let Err(e) = filler.fill(output, &template[1..i + 1]) {
                                    return Err(Error::BadReplacement {
                                        key: &template[1..i + 1],
                                        range: (index + 1)..(index + i + 1),
                                        error: e,
                                    });
                                }
                                template = &template[i + 2..];
                                index += i + 2;
                            }
                            // (Again, _ is unreachable.)
                            b'{' | _ => {
                                return Err(Error::UnexpectedOpeningBrace {
                                    index: index + i + 1,
                                })
                            }
                        }
                    } else {
                        return Err(Error::UnclosedRegion {
                            source: template,
                            range: index..(index + template.len()),
                        });
                    }
                }
            }
        } else {
            output.write_str(template)?;
            break;
        }
    }

    Ok(())
}

/// Fill a template, producing a new string.
///
/// This is a convenience function for ergonomics in the case where you aren’t fussed about
/// allocations and are using the standard `String` type.
///
/// See also [`StrExt::fill_to_string`], which respells `fill_to_string(template, filler)` as
/// `template.fill_to_string(filler)`.
#[cfg(feature = "alloc")]
#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
pub fn fill_to_string<F, E>(template: &str, filler: F) -> Result<String, Error<E>>
where
    F: Filler<String, E>,
{
    let mut out = String::new();
    fill(template, filler, &mut out).map(|()| out)
}

/// A convenience function to split a string on a character.
///
/// This is nicer than using `string.split(c, 2)` because it gives you the two values up-front.
///
/// # Returns
///
/// A two-tuple of:
///
/// 1. What comes before the split character, or the entire string if there was none; and
/// 2. The remainder after the split character, if there was one (even if it’s empty).
///
/// ```
/// # use human_string_filler::split_on;
/// assert_eq!(split_on("The quick brown fox", ':'), ("The quick brown fox", None));
/// assert_eq!(split_on("/", '/'), ("", Some("")));
/// assert_eq!(split_on("harum = scarum", '='), ("harum ", Some(" scarum")));
/// assert_eq!(split_on("diæresis:tréma:umlaut", ':'), ("diæresis", Some("tréma:umlaut")));
/// ```
pub fn split_on(string: &str, c: char) -> (&str, Option<&str>) {
    match string.find(c) {
        Some(i) => (&string[..i], Some(&string[i + c.len_utf8()..])),
        None => (string, None),
    }
}

/// The separators to use in [`split_propertied`].
///
/// A couple of sets of plausible-looking values (but if you want a concrete recommendation, like
/// Gallio of old I refuse to be a judge of these things):
///
/// - `(' ', ' ', '=')` looks like `Hello, {name first formal=false case=lower}!`.
/// - `('|', ',', ':')` looks like `Hello, {name|first,formal:false,case:lower}!`.
#[derive(Clone, Copy, Debug)]
pub struct Separators {
    /// What character indicates the end of the key and the start of the properties.
    pub between_key_and_properties: char,

    /// What character indicates the end of one property’s name or value and the start of the next
    /// property’s name.
    pub between_properties: char,

    /// What character indicates the end of a property’s name and the start of its value.
    /// Remember that properties aren’t required to have values, but can be booleanyish.
    // “booleanyish” sounded better than “booleanishy”. That’s my story and I’m sticking with it.
    /// For that matter, if you want *all* properties to be boolean, set this to the same value as
    /// `between_properties`, because `between_properties` is greedier.
    pub between_property_name_and_value: char,
}

/// A convenience function to split a key that is followed by properties.
///
/// In keeping with this library in general, this is deliberately very simple and consequently not
/// able to express all possible values; for example, if you use space as the separator between
/// properties, you can’t use space in property values; and this doesn’t guard against empty keys
/// or property names in any way.
///
/// ```
/// use human_string_filler::{Separators, split_propertied};
///
/// let (key, properties) = split_propertied("key:prop1,prop2=value2,prop3=4+5=9", Separators {
///     between_key_and_properties: ':',
///     between_properties: ',',
///     between_property_name_and_value: '=',
/// });
///
/// assert_eq!(key, "key");
/// assert_eq!(properties.collect::<Vec<_>>(),
///            vec![("prop1", None), ("prop2", Some("value2")), ("prop3", Some("4+5=9"))]);
/// ```
///
/// This method consumes exactly one character for the separators; if space is your
/// between-properties separator, for example, multiple spaces will not be combined, but
/// you’ll get `("", None)` properties instead. As I say, this is deliberately simple.
pub fn split_propertied(
    s: &str,
    separators: Separators,
) -> (
    &str,
    impl Iterator<Item = (&str, Option<&str>)>
        + DoubleEndedIterator
        + FusedIterator
        + Clone
        + fmt::Debug,
) {
    let (key, properties) = split_on(s, separators.between_key_and_properties);
    let properties = properties
        .map(|properties| properties.split(separators.between_properties))
        .unwrap_or_else(|| {
            // We need an iterator of the same type that will yield None, but Split yields an empty
            // string first. Nice and easy: consume that, then continue on our way.
            let mut dummy = "".split(' ');
            dummy.next();
            dummy
        })
        .map(move |word| split_on(word, separators.between_property_name_and_value));
    (key, properties)
}

#[cfg(test)]
mod tests {
    #[allow(unused_imports)]
    use super::*;

    #[cfg(feature = "alloc")]
    macro_rules! test {
        ($name:ident, $filler:expr) => {
            #[test]
            fn $name() {
                let filler = $filler;

                assert_eq!(
                    "Hello, {}!".fill_to_string(&filler).as_ref().map(|s| &**s),
                    Ok("Hello, (this space intentionally left blank)!"),
                );
                assert_eq!(
                    "Hello, {name}!"
                        .fill_to_string(&filler)
                        .as_ref()
                        .map(|s| &**s),
                    Ok("Hello, world!"),
                );
                assert_eq!(
                    "Hello, {you}!".fill_to_string(&filler),
                    Err(Error::BadReplacement {
                        key: "you",
                        range: 8..11,
                        error: SimpleFillerError::NoSuchKey,
                    }),
                );
                assert_eq!(
                    "I like {keys with SPACES!? 😱}"
                        .fill_to_string(&filler)
                        .as_ref()
                        .map(|s| &**s),
                    Ok("I like identifier-only keys 👌"),
                );
            }
        };
    }

    #[cfg(feature = "alloc")]
    test!(closure_filler, |out: &mut String, key: &str| {
        use core::fmt::Write;
        out.write_str(match key {
            "" => "(this space intentionally left blank)",
            "name" => "world",
            "keys with SPACES!? 😱" => "identifier-only keys 👌",
            _ => return Err(SimpleFillerError::NoSuchKey),
        })
        .map_err(Into::into)
    });

    #[cfg(feature = "std")]
    test!(hash_map_fillter, {
        [
            ("", "(this space intentionally left blank)"),
            ("name", "world"),
            ("keys with SPACES!? 😱", "identifier-only keys 👌"),
        ]
        .into_iter()
        .collect::<HashMap<_, _>>()
    });

    #[cfg(feature = "alloc")]
    test!(btree_map_fillter, {
        [
            ("", "(this space intentionally left blank)"),
            ("name", "world"),
            ("keys with SPACES!? 😱", "identifier-only keys 👌"),
        ]
        .into_iter()
        .collect::<BTreeMap<_, _>>()
    });

    #[test]
    #[cfg(feature = "alloc")]
    fn fill_errors() {
        let c = |_: &mut String, _: &str| -> Result<(), ()> { Ok(()) };

        assert_eq!(
            fill_to_string("Hello, {thing", c),
            Err(Error::UnclosedRegion {
                source: "{thing",
                range: 7..13
            })
        );
        assert_eq!(
            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{thing", c),
            Err(Error::UnclosedRegion {
                source: "{thing",
                range: 24..30
            })
        );

        assert_eq!(
            fill_to_string("Hello, }thing", c),
            Err(Error::UnexpectedClosingBrace { index: 7 })
        );
        assert_eq!(
            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}thing", c),
            Err(Error::UnexpectedClosingBrace { index: 24 })
        );

        assert_eq!(
            fill_to_string("Hello, {thi{{ng}", c),
            Err(Error::UnexpectedOpeningBrace { index: 11 })
        );
        assert_eq!(
            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{x{", c),
            Err(Error::UnexpectedOpeningBrace { index: 26 })
        );

        assert_eq!(
            fill_to_string("Hello, {thi}}ng}", c),
            Err(Error::UnexpectedClosingBrace { index: 12 })
        );
        assert_eq!(
            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}", c),
            Err(Error::UnexpectedClosingBrace { index: 24 })
        );
    }

    // This is almost enough to make me only expose a dyn fmt::Writer.
    #[test]
    #[cfg(feature = "alloc")]
    fn do_not_do_this_at_home_kids() {
        // Whatever possessed me!?
        let s = "Don’t{␡}{}{^H} do this at home, {who}!".fill_to_string(
            |output: &mut String, key: &str| {
                match key {
                    "" | "" | "^H" => {
                        output.pop();
                    }
                    "who" => {
                        output.push_str("kids");
                    }
                    _ => return Err(()),
                }
                Ok(())
            },
        );
        assert_eq!(s.unwrap(), "Do do this at home, kids!");

        // I haven’t yet decided whether this is better or worse than the previous one.
        let s = "Don’t yell at {who}!{←make ASCII uppercase} (Please.)".fill_to_string(
            |output: &mut String, key: &str| {
                match key {
                    "←make ASCII uppercase" => {
                        output.make_ascii_uppercase();
                    }
                    "who" => {
                        output.push_str("me");
                    }
                    _ => return Err(()),
                }
                Ok(())
            },
        );
        assert_eq!(s.unwrap(), "DON’T YELL AT ME! (Please.)");
    }
}