Skip to content

Commit f375cff

Browse files
authored
Iter formatter (#7)
1 parent 682547e commit f375cff

File tree

7 files changed

+896
-507
lines changed

7 files changed

+896
-507
lines changed

Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ documentation = "https://docs.rs/interpolator"
1212
include = ["src/**/*", "LICENSE-*", "README.md", "CHANGELOG.md"]
1313

1414
[features]
15-
# default = ["debug", "number", "pointer"]
15+
default = ["debug", "number", "pointer", "iter"]
1616
debug = []
1717
number = []
1818
pointer = []
19+
iter = []
1920

2021
[dev-dependencies]
22+
collection_literals = "1.0.1"
2123
derive_more = "0.99.17"
2224
proptest = "1.1.0"
2325
proptest-derive = "0.3.0"

src/error.rs

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
use super::*;
2+
3+
#[derive(Debug, PartialEq, Clone)]
4+
/// Error returned by [`format()`].
5+
pub enum Error {
6+
/// Value was formatted with unimplemented trait.
7+
/// - `.0` the trait
8+
/// - `.1` the byte index of the format argument
9+
MissingTraitImpl(Trait, usize),
10+
/// Error occurred while calling `::fmt`
11+
/// - `.0` the error
12+
/// - `.1` the byte index of the format argument
13+
Fmt(FmtError, usize),
14+
/// Error occurred while parsing format string
15+
Parse(ParseError),
16+
/// Tried to format value that was not in context
17+
/// - `.0` the ident of the value
18+
/// - `.1` the byte index of the format argument
19+
MissingValue(String, usize),
20+
/// Unsupported Option was used.
21+
/// - `.0` is the option
22+
/// - `.1` the feature that needs to be enabled to use it
23+
/// - `.2` the byte index of the format argument
24+
UnsupportedOption(&'static str, &'static str, usize),
25+
}
26+
27+
impl Error {
28+
pub(crate) fn add_idx(self, idx: usize) -> Self {
29+
use Error::*;
30+
match self {
31+
MissingTraitImpl(t, i) => MissingTraitImpl(t, i + idx),
32+
Fmt(e, i) => Fmt(e, i),
33+
Parse(e) => Parse(e.add_idx(idx)),
34+
MissingValue(v, i) => MissingValue(v, i + idx),
35+
UnsupportedOption(o, f, i) => UnsupportedOption(o, f, i + idx),
36+
}
37+
}
38+
}
39+
40+
impl From<ParseError> for Error {
41+
fn from(value: ParseError) -> Self {
42+
Self::Parse(value)
43+
}
44+
}
45+
46+
impl Display for Error {
47+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48+
match self {
49+
Error::MissingTraitImpl(t, idx) => write!(
50+
f,
51+
"trait `{t:?}` not implemented, used by format argument at {idx}"
52+
),
53+
Error::Fmt(e, idx) => write!(f, "error while formatting at {idx}: {e}"),
54+
Error::Parse(e) => write!(f, "error while parsing input: {e}"),
55+
Error::MissingValue(ident, idx) => {
56+
write!(f, "specified value not in context `{ident}` at {idx}")
57+
}
58+
Error::UnsupportedOption(option, feature, idx) => write!(
59+
f,
60+
"option `{option}` is not supported without feature `{feature}` at {idx}"
61+
),
62+
}
63+
}
64+
}
65+
66+
impl StdError for Error {}
67+
68+
#[derive(Debug, PartialEq, Clone)]
69+
#[non_exhaustive]
70+
/// The trait used to format.
71+
pub enum Trait {
72+
/// [`Binary`]
73+
#[cfg(feature = "number")]
74+
Binary,
75+
/// [`Debug`]
76+
#[cfg(feature = "debug")]
77+
Debug,
78+
/// [`Display`]
79+
Display,
80+
/// [`LowerExp`]
81+
#[cfg(feature = "number")]
82+
LowerExp,
83+
/// [`LowerHex`]
84+
#[cfg(feature = "number")]
85+
LowerHex,
86+
/// [`Octal`]
87+
#[cfg(feature = "number")]
88+
Octal,
89+
/// [`Pointer`]
90+
#[cfg(feature = "pointer")]
91+
Pointer,
92+
/// [`UpperExp`]
93+
#[cfg(feature = "number")]
94+
UpperExp,
95+
/// [`UpperHex`]
96+
#[cfg(feature = "number")]
97+
UpperHex,
98+
#[cfg(feature = "iter")]
99+
/// Custom "trait" that allows iterating over lists i.e. `&[Formattable]`
100+
Iter,
101+
}
102+
103+
#[derive(Debug, PartialEq, Clone)]
104+
/// Error caused by invalid format string
105+
pub enum ParseError {
106+
/// Format spec at byte index is nether closed with a `}`
107+
FormatSpecUnclosed(usize),
108+
/// Expected sequence at byte index
109+
Expected(&'static str, usize),
110+
/// Unable to parse specified width as usize
111+
InvalidWidth(ParseIntError, usize),
112+
/// Unable to parse specified precision as usize
113+
InvalidPrecision(ParseIntError, usize),
114+
/// Fill is not supported due to [rust-lang/rfcs#3394](https://github.com/rust-lang/rfcs/pull/3394).
115+
Fill(usize),
116+
/// Width, precision, `-`, `+` and `#` are not supported for `i`
117+
Iter(usize),
118+
/// Unable to parse specified range bound as isize
119+
RangeBound(ParseIntError, usize),
120+
}
121+
122+
impl ParseError {
123+
pub(crate) fn add_idx(self, idx: usize) -> ParseError {
124+
use ParseError::*;
125+
match self {
126+
FormatSpecUnclosed(i) => FormatSpecUnclosed(i + idx),
127+
Expected(c, i) => Expected(c, i + idx),
128+
InvalidWidth(e, i) => InvalidWidth(e, i + idx),
129+
InvalidPrecision(e, i) => InvalidPrecision(e, i + idx),
130+
Fill(i) => Fill(i + idx),
131+
Iter(i) => Iter(i + idx),
132+
RangeBound(e, i) => RangeBound(e, i + idx),
133+
}
134+
}
135+
}
136+
137+
impl Display for ParseError {
138+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139+
match self {
140+
ParseError::FormatSpecUnclosed(idx) => {
141+
write!(f, "Format spec at {idx} is nether closed with a `}}`")
142+
}
143+
ParseError::Expected(c, idx) => write!(f, "Expected `{c}` at {idx}"),
144+
ParseError::InvalidWidth(e, idx) => {
145+
write!(f, "Unable to parse width at {idx} as usize: {e}")
146+
}
147+
ParseError::InvalidPrecision(e, idx) => {
148+
write!(f, "Unable to parse precision at {idx} as usize: {e}")
149+
}
150+
ParseError::Fill(idx) => write!(
151+
f,
152+
"Fill is not supported due to https://github.com/rust-lang/rfcs/pull/3394 at {idx}"
153+
),
154+
ParseError::Iter(idx) => write!(
155+
f,
156+
"Width, precision, `-`, `+` and `#` are not supported for `i` at {idx}"
157+
),
158+
ParseError::RangeBound(e, idx) => {
159+
write!(f, "Unable to parse range bound at {idx} as isize: {e}")
160+
}
161+
}
162+
}
163+
}
164+
165+
impl StdError for ParseError {}
166+
167+
macro_rules! ensure {
168+
($condition:expr, $error:expr) => {
169+
if !$condition {
170+
return Err($error.into());
171+
}
172+
};
173+
}

