Skip to content

Commit bf648a4

Browse files
authored
Improve ParseIndexError::InvalidCharacter error (#94)
* improves ParseIndexError::InvalidCharacter
1 parent 5c81e47 commit bf648a4

File tree

2 files changed

+91
-36
lines changed

2 files changed

+91
-36
lines changed

src/assign.rs

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -552,10 +552,10 @@ mod toml {
552552
mod tests {
553553
use super::{Assign, AssignError};
554554
use crate::{
555-
index::{OutOfBoundsError, ParseIndexError},
555+
index::{InvalidCharacterError, OutOfBoundsError, ParseIndexError},
556556
Pointer,
557557
};
558-
use alloc::str::FromStr;
558+
use alloc::vec;
559559
use core::fmt::{Debug, Display};
560560

561561
#[derive(Debug)]
@@ -605,8 +605,8 @@ mod tests {
605605
#[test]
606606
#[cfg(feature = "json")]
607607
fn assign_json() {
608-
use alloc::vec;
609608
use serde_json::json;
609+
610610
Test::all([
611611
Test {
612612
ptr: "/foo",
@@ -748,12 +748,15 @@ mod tests {
748748
expected_data: json!(["bar"]),
749749
},
750750
Test {
751-
ptr: "/a",
751+
ptr: "/12a",
752752
data: json!([]),
753753
assign: json!("foo"),
754754
expected: Err(AssignError::FailedToParseIndex {
755755
offset: 0,
756-
source: ParseIndexError::InvalidInteger(usize::from_str("foo").unwrap_err()),
756+
source: ParseIndexError::InvalidCharacter(InvalidCharacterError {
757+
source: "12a".into(),
758+
offset: 2,
759+
}),
757760
}),
758761
expected_data: json!([]),
759762
},
@@ -773,7 +776,10 @@ mod tests {
773776
assign: json!("foo"),
774777
expected: Err(AssignError::FailedToParseIndex {
775778
offset: 0,
776-
source: ParseIndexError::InvalidCharacters("+".into()),
779+
source: ParseIndexError::InvalidCharacter(InvalidCharacterError {
780+
source: "+23".into(),
781+
offset: 0,
782+
}),
777783
}),
778784
expected_data: json!([]),
779785
},
@@ -789,8 +795,8 @@ mod tests {
789795
#[test]
790796
#[cfg(feature = "toml")]
791797
fn assign_toml() {
792-
use alloc::vec;
793798
use toml::{toml, Table, Value};
799+
794800
Test::all([
795801
Test {
796802
data: Value::Table(toml::Table::new()),
@@ -925,7 +931,10 @@ mod tests {
925931
assign: "foo".into(),
926932
expected: Err(AssignError::FailedToParseIndex {
927933
offset: 0,
928-
source: ParseIndexError::InvalidInteger(usize::from_str("foo").unwrap_err()),
934+
source: ParseIndexError::InvalidCharacter(InvalidCharacterError {
935+
source: "a".into(),
936+
offset: 0,
937+
}),
929938
}),
930939
expected_data: Value::Array(vec![]),
931940
},

src/index.rs

Lines changed: 74 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
//! ```
3737
3838
use crate::Token;
39-
use alloc::{string::String, vec::Vec};
39+
use alloc::string::String;
4040
use core::{fmt, num::ParseIntError, str::FromStr};
4141

4242
/// Represents an abstract index into an array.
@@ -166,21 +166,22 @@ impl FromStr for Index {
166166
} else if s.starts_with('0') && s != "0" {
167167
Err(ParseIndexError::LeadingZeros)
168168
} else {
169-
let idx = s.parse::<usize>().map(Index::Num)?;
170-
if s.chars().all(|c| c.is_ascii_digit()) {
171-
Ok(idx)
172-
} else {
173-
// this comes up with the `+` sign which is valid for
174-
// representing a `usize` but not allowed in RFC 6901 array
175-
// indices
176-
let mut invalid: Vec<_> = s.chars().filter(|c| !c.is_ascii_digit()).collect();
177-
// remove duplicate characters
178-
invalid.sort_unstable();
179-
invalid.dedup();
180-
Err(ParseIndexError::InvalidCharacters(
181-
invalid.into_iter().collect(),
182-
))
183-
}
169+
s.chars().position(|c| !c.is_ascii_digit()).map_or_else(
170+
|| {
171+
s.parse::<usize>()
172+
.map(Index::Num)
173+
.map_err(ParseIndexError::from)
174+
},
175+
|offset| {
176+
// this comes up with the `+` sign which is valid for
177+
// representing a `usize` but not allowed in RFC 6901 array
178+
// indices
179+
Err(ParseIndexError::InvalidCharacter(InvalidCharacterError {
180+
source: String::from(s),
181+
offset,
182+
}))
183+
},
184+
)
184185
}
185186
}
186187
}
@@ -274,15 +275,15 @@ impl std::error::Error for OutOfBoundsError {}
274275
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
275276
*/
276277

277-
/// Indicates that the `Token` could not be parsed as valid RFC 6901 index.
278+
/// Indicates that the `Token` could not be parsed as valid RFC 6901 array index.
278279
#[derive(Debug, PartialEq, Eq)]
279280
pub enum ParseIndexError {
280281
/// The Token does not represent a valid integer.
281282
InvalidInteger(ParseIntError),
282283
/// The Token contains leading zeros.
283284
LeadingZeros,
284-
/// The Token contains non-digit characters.
285-
InvalidCharacters(String),
285+
/// The Token contains a non-digit character.
286+
InvalidCharacter(InvalidCharacterError),
286287
}
287288

288289
impl From<ParseIntError> for ParseIndexError {
@@ -301,11 +302,7 @@ impl fmt::Display for ParseIndexError {
301302
f,
302303
"token contained leading zeros, which are disallowed by RFC 6901"
303304
),
304-
ParseIndexError::InvalidCharacters(chars) => write!(
305-
f,
306-
"token contains non-digit character(s) '{chars}', \
307-
which are disallowed by RFC 6901",
308-
),
305+
ParseIndexError::InvalidCharacter(err) => err.fmt(f),
309306
}
310307
}
311308
}
@@ -315,11 +312,56 @@ impl std::error::Error for ParseIndexError {
315312
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
316313
match self {
317314
ParseIndexError::InvalidInteger(source) => Some(source),
318-
ParseIndexError::LeadingZeros | ParseIndexError::InvalidCharacters(_) => None,
315+
ParseIndexError::InvalidCharacter(source) => Some(source),
316+
ParseIndexError::LeadingZeros => None,
319317
}
320318
}
321319
}
322320

321+
/// Indicates that a non-digit character was found when parsing the RFC 6901 array index.
322+
#[derive(Debug, PartialEq, Eq)]
323+
pub struct InvalidCharacterError {
324+
pub(crate) source: String,
325+
pub(crate) offset: usize,
326+
}
327+
328+
impl InvalidCharacterError {
329+
/// Returns the offset of the character in the string.
330+
///
331+
/// This offset is given in characters, not in bytes.
332+
pub fn offset(&self) -> usize {
333+
self.offset
334+
}
335+
336+
/// Returns the source string.
337+
pub fn source(&self) -> &str {
338+
&self.source
339+
}
340+
341+
/// Returns the offending character.
342+
#[allow(clippy::missing_panics_doc)]
343+
pub fn char(&self) -> char {
344+
self.source
345+
.chars()
346+
.nth(self.offset)
347+
.expect("char was found at offset")
348+
}
349+
}
350+
351+
impl fmt::Display for InvalidCharacterError {
352+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
353+
write!(
354+
f,
355+
"token contains the non-digit character '{}', \
356+
which is disallowed by RFC 6901",
357+
self.char()
358+
)
359+
}
360+
}
361+
362+
#[cfg(feature = "std")]
363+
impl std::error::Error for InvalidCharacterError {}
364+
323365
/*
324366
░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░
325367
╔══════════════════════════════════════════════════════════════════════════════╗
@@ -435,9 +477,13 @@ mod tests {
435477
"token contained leading zeros, which are disallowed by RFC 6901"
436478
);
437479
assert_eq!(
438-
ParseIndexError::InvalidCharacters("+@".into()).to_string(),
439-
"token contains non-digit character(s) '+@', \
440-
which are disallowed by RFC 6901"
480+
ParseIndexError::InvalidCharacter(InvalidCharacterError {
481+
source: "+10".into(),
482+
offset: 0
483+
})
484+
.to_string(),
485+
"token contains the non-digit character '+', \
486+
which is disallowed by RFC 6901"
441487
);
442488
}
443489

0 commit comments

Comments
 (0)