Skip to content

Commit 5dbf330

Browse files
asmellochanced
authored andcommitted
Improve ParseIndexError::InvalidCharacter error (#94)
* improves ParseIndexError::InvalidCharacter
1 parent e892a48 commit 5dbf330

File tree

2 files changed

+89
-36
lines changed

2 files changed

+89
-36
lines changed

src/assign.rs

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -585,10 +585,10 @@ mod toml {
585585
mod tests {
586586
use super::{Assign, Error};
587587
use crate::{
588-
index::{OutOfBoundsError, ParseIndexError},
588+
index::{InvalidCharacterError, OutOfBoundsError, ParseIndexError},
589589
Pointer,
590590
};
591-
use alloc::str::FromStr;
591+
use alloc::vec;
592592
use core::fmt::{Debug, Display};
593593

594594
#[derive(Debug)]
@@ -635,7 +635,6 @@ mod tests {
635635
#[test]
636636
#[cfg(feature = "json")]
637637
fn assign_json() {
638-
use alloc::vec;
639638
use serde_json::json;
640639
[
641640
Test {
@@ -779,13 +778,16 @@ mod tests {
779778
expected_data: json!(["bar"]),
780779
},
781780
Test {
782-
ptr: "/a",
781+
ptr: "/12a",
783782
data: json!([]),
784783
assign: json!("foo"),
785784
expected: Err(Error::FailedToParseIndex {
786785
position: 0,
787786
offset: 0,
788-
source: ParseIndexError::InvalidInteger(usize::from_str("foo").unwrap_err()),
787+
source: ParseIndexError::InvalidCharacter(InvalidCharacterError {
788+
source: "12a".into(),
789+
offset: 2,
790+
}),
789791
}),
790792
expected_data: json!([]),
791793
},
@@ -807,7 +809,10 @@ mod tests {
807809
expected: Err(Error::FailedToParseIndex {
808810
position: 0,
809811
offset: 0,
810-
source: ParseIndexError::InvalidCharacters("+".into()),
812+
source: ParseIndexError::InvalidCharacter(InvalidCharacterError {
813+
source: "+23".into(),
814+
offset: 0,
815+
}),
811816
}),
812817
expected_data: json!([]),
813818
},
@@ -826,7 +831,6 @@ mod tests {
826831
#[test]
827832
#[cfg(feature = "toml")]
828833
fn assign_toml() {
829-
use alloc::vec;
830834
use toml::{toml, Table, Value};
831835
[
832836
Test {
@@ -964,7 +968,10 @@ mod tests {
964968
expected: Err(Error::FailedToParseIndex {
965969
position: 0,
966970
offset: 0,
967-
source: ParseIndexError::InvalidInteger(usize::from_str("foo").unwrap_err()),
971+
source: ParseIndexError::InvalidCharacter(InvalidCharacterError {
972+
source: "a".into(),
973+
offset: 0,
974+
}),
968975
}),
969976
expected_data: Value::Array(vec![]),
970977
},

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)