use crate::{
diagnostic::{diagnostic_url, Diagnostic, Label},
Token,
};
use alloc::{boxed::Box, string::String};
use core::{fmt, iter::once, num::ParseIntError, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Index {
Num(usize),
Next,
}
impl Index {
pub fn for_len(&self, length: usize) -> Result<usize, OutOfBoundsError> {
match *self {
Self::Num(index) if index < length => Ok(index),
Self::Num(index) => Err(OutOfBoundsError { length, index }),
Self::Next => Err(OutOfBoundsError {
length,
index: length,
}),
}
}
pub fn for_len_incl(&self, length: usize) -> Result<usize, OutOfBoundsError> {
match *self {
Self::Num(index) if index <= length => Ok(index),
Self::Num(index) => Err(OutOfBoundsError { length, index }),
Self::Next => Ok(length),
}
}
pub fn for_len_unchecked(&self, length: usize) -> usize {
match *self {
Self::Num(idx) => idx,
Self::Next => length,
}
}
}
impl fmt::Display for Index {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match *self {
Self::Num(index) => write!(f, "{index}"),
Self::Next => f.write_str("-"),
}
}
}
impl From<usize> for Index {
fn from(value: usize) -> Self {
Self::Num(value)
}
}
impl FromStr for Index {
type Err = ParseIndexError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "-" {
Ok(Index::Next)
} else if s.starts_with('0') && s != "0" {
Err(ParseIndexError::LeadingZeros)
} else {
s.chars().position(|c| !c.is_ascii_digit()).map_or_else(
|| {
s.parse::<usize>()
.map(Index::Num)
.map_err(ParseIndexError::from)
},
|offset| {
Err(ParseIndexError::InvalidCharacter(InvalidCharacterError {
offset,
}))
},
)
}
}
}
impl TryFrom<&Token<'_>> for Index {
type Error = ParseIndexError;
fn try_from(value: &Token) -> Result<Self, Self::Error> {
Index::from_str(value.encoded())
}
}
impl TryFrom<&str> for Index {
type Error = ParseIndexError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Index::from_str(value)
}
}
impl TryFrom<Token<'_>> for Index {
type Error = ParseIndexError;
fn try_from(value: Token) -> Result<Self, Self::Error> {
Index::from_str(value.encoded())
}
}
macro_rules! derive_try_from {
($($t:ty),+ $(,)?) => {
$(
impl TryFrom<$t> for Index {
type Error = ParseIndexError;
fn try_from(value: $t) -> Result<Self, Self::Error> {
Index::from_str(&value)
}
}
)*
}
}
derive_try_from!(String, &String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutOfBoundsError {
pub length: usize,
pub index: usize,
}
impl fmt::Display for OutOfBoundsError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"index {} out of bounds (len: {})",
self.index, self.length
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for OutOfBoundsError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseIndexError {
InvalidInteger(ParseIntError),
LeadingZeros,
InvalidCharacter(InvalidCharacterError),
}
impl From<ParseIntError> for ParseIndexError {
fn from(source: ParseIntError) -> Self {
Self::InvalidInteger(source)
}
}
impl fmt::Display for ParseIndexError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ParseIndexError::InvalidInteger(_) => {
write!(f, "failed to parse token as an integer")
}
ParseIndexError::LeadingZeros => write!(
f,
"token contained leading zeros, which are disallowed by RFC 6901"
),
ParseIndexError::InvalidCharacter(_) => {
write!(f, "failed to parse token as an index")
}
}
}
}
#[doc(hidden)]
#[derive(Debug)]
pub enum StringOrToken {
String(String),
Token(Token<'static>),
}
impl From<String> for StringOrToken {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<Token<'static>> for StringOrToken {
fn from(value: Token<'static>) -> Self {
Self::Token(value)
}
}
impl core::ops::Deref for StringOrToken {
type Target = str;
fn deref(&self) -> &Self::Target {
match self {
StringOrToken::String(s) => s.as_str(),
StringOrToken::Token(t) => t.encoded(),
}
}
}
#[cfg(feature = "miette")]
impl miette::SourceCode for StringOrToken {
fn read_span<'a>(
&'a self,
span: &miette::SourceSpan,
context_lines_before: usize,
context_lines_after: usize,
) -> Result<Box<dyn miette::SpanContents<'a> + 'a>, miette::MietteError> {
let s: &str = self;
s.read_span(span, context_lines_before, context_lines_after)
}
}
impl Diagnostic for ParseIndexError {
type Subject = StringOrToken;
fn url() -> &'static str {
diagnostic_url!(enum ParseIndexError)
}
fn labels(
&self,
subject: &Self::Subject,
) -> Option<Box<dyn Iterator<Item = crate::diagnostic::Label>>> {
let subject = &**subject;
match self {
ParseIndexError::InvalidInteger(_) => None,
ParseIndexError::LeadingZeros => {
let len = subject
.chars()
.position(|c| c != '0')
.expect("starts with zeros");
let text = String::from("leading zeros");
Some(Box::new(once(Label::new(text, 0, len))))
}
ParseIndexError::InvalidCharacter(err) => {
let len = subject
.chars()
.skip(err.offset)
.position(|c| c.is_ascii_digit())
.unwrap_or(subject.len());
let text = String::from("invalid character(s)");
Some(Box::new(once(Label::new(text, err.offset, len))))
}
}
}
}
#[cfg(feature = "miette")]
impl miette::Diagnostic for ParseIndexError {}
#[cfg(feature = "std")]
impl std::error::Error for ParseIndexError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
ParseIndexError::InvalidInteger(source) => Some(source),
ParseIndexError::InvalidCharacter(source) => Some(source),
ParseIndexError::LeadingZeros => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InvalidCharacterError {
pub(crate) offset: usize,
}
impl InvalidCharacterError {
pub fn offset(&self) -> usize {
self.offset
}
}
impl fmt::Display for InvalidCharacterError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"token contains a non-digit character, \
which is disallowed by RFC 6901",
)
}
}
#[cfg(feature = "std")]
impl std::error::Error for InvalidCharacterError {}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Diagnose, Token};
#[test]
fn index_from_usize() {
let index = Index::from(5usize);
assert_eq!(index, Index::Num(5));
}
#[test]
fn index_try_from_token_num() {
let token = Token::new("3");
let index = Index::try_from(&token).unwrap();
assert_eq!(index, Index::Num(3));
}
#[test]
fn index_try_from_token_next() {
let token = Token::new("-");
let index = Index::try_from(&token).unwrap();
assert_eq!(index, Index::Next);
}
#[test]
fn index_try_from_str_num() {
let index = Index::try_from("42").unwrap();
assert_eq!(index, Index::Num(42));
}
#[test]
fn index_try_from_str_next() {
let index = Index::try_from("-").unwrap();
assert_eq!(index, Index::Next);
}
#[test]
fn index_try_from_string_num() {
let index = Index::try_from(String::from("7")).unwrap();
assert_eq!(index, Index::Num(7));
}
#[test]
fn index_try_from_string_next() {
let index = Index::try_from(String::from("-")).unwrap();
assert_eq!(index, Index::Next);
}
#[test]
fn index_for_len_incl_valid() {
assert_eq!(Index::Num(0).for_len_incl(1), Ok(0));
assert_eq!(Index::Next.for_len_incl(2), Ok(2));
}
#[test]
fn index_for_len_incl_out_of_bounds() {
Index::Num(2).for_len_incl(1).unwrap_err();
}
#[test]
fn index_for_len_unchecked() {
assert_eq!(Index::Num(10).for_len_unchecked(5), 10);
assert_eq!(Index::Next.for_len_unchecked(3), 3);
}
#[test]
fn display_index_num() {
let index = Index::Num(5);
assert_eq!(index.to_string(), "5");
}
#[test]
fn display_index_next() {
assert_eq!(Index::Next.to_string(), "-");
}
#[test]
fn for_len() {
assert_eq!(Index::Num(0).for_len(1), Ok(0));
assert!(Index::Num(1).for_len(1).is_err());
assert!(Index::Next.for_len(1).is_err());
}
#[test]
fn try_from_token() {
let token = Token::new("3");
let index = <Index as TryFrom<Token>>::try_from(token).unwrap();
assert_eq!(index, Index::Num(3));
let token = Token::new("-");
let index = Index::try_from(&token).unwrap();
assert_eq!(index, Index::Next);
}
#[test]
fn diagnose_works_with_token_or_string() {
let token = Token::new("foo");
Index::try_from(token.clone()).diagnose(token).unwrap_err();
let s = String::from("bar");
Index::try_from(&s).diagnose(s).unwrap_err();
}
#[test]
fn error_from_invalid_chars() {
let s = String::from("bar");
let err = Index::try_from(&s).diagnose(s).unwrap_err();
#[cfg(feature = "miette")]
{
let labels: Vec<_> = miette::Diagnostic::labels(&err).unwrap().collect();
assert_eq!(
labels,
vec![miette::LabeledSpan::new(
Some("invalid character(s)".into()),
0,
3
)]
);
}
let (src, sub) = err.decompose();
let labels: Vec<_> = src.labels(&sub).unwrap().collect();
assert_eq!(
labels,
vec![Label::new("invalid character(s)".into(), 0, 3)]
);
}
#[test]
fn error_from_leading_zeros() {
let s = String::from("000001");
let err = Index::try_from(&s).diagnose(s).unwrap_err();
#[cfg(feature = "miette")]
{
let labels: Vec<_> = miette::Diagnostic::labels(&err).unwrap().collect();
assert_eq!(
labels,
vec![miette::LabeledSpan::new(Some("leading zeros".into()), 0, 5)]
);
}
let (src, sub) = err.decompose();
let labels: Vec<_> = src.labels(&sub).unwrap().collect();
assert_eq!(labels, vec![Label::new("leading zeros".into(), 0, 5)]);
}
#[test]
fn error_from_empty_string() {
let s = String::new();
let err = Index::try_from(&s).diagnose(s).unwrap_err();
#[cfg(feature = "miette")]
{
assert!(miette::Diagnostic::labels(&err).is_none());
}
let (src, sub) = err.decompose();
assert!(src.labels(&sub).is_none());
}
}