human_string_filler/
lib.rs

1#![cfg_attr(not(feature = "std"), no_std)]
2#![deny(missing_docs)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4//! A tiny template language for human-friendly string substitutions.
5//!
6//! This crate is intended for situations where you need the user to be able to write simple
7//! templated strings, and conveniently evaluate them. It’s deliberately simple so that there are
8//! no surprises in its performance or functionality, and so that it’s not accidentally tied to
9//! Rust (e.g. you can readily implement it in a JavaScript-powered web app), which would happen
10//! if things like number formatting specifiers were included out of the box—instead, if you want
11//! that sort of thing, you’ll have to implement it yourself (don’t worry, it won’t be hard).
12//!
13//! No logic is provided in this template language, only simple string formatting: `{…}` template
14//! regions get replaced in whatever way you decide, curly braces get escaped by doubling them
15//! (`{{` and `}}`), and *that’s it*.
16//!
17//! ## Sample usage
18//!
19//! The **lowest-level** handling looks like this:
20//!
21//! ```rust
22//! use human_string_filler::{StrExt, SimpleFillerError};
23//!
24//! let mut output = String::new();
25//! "Hello, {name}!".fill_into(&mut output, |output: &mut String, key: &str| {
26//!     match key {
27//!         "name" => output.push_str("world"),
28//!         _ => return Err(SimpleFillerError::NoSuchKey),
29//!     }
30//!     Ok(())
31//! }).unwrap();
32//!
33//! assert_eq!(output, "Hello, world!");
34//! ```
35//!
36//! `template.fill_into(output, filler)` (provided by `StrExt`) can also be spelled
37//! `fill(template, filler, output)` if you prefer a function to a method
38//! (I reckon the method syntax is clearer, but opinions will differ so I provided both).
39//!
40//! The filler function appends to the string directly for efficiency in case of computed values,
41//! and returns `Result<(), E>`; any error will become `Err(Error::BadReplacement { error, .. })`
42//! on the fill call. (In this example I’ve used `SimpleFillerError::NoSuchKey`, but `()` would
43//! work almost as well, or you can write your own error type altogether.)
44//!
45//! This example showed a closure that took `&mut String` and used `.push_str(…)`, but this crate
46//! is not tied to `String` in any way: for greater generality you would use a function generic
47//! over a type that implements `std::fmt::Write`, and use `.write_str(…)?` inside (`?` works there
48//! because `SimpleFillerError` implements `From<std::fmt::Error>`).
49//!
50//! At a **higher level**, you can use a string-string map as a filler, and you can also fill
51//! directly to a `String` with `.fill_to_string()` (also available as a standalone function
52//! `fill_to_string`):
53//!
54//! ```rust
55//! # #[cfg(feature = "std")] {
56//! use std::collections::HashMap;
57//! use human_string_filler::StrExt;
58//!
59//! let mut map = HashMap::new();
60//! map.insert("name", "world");
61//!
62//! let s = "Hello, {name}!".fill_to_string(&map);
63//!
64//! assert_eq!(s.unwrap(), "Hello, world!");
65//! # }
66//! ```
67//!
68//! Or you can implement the [`Filler`] trait for some other type of your own if you like.
69//!
70//! ## Cargo features
71//!
72#![cfg_attr(
73    feature = "std",
74    doc = " \
75    - **std** (enabled by default, enabled in this build): remove for `#![no_std]` operation. \
76      Implies *alloc*.\
77"
78)]
79#![cfg_attr(
80    not(feature = "std"),
81    doc = " \
82    - **std** (enabled by default, *disabled* in this build): remove for `#![no_std]` operation. \
83      Implies *alloc*.\
84"
85)]
86//!     - Implementation of `std::error::Error` for `Error`;
87//!     - Implementation of `Filler` for `&HashMap`.
88//!
89#![cfg_attr(
90    feature = "alloc",
91    doc = " \
92    - **alloc** (enabled by default via *std*, enabled in this build):\
93"
94)]
95#![cfg_attr(
96    not(feature = "alloc"),
97    doc = " \
98    - **alloc** (enabled by default via *std*, disabled in this build):\
99"
100)]
101//!     - Implementation of `Filler` for `&BTreeMap`.
102//!     - `fill_to_string` and `StrExt::fill_to_string`.
103//!
104//! ## The template language
105//!
106//! This is the grammar of the template language in [ABNF](https://tools.ietf.org/html/rfc5234):
107//!
108//! ```abnf
109//! unescaped-normal-char = %x00-7A / %x7C / %x7E-D7FF / %xE000-10FFFF
110//!                       ; any Unicode scalar value except for "{" and "}"
111//!
112//! normal-char           = unescaped-normal-char / "{{" / "}}"
113//!
114//! template-region       = "{" *unescaped-normal-char "}"
115//!
116//! template-string       = *( normal-char / template-region )
117//! ```
118//!
119//! This regular expression will validate a template string:
120//!
121//! ```text
122//! ^([^{}]|\{\{|\}\}|\{[^{}]*\})*$
123//! ```
124//!
125//! Sample legal template strings:
126//!
127//! - The empty string
128//! - `Hello, {name}!`: one template region with key "name".
129//! - `Today is {date:short}`: one template region with key "date:short". (Although there’s no
130//!   format specification like with the `format!()` macro, a colon convention is one reasonable
131//!   option—see the next section.)
132//! - `Hello, {}!`: one template region with an empty key, not recommended but allowed.
133//! - `Escaped {{ braces {and replacements} for {fun}!`: string "Escaped { braces ", followed by a
134//!   template region with key "and replacements", followed by string " for ", followed by a
135//!   template region with key "fun", followed by string "!".
136//!
137//! Sample illegal template strings:
138//!
139//! - `hello, {world}foo}`: opening and closing curlies must match; any others (specifically, the
140//!   last character of the string) must be escaped by doubling.
141//! - `{{thing}`: the `{{` is an escaped opening curly, so the `}` is unmatched.
142//! - `{thi{{n}}g}`: no curlies of any form inside template region keys. (It’s possible that a
143//!   future version may make it possible to escape curlies inside template regions, if it proves
144//!   to be useful in something like format specifiers; but not at this time.)
145//!
146//! ## Conventions on key semantics
147//!
148//! The key is an arbitrary string (except that it can’t contain `{` or `}`) with explicitly no
149//! defined semantics, but here are some suggestions, including helper functions:
150//!
151//! 1. If it makes sense to have a format specifier (e.g. to specify a date format to use, or
152//!    whether to pad numbers with leading zeroes, *&c.*), split once on a character like `:`.
153//!    To do this most conveniently, a function [`split_on`] is provided.
154//!
155//! 2. For more advanced formatting where you have multiple properties you could wish to set,
156//!    [`split_propertied`] offers some sound and similarly simple semantics for such strings as
157//!    `{key prop1 prop2=val2}` and `{key:prop1,prop2=val2}`.
158//!
159//! 3. If it makes sense to have nested property access, split on `.` with the `key.split('.')`
160//!    iterator. (If you’re using `split_on` or `split_propertied` as mentioned above, you
161//!    probably want to apply them first to separate out the key part.)
162//!
163//! 4. Only use [UAX #31 identifiers](https://www.unicode.org/reports/tr31/) for the key
164//!    (or keys, if supporting nested property access). Most of the time, empty strings and
165//!    numbers are probably not a good idea.
166//!
167//! With these suggestions, you might end up with the key `foo.bar:baz` being interpreted as
168//! retrieving the “bar” property from the “foo” object, and formatting it according to “baz”; or
169//! `aleph.beth.gimmel|alpha beta=5` as retrieving “gimmel” from “beth” of “aleph”, and formatting
170//! it with properties “alpha” set to true and “beta” set to 5. What those things actually *mean*
171//! is up to you to decide. *I* certainly haven’t a clue.
172
173use core::fmt;
174use core::iter::FusedIterator;
175use core::ops::Range;
176
177#[cfg(feature = "alloc")]
178extern crate alloc;
179
180#[cfg(feature = "alloc")]
181use alloc::string::String;
182
183#[cfg(feature = "alloc")]
184use alloc::collections::BTreeMap;
185#[cfg(feature = "alloc")]
186use core::borrow::Borrow;
187#[cfg(feature = "std")]
188use std::collections::HashMap;
189#[cfg(feature = "std")]
190use std::hash::Hash;
191
192/// Any error that occurs when filling a template string.
193///
194/// Template parsing and filling is all done in a single pass; so a failed replacement due to an
195/// unknown key will shadow a syntax error later in the string.
196#[derive(Debug, PartialEq, Eq)]
197pub enum Error<'a, E> {
198    /// A template region was not closed.
199    /// That is, an opening curly brace (`{`) with no matching closing curly brace (`}`).
200    ///
201    /// Example:
202    ///
203    /// ```rust
204    /// # #[cfg(feature = "alloc")] {
205    /// # use human_string_filler::{StrExt, Error};
206    /// # assert_eq!(
207    /// "Hello, {thing"
208    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
209    /// # Err(Error::UnclosedRegion { source: "{thing", range: 7..13 }),
210    /// # );
211    /// # }
212    /// ```
213    UnclosedRegion {
214        /// The text of the unclosed region, which will start with `{` and contain no other curly
215        /// braces.
216        source: &'a str,
217        /// The indexes of `source` within the template string.
218        range: Range<usize>,
219    },
220
221    /// An unescaped closing curly brace (`}`) was found, outside a template region.
222    ///
223    /// Examples:
224    ///
225    /// ```rust
226    /// # #[cfg(feature = "alloc")] {
227    /// # use human_string_filler::{StrExt, Error};
228    /// # assert_eq!(
229    /// "Hello, thing}!"
230    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
231    /// # Err(Error::UnexpectedClosingBrace { index: 12 }),
232    /// # );
233    /// # assert_eq!(
234    /// "Hello, {name}, look at my magnificent moustache: (}-:"
235    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
236    /// # Err(Error::UnexpectedClosingBrace { index: 50 }),
237    /// # );
238    /// # assert_eq!(
239    /// "Hello, {name}}!"
240    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
241    /// # Err(Error::UnexpectedClosingBrace { index: 13 }),
242    /// # );
243    /// # }
244    /// ```
245    UnexpectedClosingBrace {
246        /// The index of the closing brace within the template string.
247        index: usize,
248    },
249
250    /// An opening curly brace (`{`) was found within a template region.
251    ///
252    /// Examples:
253    ///
254    /// ```rust
255    /// # #[cfg(feature = "alloc")] {
256    /// # use human_string_filler::{StrExt, Error};
257    /// # assert_eq!(
258    /// "Hello, {thing{{sadness}}}"
259    /// # .fill_to_string(|_: &mut String, _: &str| Result::<(), ()>::Ok(())),
260    /// # Err(Error::UnexpectedOpeningBrace { index: 13 }),
261    /// # );
262    /// # }
263    /// ```
264    UnexpectedOpeningBrace {
265        /// The index of the opening brace within the template string.
266        index: usize,
267    },
268
269    /// The filler returned an error for the specified key.
270    BadReplacement {
271        /// The key on which the filler failed. Curly braces not included.
272        key: &'a str,
273        /// The indexes of `key` within the template string.
274        range: Range<usize>,
275        /// The error value returned by the filler.
276        error: E,
277    },
278
279    /// Writing to the output failed.
280    WriteFailed(fmt::Error),
281}
282
283impl<'a, E> From<fmt::Error> for Error<'a, E> {
284    fn from(e: fmt::Error) -> Self {
285        Error::WriteFailed(e)
286    }
287}
288
289impl<'a, E> fmt::Display for Error<'a, E>
290where
291    E: fmt::Display,
292{
293    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
294        match self {
295            Error::UnclosedRegion { source, .. } => {
296                write!(f, "Unclosed template region at \"{}\"", source)
297            }
298
299            Error::UnexpectedClosingBrace { index } => {
300                write!(f, "Unexpected closing brace at index {}", index)
301            }
302
303            Error::UnexpectedOpeningBrace { index } => {
304                write!(
305                    f,
306                    "Unexpected curly brace within template region at index {}",
307                    index
308                )
309            }
310
311            Error::BadReplacement { key, error, .. } => {
312                write!(f, "Error in template string at \"{{{}}}\": {}", key, error)
313            }
314
315            Error::WriteFailed(fmt::Error) => f.write_str("Error in writing output"),
316        }
317    }
318}
319
320#[cfg(feature = "std")]
321#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
322impl<'a, E> std::error::Error for Error<'a, E>
323where
324    E: std::error::Error + 'static,
325{
326    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
327        match self {
328            Error::BadReplacement { error, .. } => Some(error),
329            Error::WriteFailed(error) => Some(error),
330            _ => None,
331        }
332    }
333}
334
335/// Implementers of this trait have the ability to fill template strings.
336///
337/// It is extremely strongly recommended that fillers only push to the output, and do not perform
338/// any other modifications of it.
339///
340/// I mean, if you implement `Filler<String, _>`, you get a `&mut String` and it’s *possible* to do
341/// other things with it, but that’s a terrible idea. I’m almost ashamed of ideas like making `{␡}`
342/// pop the last character, and `{←rot13}` ROT-13-encode what precedes it in the string.
343pub trait Filler<W, E>
344where
345    W: fmt::Write,
346{
347    /// Fill the value for the given key into the output string.
348    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E>;
349}
350
351impl<F, W, E> Filler<W, E> for F
352where
353    F: FnMut(&mut W, &str) -> Result<(), E>,
354    W: fmt::Write,
355{
356    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), E> {
357        self(output, key)
358    }
359}
360
361#[cfg_attr(not(feature = "std"), allow(rustdoc::broken_intra_doc_links))]
362/// A convenient error type for fillers; you might even like to use it yourself.
363///
364/// You could also use `()`, but this gives you
365/// <code>[From](core::convert::From)&lt;[core::fmt::Error]></code> so that you can use
366/// `write!(out, …)?`, and sane [`core::fmt::Display`] and [`std::error::Error`] implementations.
367#[derive(Clone, Debug, PartialEq, Eq)]
368pub enum SimpleFillerError {
369    /// The map didn’t contain the requested key.
370    NoSuchKey,
371    /// Some fmt::Write operation returned an error.
372    WriteFailed(fmt::Error),
373}
374
375impl From<fmt::Error> for SimpleFillerError {
376    fn from(e: fmt::Error) -> Self {
377        SimpleFillerError::WriteFailed(e)
378    }
379}
380
381impl fmt::Display for SimpleFillerError {
382    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
383        match self {
384            SimpleFillerError::NoSuchKey => f.write_str("no such key"),
385            SimpleFillerError::WriteFailed(fmt::Error) => f.write_str("write failed"),
386        }
387    }
388}
389
390#[cfg(feature = "std")]
391#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
392impl std::error::Error for SimpleFillerError {
393    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
394        match self {
395            SimpleFillerError::WriteFailed(error) => Some(error),
396            _ => None,
397        }
398    }
399}
400
401#[cfg(feature = "std")]
402#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
403impl<K, V, W> Filler<W, SimpleFillerError> for &HashMap<K, V>
404where
405    K: Borrow<str> + Eq + Hash,
406    V: AsRef<str>,
407    W: fmt::Write,
408{
409    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
410        self.get(key)
411            .ok_or(SimpleFillerError::NoSuchKey)
412            .and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
413    }
414}
415
416#[cfg(feature = "alloc")]
417#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
418impl<K, V, W> Filler<W, SimpleFillerError> for &BTreeMap<K, V>
419where
420    K: Borrow<str> + Ord,
421    V: AsRef<str>,
422    W: fmt::Write,
423{
424    fn fill(&mut self, output: &mut W, key: &str) -> Result<(), SimpleFillerError> {
425        self.get(key)
426            .ok_or(SimpleFillerError::NoSuchKey)
427            .and_then(|value| output.write_str(value.as_ref()).map_err(Into::into))
428    }
429}
430
431/// String extension methods for the template string.
432///
433/// This is generally how I recommend using this library, because I find that the method receiver
434/// makes code clearer: that `template.fill_into(output, filler)` is easier to understand than
435/// `fill(template, filler, output)`.
436pub trait StrExt {
437    /// Fill this template, producing a new string.
438    ///
439    /// This is a convenience method for ergonomics in the case where you aren’t fussed about
440    /// allocations and are using the standard `String` type.
441    ///
442    #[cfg_attr(feature = "std", doc = " Example, using a hash map:")]
443    #[cfg_attr(
444        not(feature = "std"),
445        doc = " Example, using a hash map (requires the *std* feature):"
446    )]
447    ///
448    /// ```rust
449    /// # #[cfg(feature = "std")] {
450    /// # use human_string_filler::StrExt;
451    /// # use std::collections::HashMap;
452    /// let map = [("name", "world")].into_iter().collect::<HashMap<_, _>>();
453    /// assert_eq!(
454    ///     "Hello, {name}!".fill_to_string(&map).unwrap(),
455    ///     "Hello, world!",
456    /// );
457    /// # }
458    /// ```
459    #[cfg(feature = "alloc")]
460    #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
461    fn fill_to_string<F, E>(&self, filler: F) -> Result<String, Error<E>>
462    where
463        F: Filler<String, E>,
464    {
465        let mut out = String::new();
466        self.fill_into(&mut out, filler).map(|()| out)
467    }
468
469    /// Fill this template string into the provided string, with the provided filler.
470    ///
471    /// Uses an existing string, which is more efficient if you want to push to an existing string
472    /// or can reuse a string allocation.
473    ///
474    /// Example, using a closure:
475    ///
476    /// ```rust
477    /// # use human_string_filler::StrExt;
478    /// let filler = |output: &mut String, key: &str| {
479    ///     match key {
480    ///         "name" => output.push_str("world"),
481    ///         _ => return Err(()),
482    ///     }
483    ///     Ok(())
484    /// };
485    /// let mut string = String::new();
486    /// assert!("Hello, {name}!".fill_into(&mut string, filler).is_ok());
487    /// assert_eq!(string, "Hello, world!");
488    /// ```
489    fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
490    where
491        F: Filler<W, E>,
492        W: fmt::Write;
493}
494
495impl StrExt for str {
496    #[inline]
497    fn fill_into<F, W, E>(&self, output: &mut W, filler: F) -> Result<(), Error<E>>
498    where
499        F: Filler<W, E>,
500        W: fmt::Write,
501    {
502        fill(self, filler, output)
503    }
504}
505
506/// The lowest-level form, as a function: fill the template string, into a provided writer.
507///
508/// This is the most efficient form. It splits a string by `{…}` sections, adding anything outside
509/// them to the output string (with escaped curlies dedoubled) and passing template regions through
510/// the filler, which handles pushing to the output string itself.
511///
512/// See also [`StrExt::fill_into`] which respells `fill(template, filler, output)` as
513/// `template.fill_into(output, filler)`.
514pub fn fill<'a, F, W, E>(
515    mut template: &'a str,
516    mut filler: F,
517    output: &mut W,
518) -> Result<(), Error<'a, E>>
519where
520    F: Filler<W, E>,
521    W: fmt::Write,
522{
523    let mut index = 0;
524    loop {
525        if let Some(i) = template.find(|c| c == '{' || c == '}') {
526            #[allow(clippy::wildcard_in_or_patterns)]
527            match template.as_bytes()[i] {
528                c @ b'}' | c @ b'{' if template.as_bytes().get(i + 1) == Some(&c) => {
529                    output.write_str(&template[0..i + 1])?;
530                    template = &template[i + 2..];
531                    index += i + 2;
532                }
533                b'}' => return Err(Error::UnexpectedClosingBrace { index: index + i }),
534                b'{' | _ => {
535                    // (_ here just to lazily skip an unreachable!().)
536                    output.write_str(&template[0..i])?;
537                    template = &template[i..];
538                    index += i;
539                    if let Some(i) = template[1..].find(|c| c == '{' || c == '}') {
540                        match template.as_bytes()[i + 1] {
541                            b'}' => {
542                                if let Err(e) = filler.fill(output, &template[1..i + 1]) {
543                                    return Err(Error::BadReplacement {
544                                        key: &template[1..i + 1],
545                                        range: (index + 1)..(index + i + 1),
546                                        error: e,
547                                    });
548                                }
549                                template = &template[i + 2..];
550                                index += i + 2;
551                            }
552                            // (Again, _ is unreachable.)
553                            b'{' | _ => {
554                                return Err(Error::UnexpectedOpeningBrace {
555                                    index: index + i + 1,
556                                })
557                            }
558                        }
559                    } else {
560                        return Err(Error::UnclosedRegion {
561                            source: template,
562                            range: index..(index + template.len()),
563                        });
564                    }
565                }
566            }
567        } else {
568            output.write_str(template)?;
569            break;
570        }
571    }
572
573    Ok(())
574}
575
576/// Fill a template, producing a new string.
577///
578/// This is a convenience function for ergonomics in the case where you aren’t fussed about
579/// allocations and are using the standard `String` type.
580///
581/// See also [`StrExt::fill_to_string`], which respells `fill_to_string(template, filler)` as
582/// `template.fill_to_string(filler)`.
583#[cfg(feature = "alloc")]
584#[cfg_attr(docsrs, doc(cfg(feature = "alloc")))]
585pub fn fill_to_string<F, E>(template: &str, filler: F) -> Result<String, Error<E>>
586where
587    F: Filler<String, E>,
588{
589    let mut out = String::new();
590    fill(template, filler, &mut out).map(|()| out)
591}
592
593/// A convenience function to split a string on a character.
594///
595/// This is nicer than using `string.split(c, 2)` because it gives you the two values up-front.
596///
597/// # Returns
598///
599/// A two-tuple of:
600///
601/// 1. What comes before the split character, or the entire string if there was none; and
602/// 2. The remainder after the split character, if there was one (even if it’s empty).
603///
604/// ```
605/// # use human_string_filler::split_on;
606/// assert_eq!(split_on("The quick brown fox", ':'), ("The quick brown fox", None));
607/// assert_eq!(split_on("/", '/'), ("", Some("")));
608/// assert_eq!(split_on("harum = scarum", '='), ("harum ", Some(" scarum")));
609/// assert_eq!(split_on("diæresis:tréma:umlaut", ':'), ("diæresis", Some("tréma:umlaut")));
610/// ```
611pub fn split_on(string: &str, c: char) -> (&str, Option<&str>) {
612    match string.find(c) {
613        Some(i) => (&string[..i], Some(&string[i + c.len_utf8()..])),
614        None => (string, None),
615    }
616}
617
618/// The separators to use in [`split_propertied`].
619///
620/// A couple of sets of plausible-looking values (but if you want a concrete recommendation, like
621/// Gallio of old I refuse to be a judge of these things):
622///
623/// - `(' ', ' ', '=')` looks like `Hello, {name first formal=false case=lower}!`.
624/// - `('|', ',', ':')` looks like `Hello, {name|first,formal:false,case:lower}!`.
625#[derive(Clone, Copy, Debug)]
626pub struct Separators {
627    /// What character indicates the end of the key and the start of the properties.
628    pub between_key_and_properties: char,
629
630    /// What character indicates the end of one property’s name or value and the start of the next
631    /// property’s name.
632    pub between_properties: char,
633
634    /// What character indicates the end of a property’s name and the start of its value.
635    /// Remember that properties aren’t required to have values, but can be booleanyish.
636    // “booleanyish” sounded better than “booleanishy”. That’s my story and I’m sticking with it.
637    /// For that matter, if you want *all* properties to be boolean, set this to the same value as
638    /// `between_properties`, because `between_properties` is greedier.
639    pub between_property_name_and_value: char,
640}
641
642/// A convenience function to split a key that is followed by properties.
643///
644/// In keeping with this library in general, this is deliberately very simple and consequently not
645/// able to express all possible values; for example, if you use space as the separator between
646/// properties, you can’t use space in property values; and this doesn’t guard against empty keys
647/// or property names in any way.
648///
649/// ```
650/// use human_string_filler::{Separators, split_propertied};
651///
652/// let (key, properties) = split_propertied("key:prop1,prop2=value2,prop3=4+5=9", Separators {
653///     between_key_and_properties: ':',
654///     between_properties: ',',
655///     between_property_name_and_value: '=',
656/// });
657///
658/// assert_eq!(key, "key");
659/// assert_eq!(properties.collect::<Vec<_>>(),
660///            vec![("prop1", None), ("prop2", Some("value2")), ("prop3", Some("4+5=9"))]);
661/// ```
662///
663/// This method consumes exactly one character for the separators; if space is your
664/// between-properties separator, for example, multiple spaces will not be combined, but
665/// you’ll get `("", None)` properties instead. As I say, this is deliberately simple.
666pub fn split_propertied(
667    s: &str,
668    separators: Separators,
669) -> (
670    &str,
671    impl Iterator<Item = (&str, Option<&str>)>
672        + DoubleEndedIterator
673        + FusedIterator
674        + Clone
675        + fmt::Debug,
676) {
677    let (key, properties) = split_on(s, separators.between_key_and_properties);
678    let properties = properties
679        .map(|properties| properties.split(separators.between_properties))
680        .unwrap_or_else(|| {
681            // We need an iterator of the same type that will yield None, but Split yields an empty
682            // string first. Nice and easy: consume that, then continue on our way.
683            let mut dummy = "".split(' ');
684            dummy.next();
685            dummy
686        })
687        .map(move |word| split_on(word, separators.between_property_name_and_value));
688    (key, properties)
689}
690
691#[cfg(test)]
692mod tests {
693    #[allow(unused_imports)]
694    use super::*;
695
696    #[cfg(feature = "alloc")]
697    macro_rules! test {
698        ($name:ident, $filler:expr) => {
699            #[test]
700            fn $name() {
701                let filler = $filler;
702
703                assert_eq!(
704                    "Hello, {}!".fill_to_string(&filler).as_ref().map(|s| &**s),
705                    Ok("Hello, (this space intentionally left blank)!"),
706                );
707                assert_eq!(
708                    "Hello, {name}!"
709                        .fill_to_string(&filler)
710                        .as_ref()
711                        .map(|s| &**s),
712                    Ok("Hello, world!"),
713                );
714                assert_eq!(
715                    "Hello, {you}!".fill_to_string(&filler),
716                    Err(Error::BadReplacement {
717                        key: "you",
718                        range: 8..11,
719                        error: SimpleFillerError::NoSuchKey,
720                    }),
721                );
722                assert_eq!(
723                    "I like {keys with SPACES!? 😱}"
724                        .fill_to_string(&filler)
725                        .as_ref()
726                        .map(|s| &**s),
727                    Ok("I like identifier-only keys 👌"),
728                );
729            }
730        };
731    }
732
733    #[cfg(feature = "alloc")]
734    test!(closure_filler, |out: &mut String, key: &str| {
735        use core::fmt::Write;
736        out.write_str(match key {
737            "" => "(this space intentionally left blank)",
738            "name" => "world",
739            "keys with SPACES!? 😱" => "identifier-only keys 👌",
740            _ => return Err(SimpleFillerError::NoSuchKey),
741        })
742        .map_err(Into::into)
743    });
744
745    #[cfg(feature = "std")]
746    test!(hash_map_fillter, {
747        [
748            ("", "(this space intentionally left blank)"),
749            ("name", "world"),
750            ("keys with SPACES!? 😱", "identifier-only keys 👌"),
751        ]
752        .into_iter()
753        .collect::<HashMap<_, _>>()
754    });
755
756    #[cfg(feature = "alloc")]
757    test!(btree_map_fillter, {
758        [
759            ("", "(this space intentionally left blank)"),
760            ("name", "world"),
761            ("keys with SPACES!? 😱", "identifier-only keys 👌"),
762        ]
763        .into_iter()
764        .collect::<BTreeMap<_, _>>()
765    });
766
767    #[test]
768    #[cfg(feature = "alloc")]
769    fn fill_errors() {
770        let c = |_: &mut String, _: &str| -> Result<(), ()> { Ok(()) };
771
772        assert_eq!(
773            fill_to_string("Hello, {thing", c),
774            Err(Error::UnclosedRegion {
775                source: "{thing",
776                range: 7..13
777            })
778        );
779        assert_eq!(
780            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{thing", c),
781            Err(Error::UnclosedRegion {
782                source: "{thing",
783                range: 24..30
784            })
785        );
786
787        assert_eq!(
788            fill_to_string("Hello, }thing", c),
789            Err(Error::UnexpectedClosingBrace { index: 7 })
790        );
791        assert_eq!(
792            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}thing", c),
793            Err(Error::UnexpectedClosingBrace { index: 24 })
794        );
795
796        assert_eq!(
797            fill_to_string("Hello, {thi{{ng}", c),
798            Err(Error::UnexpectedOpeningBrace { index: 11 })
799        );
800        assert_eq!(
801            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/{x{", c),
802            Err(Error::UnexpectedOpeningBrace { index: 26 })
803        );
804
805        assert_eq!(
806            fill_to_string("Hello, {thi}}ng}", c),
807            Err(Error::UnexpectedClosingBrace { index: 12 })
808        );
809        assert_eq!(
810            fill_to_string("{}/{x}/{xx}/{xxx}/{{/}}/}", c),
811            Err(Error::UnexpectedClosingBrace { index: 24 })
812        );
813    }
814
815    // This is almost enough to make me only expose a dyn fmt::Writer.
816    #[test]
817    #[cfg(feature = "alloc")]
818    fn do_not_do_this_at_home_kids() {
819        // Whatever possessed me!?
820        let s = "Don’t{␡}{}{^H} do this at home, {who}!".fill_to_string(
821            |output: &mut String, key: &str| {
822                match key {
823                    "␡" | "" | "^H" => {
824                        output.pop();
825                    }
826                    "who" => {
827                        output.push_str("kids");
828                    }
829                    _ => return Err(()),
830                }
831                Ok(())
832            },
833        );
834        assert_eq!(s.unwrap(), "Do do this at home, kids!");
835
836        // I haven’t yet decided whether this is better or worse than the previous one.
837        let s = "Don’t yell at {who}!{←make ASCII uppercase} (Please.)".fill_to_string(
838            |output: &mut String, key: &str| {
839                match key {
840                    "←make ASCII uppercase" => {
841                        output.make_ascii_uppercase();
842                    }
843                    "who" => {
844                        output.push_str("me");
845                    }
846                    _ => return Err(()),
847                }
848                Ok(())
849            },
850        );
851        assert_eq!(s.unwrap(), "DON’T YELL AT ME! (Please.)");
852    }
853}