src/formattable.rs

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
use super::*;
2+
/// Utility struct holding references to the trait implementation of a value to
3+
/// enable runtime verification and execution of them
4+
#[derive(Default)]
5+
#[must_use]
6+
pub struct Formattable<'a> {
7+
#[cfg(any(feature = "debug", feature = "number"))]
8+
debug: Option<&'a dyn Debug>,
9+
display: Option<&'a dyn Display>,
10+
#[cfg(feature = "number")]
11+
binary: Option<&'a dyn Binary>,
12+
#[cfg(feature = "number")]
13+
lower_exp: Option<&'a dyn LowerExp>,
14+
#[cfg(feature = "number")]
15+
lower_hex: Option<&'a dyn LowerHex>,
16+
#[cfg(feature = "number")]
17+
octal: Option<&'a dyn Octal>,
18+
#[cfg(feature = "number")]
19+
upper_exp: Option<&'a dyn UpperExp>,
20+
#[cfg(feature = "number")]
21+
upper_hex: Option<&'a dyn UpperHex>,
22+
#[cfg(feature = "pointer")]
23+
pointer: Option<PointerWrapper<'a>>,
24+
#[cfg(feature = "iter")]
25+
iter: Option<&'a [Formattable<'a>]>,
26+
}
27+
28+
macro_rules! formattable_fn {
29+
(($($cfg:tt)*), ($doc:expr), $name:ident, $builder:ident<$($traits:ident),*> $($fields:ident),+) => {
30+
/// Creates a [`Formattable`] from a value implementing
31+
#[doc = $doc]
32+
$($cfg)* pub fn $name<T: $($traits+)*>(value: &'a T) -> Self {
33+
#[allow(clippy::needless_update)]
34+
Self {
35+
$($fields: Some(value),)*
36+
..Default::default()
37+
}
38+
}
39+
/// Adds implementation for
40+
#[doc = $doc]
41+
$($cfg)* pub fn $builder<T: $($traits+)*>(mut self, value: &'a T) -> Self {
42+
$(self.$fields = Some(value);)*
43+
self
44+
}
45+
};
46+
(($($cfg:tt)*), (), $name:ident, $builder:ident, $getter:ident<$trait:ident>) => {
47+
formattable_fn!(($($cfg)*), (concat!("[`",stringify!($trait),"`]")), $name, $builder<$trait> $name);
48+
$($cfg)* pub(crate) fn $getter(&self) -> Result<&dyn $trait, Trait> {
49+
self.$name.ok_or(Trait::$trait)
50+
}
51+
};
52+
}
53+
macro_rules! formattable {
54+
[$($($cfg:literal, $($doc:literal,)?)? $name:ident, $builder:ident$(, $getter:ident)?<$($traits:ident),*> $($fields:ident),*;)*] => {
55+
impl<'a> Formattable<'a> {
56+
$(formattable_fn!(($(#[cfg(feature=$cfg)])?), ($($($doc)?)?), $name, $builder$(, $getter)?<$($traits),*> $($fields),*);)*
57+
}
58+
};
59+
}
60+
61+
formattable![
62+
"debug", "[`Debug`] and [`Display`]", debug_display, and_debug_display<Debug, Display> debug, display;
63+
"debug", debug, and_debug, get_debug<Debug>;
64+
display, and_display, get_display<Display>;
65+
"number", "[`Debug`], [`Display`], [`Octal`], [`LowerHex`], [`UpperHex`], [`Binary`], [`LowerExp`] and [`UpperExp`]", integer, and_integer<Binary, Debug, Display, LowerExp, LowerHex, Octal, UpperExp, UpperHex>
66+
binary, debug, display, lower_exp, lower_hex, octal, upper_exp, upper_hex;
67+
"number", "[`Debug`], [`Display`], [`LowerExp`] and [`UpperExp`]", float, and_float<Debug, Display, LowerExp, UpperExp>
68+
debug, display, lower_exp, upper_exp;
69+
"number", binary, and_binary, get_binary<Binary>;
70+
"number", lower_exp, and_lower_exp, get_lower_exp<LowerExp>;
71+
"number", lower_hex, and_lower_hex, get_lower_hex<LowerHex>;
72+
"number", octal, and_octal, get_octal<Octal>;
73+
"number", upper_exp, and_upper_exp, get_upper_exp<UpperExp>;
74+
"number", upper_hex, and_upper_hex, get_upper_hex<UpperHex>;
75+
];
76+
77+
#[cfg(feature = "pointer")]
78+
impl<'a> Formattable<'a> {
79+
/// Creates a [`Formattable`] from a value implementing [`Pointer`].
80+
pub fn pointer<T: Pointer>(value: &'a T) -> Self {
81+
Self::default().and_pointer(value)
82+
}
83+
84+
/// Adds implementation for [`Pointer`]
85+
pub fn and_pointer<T: Pointer>(mut self, value: &'a T) -> Self {
86+
self.pointer = Some(PointerWrapper(value));
87+
self
88+
}
89+
90+
pub(crate) fn get_pointer(&self) -> Result<PointerWrapper, Trait> {
91+
self.pointer.ok_or(Trait::Pointer)
92+
}
93+
}
94+
95+
impl<'a> Formattable<'a> {
96+
/// Creates a [`Formattable`] from a list of values
97+
pub fn iter(value: &'a [Formattable<'a>]) -> Self {
98+
Self::default().and_iter(value)
99+
}
100+
101+
/// Adds implementation for mapping operations
102+
pub fn and_iter(mut self, value: &'a [Formattable<'a>]) -> Self {
103+
self.iter = Some(value);
104+
self
105+
}
106+
107+
pub(crate) fn get_iter(&self) -> Result<&'a [Formattable<'a>], Trait> {
108+
self.iter.ok_or(Trait::Iter)
109+
}
110+
}
111+
112+
#[cfg(feature = "pointer")]
113+
#[derive(Clone, Copy)]
114+
pub(crate) struct PointerWrapper<'a>(&'a dyn Pointer);
115+
116+
#[cfg(feature = "pointer")]
117+
impl Pointer for PointerWrapper<'_> {
118+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119+
Pointer::fmt(self.0, f)
120+
}
121+
}
122+
123+
#[cfg(all(feature = "display", feature = "debug"))]
124+
impl<'a, T: Display + Debug> From<&'a T> for Formattable<'a> {
125+
fn from(value: &'a T) -> Self {
126+
Self::debug_display(value)
127+
}
128+
}

