human-string-filler version 1.0.0
[human-string-filler] / src / 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
173 use core::fmt;
174 use core::iter::FusedIterator;
175 use core::ops::Range;
176
177 #[cfg(feature = "alloc")]
178 extern crate alloc;
179
180 #[cfg(feature = "alloc")]
181 use alloc::string::String;
182
183 #[cfg(feature = "alloc")]
184 use alloc::collections::BTreeMap;
185 #[cfg(feature = "alloc")]
186 use core::borrow::Borrow;
187 #[cfg(feature = "std")]
188 use std::collections::HashMap;
189 #[cfg(feature = "std")]
190 use 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)]
197 pub 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
283 impl<'a, E> From<fmt::Error> for Error<'a, E> {
284 fn from(e: fmt::Error) -> Self {
285 Error::WriteFailed(e)
286 }
287 }
288
289 impl<'a, E> fmt::Display for Error<'a, E>
290 where
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")))]
322 impl<'a, E> std::error::Error for Error<'a, E>
323 where
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.
343 pub trait Filler<W, E>
344 where
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
351 impl<F, W, E> Filler<W, E> for F
352 where
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)]
368 pub 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
375 impl From<fmt::Error> for SimpleFillerError {
376 fn from(e: fmt::Error) -> Self {
377 SimpleFillerError::WriteFailed(e)
378 }
379 }
380
381 impl 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")))]
392 impl 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")))]
403 impl<K, V, W> Filler<W, SimpleFillerError> for &HashMap<K, V>
404 where
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")))]
418 impl<K, V, W> Filler<W, SimpleFillerError> for &BTreeMap<K, V>
419 where
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)`.
436 pub 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
495 impl 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)`.
514 pub fn fill<'a, F, W, E>(
515 mut template: &'a str,
516 mut filler: F,
517 output: &mut W,
518 ) -> Result<(), Error<'a, E>>
519 where
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")))]
585 pub fn fill_to_string<F, E>(template: &str, filler: F) -> Result<String, Error<E>>
586 where
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 /// ```
611 pub 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)]
626 pub 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.
666 pub 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)]
692 mod 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{␡}{\b}{^H} do this at home, {who}!".fill_to_string(
821 |output: &mut String, key: &str| {
822 match key {
823 "␡" | "\b" | "^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 }