dynfmt/
lib.rs

1//! A crate for formatting strings dynamically.
2//!
3//! `dynfmt` provides several implementations for formats that implement a subset of the
4//! [`std::fmt`] facilities. Parsing of the format string and arguments checks are performed at
5//! runtime. There is also the option to implement new formats.
6//!
7//! The public API is exposed via the [`Format`] trait, which contains formatting helper functions
8//! and lower-level utilities to interface with format strings. See the Features section for a list
9//! of provided implementations.
10//!
11//! # Usage
12//!
13//! ```rust
14//! use dynfmt::{Format, NoopFormat};
15//!
16//! let formatted = NoopFormat.format("hello, world", &["unused"]);
17//! assert_eq!("hello, world", formatted.expect("formatting failed"));
18//! ```
19//!
20//! See the [`Format`] trait for more methods.
21//!
22//! # Features
23//!
24//! This crate ships with a set of features that either activate formatting capabilities or new
25//! format implementations:
26//!
27//!  - `json` **(default)**: Implements the serialization of complex structures via JSON. Certain
28//!    formats, such as Python, also have a _representation_ format (`%r`) that makes use of this
29//!    feature, if enabled. Without this feature, such values will cause an error.
30//!  - `python`: Implements the `printf`-like format that python 2 used for formatting strings. See
31//!    [`PythonFormat`] for more information.
32//!  - `curly`: A simple format string syntax using curly braces for arguments. Similar to .NET and
33//!    Rust, but much less capable. See [`SimpleCurlyFormat`] for mor information.
34//!
35//! # Extensibility
36//!
37//! Implement the [`Format`] trait to create a new format. The only required method is `iter_args`,
38//! which must return an iterator over [`ArgumentSpec`] structs. Based on the capabilities of the
39//! format, the specs can be parameterized with formatting parameters.
40//!
41//! ```rust
42//! use std::str::MatchIndices;
43//! use dynfmt::{ArgumentSpec, Format, Error};
44//!
45//! struct HashFormat;
46//!
47//! impl<'f> Format<'f> for HashFormat {
48//!     type Iter = HashIter<'f>;
49//!
50//!     fn iter_args(&self, format: &'f str) -> Result<Self::Iter, Error<'f>> {
51//!         Ok(HashIter(format.match_indices('#')))
52//!     }
53//! }
54//!
55//! struct HashIter<'f>(MatchIndices<'f, char>);
56//!
57//! impl<'f> Iterator for HashIter<'f> {
58//!     type Item = Result<ArgumentSpec<'f>, Error<'f>>;
59//!
60//!     fn next(&mut self) -> Option<Self::Item> {
61//!         self.0.next().map(|(index, _)| Ok(ArgumentSpec::new(index, index + 1)))
62//!     }
63//! }
64//!
65//! let formatted = HashFormat.format("hello, #", &["world"]);
66//! assert_eq!("hello, world", formatted.expect("formatting failed"));
67//! ```
68//!
69//! [`std::fmt`]: https://doc.rust-lang.org/stable/std/fmt/
70//! [`serde::Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html
71//! [`Format`]: trait.Format.html
72//! [`ArgumentSpec`]: struct.ArgumentSpec.html
73//! [`PythonFormat`]: python/struct.PythonFormat.html
74//! [`SimpleCurlyFormat`]: curly/struct.SimpleCurlyFormat.html
75
76#![warn(missing_docs)]
77#![allow(clippy::result_unit_err)]
78
79use std::borrow::Cow;
80use std::fmt;
81use std::io;
82
83use erased_serde::Serialize as Serializable;
84use serde::ser::Serialize;
85use thiserror::Error;
86
87mod formatter;
88
89use crate::formatter::{FormatError, Formatter};
90
91#[cfg(feature = "python")]
92pub mod python;
93#[cfg(feature = "python")]
94pub use crate::python::PythonFormat;
95
96#[cfg(feature = "curly")]
97pub mod curly;
98#[cfg(feature = "curly")]
99pub use crate::curly::SimpleCurlyFormat;
100
101/// Refers to an argument within an argument list.
102///
103/// During formatting, the formatter will pull arguments from the argument list. Depending on
104/// whether the argument list supports indexed or named lookups, this might result in an error. See
105/// [`FormatArgs`] for argument list implementations.
106///
107/// A Position may borrow they key name from the format string.
108///
109/// [`FormatArgs`]: trait.FormatArgs.html
110#[derive(Clone, Copy, Debug, PartialEq, Eq, Ord, PartialOrd, Hash)]
111pub enum Position<'a> {
112    /// The next indexed argument in line.
113    ///
114    /// The formatter maintains an internal state to keep track of the sequence of auto-arguments.
115    /// If an interim access to a specific indexed or named argument occurs, the formatter will
116    /// afterwards continue after the last auto argument.
117    ///
118    /// Requires the argument list to be indexable by numbers.
119    Auto,
120
121    /// Index argument at the given offset.
122    ///
123    /// Requires the argument list to be indexable by numbers.
124    Index(usize),
125
126    /// Named argument with the given key.
127    ///
128    /// Requires the argument list to be indexable by string keys.
129    Key(&'a str),
130}
131
132impl Default for Position<'_> {
133    fn default() -> Self {
134        Position::Auto
135    }
136}
137
138impl fmt::Display for Position<'_> {
139    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
140        match self {
141            Position::Auto => write!(f, "{{next}}"),
142            Position::Index(index) => write!(f, "{}", index),
143            Position::Key(key) => f.write_str(key),
144        }
145    }
146}
147
148/// An error returned during formatting.
149///
150/// An error may borrow information from the format string.
151#[derive(Debug, Error)]
152pub enum Error<'a> {
153    /// An unknown format character has been specified in the format.
154    #[error("unsupported format '{0}'")]
155    BadFormat(char),
156
157    /// A custom error during parsing a specific format.
158    #[error("error parsing format string: {0}")]
159    Parse(Cow<'a, str>),
160
161    /// The format refers to an indexed argument, but the argument list does not support indexed
162    /// access.
163    #[error("format requires an argument list")]
164    ListRequired,
165
166    /// The format refers to a named argument, but the argument list does not support named access.
167    #[error("format requires an argument map")]
168    MapRequired,
169
170    /// An argument was missing from the argument list.
171    #[error("missing argument: {0}")]
172    MissingArg(Position<'a>),
173
174    /// An argument could not be formatted in the requested format.
175    #[error("argument '{0}' cannot be formatted as {1}")]
176    BadArg(Position<'a>, FormatType),
177
178    /// Formatting the data with the requested format resulted in an error.
179    #[error("error formatting argument '{0}': {1}")]
180    BadData(Position<'a>, String),
181
182    /// An I/O error occurred when writing into the target.
183    #[error("{0}")]
184    Io(#[source] io::Error),
185}
186
187impl<'a> Error<'a> {
188    /// Converts the internal formatting error into the public error.
189    fn from_serialize(error: FormatError, position: Position<'a>) -> Self {
190        match error {
191            FormatError::Type(ty) => Error::BadArg(position, ty),
192            FormatError::Serde(err) => Error::BadData(position, err),
193            FormatError::Io(err) => Error::Io(err),
194        }
195    }
196}
197
198/// Formatting types for arguments.
199#[derive(Clone, Copy, Debug, PartialEq, Eq)]
200pub enum FormatType {
201    /// Print the [display] representation of the argument.
202    ///
203    /// [display]: https://doc.rust-lang.org/stable/std/fmt/trait.Display.html
204    Display,
205
206    /// Print the [debug] representation of the argument.
207    ///
208    /// **This is not yet implemented!**
209    ///
210    /// [debug]: https://doc.rust-lang.org/stable/std/fmt/trait.Debug.html
211    Debug,
212
213    /// Print a structured representation of the argument.
214    ///
215    /// This will serialize the argument as JSON. If the `json` feature is turned off, an argument
216    /// like this will result in an error.
217    Object,
218
219    /// Print the [octal] representation of the argument.
220    ///
221    /// [octal]: https://doc.rust-lang.org/stable/std/fmt/trait.Octal.html
222    Octal,
223
224    /// Print the [lower hex] representation of the argument.
225    ///
226    /// [lower hex]: https://doc.rust-lang.org/stable/std/fmt/trait.LowerHex.html
227    LowerHex,
228
229    /// Print the [upper hex] representation of the argument.
230    ///
231    /// [upper hex]: https://doc.rust-lang.org/stable/std/fmt/trait.UpperHex.html
232    UpperHex,
233
234    /// Print the [pointer] representation of the argument.
235    ///
236    /// [pointer]: https://doc.rust-lang.org/stable/std/fmt/trait.Pointer.html
237    Pointer,
238
239    /// Print the [binary] representation of the argument.
240    ///
241    /// [binary]: https://doc.rust-lang.org/stable/std/fmt/trait.Binary.html
242    Binary,
243
244    /// Print the [lower exponential] representation of the argument.
245    ///
246    /// [lower exponential]: https://doc.rust-lang.org/stable/std/fmt/trait.LowerExp.html
247    LowerExp,
248
249    /// Print the [upper exponential] representation of the argument.
250    ///
251    /// [upper exponential]: https://doc.rust-lang.org/stable/std/fmt/trait.UpperExp.html
252    UpperExp,
253
254    /// Print an escaped literal from the format string.
255    Literal(&'static str),
256}
257
258impl FormatType {
259    /// Returns the name of this formatting type.
260    pub fn name(self) -> &'static str {
261        match self {
262            FormatType::Display => "string",
263            FormatType::Debug => "debug",
264            FormatType::Octal => "octal",
265            FormatType::LowerHex => "lower hex",
266            FormatType::UpperHex => "upper hex",
267            FormatType::Pointer => "pointer",
268            FormatType::Binary => "binary",
269            FormatType::LowerExp => "lower exp",
270            FormatType::UpperExp => "upper exp",
271            FormatType::Object => "object",
272            FormatType::Literal(s) => s,
273        }
274    }
275}
276
277impl Default for FormatType {
278    fn default() -> Self {
279        FormatType::Display
280    }
281}
282
283impl fmt::Display for FormatType {
284    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
285        f.write_str(self.name())
286    }
287}
288
289/// A serializable argument.
290pub type Argument<'a> = &'a dyn Serializable;
291
292/// A container that provides access to indexed or named arguments.
293///
294/// Instances of this trait can be used as argument lists to format calls. A container may implement
295/// either `get_index`, `get_key` or even both.
296pub trait FormatArgs {
297    /// Returns the argument with the specified index.
298    ///
299    /// Implement this method if the container supports indexed access. Return `Ok(Some(...))` if
300    /// the argument exists, or `Ok(None)` if the index is out of bounds.
301    #[allow(unused_variables)]
302    fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
303        Err(())
304    }
305
306    /// Returns the argument with the given name.
307    ///
308    /// Implement this method if the container supports named access. Return `Ok(Some(...))` if
309    /// the argument exists, or `Ok(None)` if the index is out of bounds.
310    #[allow(unused_variables)]
311    fn get_key(&self, key: &str) -> Result<Option<Argument<'_>>, ()> {
312        Err(())
313    }
314}
315
316impl<T> FormatArgs for Vec<T>
317where
318    T: Serialize,
319{
320    fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
321        Ok(self.get(index).map(|arg| arg as Argument<'_>))
322    }
323}
324
325impl<T> FormatArgs for &'_ [T]
326where
327    T: Serialize,
328{
329    fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
330        Ok(self.get(index).map(|arg| arg as Argument<'_>))
331    }
332}
333
334macro_rules! impl_args_array {
335    ($num:expr) => {
336        impl<T> FormatArgs for [T; $num]
337        where
338            T: Serialize,
339        {
340            fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
341                Ok(self.get(index).map(|arg| arg as Argument<'_>))
342            }
343        }
344    };
345}
346
347impl_args_array!(0);
348impl_args_array!(1);
349impl_args_array!(2);
350impl_args_array!(3);
351impl_args_array!(4);
352impl_args_array!(5);
353impl_args_array!(6);
354impl_args_array!(7);
355impl_args_array!(8);
356impl_args_array!(9);
357impl_args_array!(10);
358impl_args_array!(11);
359impl_args_array!(12);
360impl_args_array!(13);
361impl_args_array!(14);
362impl_args_array!(15);
363impl_args_array!(16);
364
365impl<T> FormatArgs for std::collections::VecDeque<T>
366where
367    T: Serialize,
368{
369    fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
370        Ok(self.get(index).map(|arg| arg as Argument<'_>))
371    }
372}
373
374impl<S, T> FormatArgs for std::collections::BTreeMap<S, T>
375where
376    S: std::borrow::Borrow<str> + Ord,
377    T: Serialize,
378{
379    fn get_key(&self, key: &str) -> Result<Option<Argument<'_>>, ()> {
380        Ok(self.get(key).map(|arg| arg as Argument<'_>))
381    }
382}
383
384impl<S, T> FormatArgs for std::collections::HashMap<S, T>
385where
386    S: std::borrow::Borrow<str> + std::hash::Hash + Eq,
387    T: Serialize,
388{
389    fn get_key(&self, key: &str) -> Result<Option<Argument<'_>>, ()> {
390        Ok(self.get(key).map(|arg| arg as Argument<'_>))
391    }
392}
393
394impl<A> FormatArgs for &A
395where
396    A: FormatArgs,
397{
398    fn get_index(&self, index: usize) -> Result<Option<Argument<'_>>, ()> {
399        (*self).get_index(index)
400    }
401
402    fn get_key(&self, key: &str) -> Result<Option<Argument<'_>>, ()> {
403        (*self).get_key(key)
404    }
405}
406
407/// Wrapper that provides a formatter with stateful access to arguments.
408struct ArgumentAccess<A> {
409    args: A,
410    index: usize,
411}
412
413impl<A> ArgumentAccess<A>
414where
415    A: FormatArgs,
416{
417    /// Creates a new arguments access.
418    pub fn new(args: A) -> Self {
419        ArgumentAccess { args, index: 0 }
420    }
421
422    /// Returns the argument specified by position.
423    ///
424    /// If the position is [`Position::Auto`], the next indexed argument in line will be pulled and
425    /// the index advanced. Otherwise, the auto index remains and the specified argument is
426    /// retrieved directly.
427    ///
428    /// [`Position::Auto`]: enum.Position.html#variant.Auto
429    pub fn get_pos<'a>(&mut self, mut position: Position<'a>) -> Result<Argument<'_>, Error<'a>> {
430        if position == Position::Auto {
431            position = Position::Index(self.index);
432            self.index += 1;
433        }
434
435        let result = match position {
436            Position::Auto => unreachable!("Position::Auto matched twice"),
437            Position::Index(index) => self.args.get_index(index).map_err(|()| Error::ListRequired),
438            Position::Key(key) => self.args.get_key(key).map_err(|()| Error::MapRequired),
439        };
440
441        result.and_then(|opt| opt.ok_or(Error::MissingArg(position)))
442    }
443}
444
445/// Specifies the alignment of an argument when formatted with a specific width.
446///
447/// Defaults to `Alignment::Right`.
448#[allow(missing_docs)]
449#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
450pub enum Alignment {
451    Left,
452    Center,
453    Right,
454}
455
456impl Default for Alignment {
457    fn default() -> Self {
458        Alignment::Right
459    }
460}
461
462/// The value of a formatting parameter, used within [`ArgumentSpec`].
463///
464/// [`ArgumentSpec`]: struct.ArgumentSpec.html
465#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
466pub enum Count<'a> {
467    /// The literal value as specified in the format string.
468    Value(usize),
469    /// Reference to an argument in the argument list.
470    Ref(Position<'a>),
471}
472
473/// The format specification for a single argument in the format string, created by
474/// [`Format::iter_args`].
475///
476/// The argument spec may borrow data from the format string, e.g. when referring to named arguments
477/// in the argument list.
478///
479/// [`Format::iter_args`]: trait.Format.html#tymethod.iter_args
480#[derive(Debug)]
481pub struct ArgumentSpec<'a> {
482    range: (usize, usize),
483    position: Position<'a>,
484    format: FormatType,
485    alternate: bool,
486    add_sign: bool,
487    pad_zero: bool,
488    fill_char: char,
489    alignment: Alignment,
490    width: Option<Count<'a>>,
491    precision: Option<Count<'a>>,
492}
493
494impl<'a> ArgumentSpec<'a> {
495    /// Creates a new argument specification with default values.
496    ///
497    /// The `start` and `end` parameters denote the exclusive range of this specification in the
498    /// format string. E.g. for a format string `"{}"`, the range is _[0, 2)_.
499    pub fn new(start: usize, end: usize) -> Self {
500        ArgumentSpec {
501            range: (start, end),
502            position: Position::default(),
503            format: FormatType::default(),
504            alternate: false,
505            add_sign: false,
506            pad_zero: false,
507            fill_char: ' ',
508            alignment: Alignment::default(),
509            width: None,
510            precision: None,
511        }
512    }
513
514    /// Sets the argument position. Defaults to [`Position::Auto`].
515    ///
516    /// [`Position::Auto`]: enum.Position.html#variant.Auto
517    pub fn with_position(mut self, position: Position<'a>) -> Self {
518        self.position = position;
519        self
520    }
521
522    /// Sets the formatting type. Defaults to [`FormatType::Display`].
523    ///
524    /// [`FormatType::Display`]: enum.FormatType.html#variant.Display
525    pub fn with_format(mut self, format: FormatType) -> Self {
526        self.format = format;
527        self
528    }
529
530    /// Switch the formatter to [alternate] mode.
531    ///
532    /// [alternate]: https://doc.rust-lang.org/stable/std/fmt/struct.Formatter.html#method.alternate
533    pub fn with_alternate(mut self, alternate: bool) -> Self {
534        self.alternate = alternate;
535        self
536    }
537
538    /// Always print a sign characters in front of numbers.
539    pub fn with_sign(mut self, sign: bool) -> Self {
540        self.add_sign = sign;
541        self
542    }
543
544    /// Activate sign-aware zero padding.
545    pub fn with_zeros(mut self, pad_zero: bool) -> Self {
546        self.pad_zero = pad_zero;
547        self
548    }
549
550    /// Set the fill character. Defaults to `' '` (a space).
551    pub fn with_fill(mut self, fill_char: char) -> Self {
552        self.fill_char = fill_char;
553        self
554    }
555
556    /// Set alignment within the width of this format. Defaults to `Alignment::Right`.
557    ///
558    /// [`Alignment::Right`]: enum.Alignment.html#variant.Right
559    pub fn with_alignment(mut self, alignment: Alignment) -> Self {
560        self.alignment = alignment;
561        self
562    }
563
564    /// Set a minimum width for this argument. Defaults to `None`.
565    ///
566    /// If the formatted argument is smaller than the threshold, the argument is padded with the
567    /// fill character. If the argument is numeric and `with_zeros` is specified, it is padded with
568    /// zeros instead.
569    pub fn with_width(mut self, width: Option<Count<'a>>) -> Self {
570        self.width = width;
571        self
572    }
573
574    /// Set the precision for floating point values. Defaults to arbitrary precision.
575    pub fn with_precision(mut self, precision: Option<Count<'a>>) -> Self {
576        self.precision = precision;
577        self
578    }
579
580    /// The start index of this specification in the format string.
581    pub fn start(&self) -> usize {
582        self.range.0
583    }
584
585    /// The end index of this specification in the format string.
586    pub fn end(&self) -> usize {
587        self.range.1
588    }
589
590    /// Executes the formatter for this argument and writes the result into the given writer.
591    fn format_into<W, A>(&self, mut write: W, args: &mut ArgumentAccess<A>) -> Result<(), Error<'a>>
592    where
593        W: io::Write,
594        A: FormatArgs,
595    {
596        if let FormatType::Literal(literal) = self.format {
597            return write!(write, "{}", literal).map_err(Error::Io);
598        }
599
600        Formatter::new(write)
601            .with_type(self.format)
602            .with_alternate(self.alternate)
603            .format(args.get_pos(self.position)?)
604            .map_err(|e| Error::from_serialize(e, self.position))
605    }
606}
607
608/// The result returned for each element of [`Format::iter_args`].
609///
610/// [`Format::iter_args`]: trait.Format.html#tymethod.iter_args
611pub type ArgumentResult<'f> = Result<ArgumentSpec<'f>, Error<'f>>;
612
613/// A format for string formatting.
614///
615/// This trait exposes formatting helper functions and lower-level utilities to interface with
616/// format strings.
617///
618/// In its core, a format can parse a format string and return an iterator over [`ArgumentSpecs`].
619/// Each specification refers to arguments in and [argument list] and contains information on how
620/// to format it.
621///
622/// [`ArgumentSpecs`]: struct.ArgumentSpec.html
623pub trait Format<'f> {
624    /// The iterator returned by [`iter_args`].
625    ///
626    /// [`iter_args`]: trait.Format.html#tymethod.iter_args
627    type Iter: Iterator<Item = ArgumentResult<'f>>;
628
629    /// Returns an iterator over format arguments in the format string.
630    ///
631    /// This method is not meant to be used directly, instead use some of the provided methods to
632    /// format a string.
633    ///
634    /// The iterator and this method are responsible for parsing the format string correctly and
635    /// returning [`ArgumentSpecs`] for each argument in the format string. See the [module level]
636    /// documentation for an example of how to implement this method.
637    ///
638    /// [`ArgumentSpecs`]: struct.ArgumentSpec.html
639    /// [module level]: index.html#extensibility
640    fn iter_args(&self, format: &'f str) -> Result<Self::Iter, Error<'f>>;
641
642    /// Formats the given string with the specified arguments.
643    ///
644    /// Individual arguments must implement [`Debug`] and [`serde::Serialize`]. The arguments
645    /// container must implement the [`FormatArgs`] trait.
646    ///
647    /// ```rust
648    /// use dynfmt::{Format, NoopFormat};
649    ///
650    /// let formatted = NoopFormat.format("hello, world", &["unused"]);
651    /// assert_eq!("hello, world", formatted.expect("formatting failed"));
652    /// ```
653    ///
654    /// [`Debug`]: https://doc.rust-lang.org/stable/std/fmt/trait.Debug.html
655    /// [`serde::Serialize`]: https://docs.rs/serde/latest/serde/trait.Serialize.html
656    /// [`FormatArgs`]: trait.FormatArgs.html
657    fn format<A>(&self, format: &'f str, arguments: A) -> Result<Cow<'f, str>, Error<'f>>
658    where
659        A: FormatArgs,
660    {
661        let mut iter = self.iter_args(format)?.peekable();
662        if iter.peek().is_none() {
663            return Ok(Cow::Borrowed(format));
664        }
665
666        let mut access = ArgumentAccess::new(arguments);
667        let mut buffer = Vec::with_capacity(format.len());
668        let mut last_match = 0;
669
670        for spec in iter {
671            let spec = spec?;
672            buffer.extend(format[last_match..spec.start()].as_bytes());
673            spec.format_into(&mut buffer, &mut access)?;
674            last_match = spec.end();
675        }
676
677        buffer.extend(format[last_match..].as_bytes());
678        Ok(Cow::Owned(unsafe { String::from_utf8_unchecked(buffer) }))
679    }
680}
681
682/// A format implementation that does not format anything.
683#[derive(Debug)]
684pub struct NoopFormat;
685
686impl<'f> Format<'f> for NoopFormat {
687    type Iter = std::iter::Empty<ArgumentResult<'f>>;
688
689    fn iter_args(&self, _format: &'f str) -> Result<Self::Iter, Error<'f>> {
690        Ok(Default::default())
691    }
692}