src/hard_coded/iter.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use crate::*;
2+
3+
fn write_iter(
4+
out: &mut impl Write,
5+
value: &[Formattable<'_>],
6+
range: Option<Range>,
7+
format: &str,
8+
join: Option<&str>,
9+
) -> Result {
10+
if value.is_empty() {
11+
return Ok(());
12+
}
13+
let Range(lhs, inclusive, rhs) = range.unwrap_or(Range(None, false, None));
14+
let rhs = rhs.unwrap_or(isize::MAX);
15+
let rhs = (usize::try_from(rhs).unwrap_or(value.len().saturating_sub(rhs.unsigned_abs()))
16+
+ usize::from(inclusive))
17+
.min(value.len());
18+
let lhs = lhs.unwrap_or(0);
19+
let lhs = usize::try_from(lhs)
20+
.unwrap_or(value.len().saturating_sub(lhs.unsigned_abs()))
21+
.min(rhs);
22+
23+
if rhs > lhs {
24+
for value in &value[lhs..rhs - 1] {
25+
let mut context = HashMap::new();
26+
context.insert("it", value);
27+
write(out, format, &context)?;
28+
if let Some(join) = join {
29+
write!(out, "{join}").map_err(|e| Error::Fmt(e, 0))?;
30+
}
31+
}
32+
if let Some(value) = value[..rhs].last() {
33+
let mut context = HashMap::new();
34+
context.insert("it", value);
35+
write(out, format, &context)?;
36+
}
37+
}
38+
Ok(())
39+
}
40+
41+
#[allow(clippy::too_many_arguments)]
42+
pub(crate) fn iter(
43+
out: &mut impl Write,
44+
value: &[Formattable<'_>],
45+
width: Option<usize>,
46+
precision: Option<usize>,
47+
alignment: crate::Alignment,
48+
sign: Sign,
49+
hash: bool,
50+
zero: bool,
51+
range: Option<Range>,
52+
format: &str,
53+
join: Option<&str>,
54+
) -> Result {
55+
match (precision, sign, hash, zero) {
56+
(None, Sign::None, false, false) => {
57+
if let Some(width) = width {
58+
let mut buf = String::new();
59+
write_iter(&mut buf, value, range, format, join)?;
60+
match alignment {
61+
Alignment::Left | Alignment::None => write!(out, "{buf:<width$}"),
62+
Alignment::Center => write!(out, "{buf:^width$}"),
63+
Alignment::Right => write!(out, "{buf:>width$}"),
64+
}
65+
.map_err(|e| Error::Fmt(e, 0))
66+
} else {
67+
write_iter(out, value, range, format, join)
68+
}
69+
}
70+
_ => Err(ParseError::Iter(0).into()),
71+
}
72+
}

0 commit comments

Comments
 (0)