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}