diff options
author | SoniEx2 <endermoneymod@gmail.com> | 2021-12-04 17:46:00 -0300 |
---|---|---|
committer | SoniEx2 <endermoneymod@gmail.com> | 2021-12-04 17:46:00 -0300 |
commit | 36395c7437218b86c832cc041f7a58f325a007bb (patch) | |
tree | 83c3e2a5c1fc265583c4b8e560b4ba732742f70d | |
parent | 6eee8e415fb2361a91cbc1151f37003bb2cd449b (diff) |
Add integer and float arguments
-rw-r--r-- | src/args.rs | 95 | ||||
-rw-r--r-- | src/error.rs | 42 | ||||
-rw-r--r-- | src/lib.rs | 3 | ||||
-rw-r--r-- | src/strcursor.rs | 32 | ||||
-rw-r--r-- | src/tree.rs | 1 | ||||
-rw-r--r-- | tests/arguments.rs | 158 | ||||
-rw-r--r-- | tests/common/errorfunc.rs | 66 | ||||
-rw-r--r-- | tests/common/errorpanic.rs | 21 |
8 files changed, 384 insertions, 34 deletions
diff --git a/src/args.rs b/src/args.rs index 178203d..2a9bc28 100644 --- a/src/args.rs +++ b/src/args.rs @@ -9,9 +9,16 @@ use ::std::any::Any; use ::std::borrow::Cow; use ::std::future::Future; use ::std::io::Cursor; +use ::std::marker::PhantomData; +use ::std::num::ParseFloatError; +use ::std::num::ParseIntError; +use ::std::ops::RangeBounds; use ::std::pin::Pin; +use ::std::str::FromStr; -use crate::strcursor::{StringReader, ReadError}; +use crate::error::RangeError; +use crate::error::ReadError; +use crate::strcursor::StringReader; use crate::suggestion::Suggestions; use crate::suggestion::SuggestionsBuilder; @@ -34,7 +41,8 @@ pub struct CommandContext<'i, S, E>(::std::marker::PhantomData<(&'i str, S, E)>) /// use ::std::io::Cursor; /// /// use ::iosonism::args::ArgumentType; -/// use ::iosonism::strcursor::{ReadError, StringReader}; +/// use ::iosonism::error::ReadError; +/// use ::iosonism::strcursor::StringReader; /// /// struct BoolArgumentType; /// @@ -120,6 +128,7 @@ impl<T: ArgumentType<S, E>, S, E> ArgumentTypeAny<S, E> for T { /// A boolean argument. // FIXME add examples/expand docs // FIXME add tests +#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash, Default)] pub struct BoolArgumentType; /// An `ArgumentType` for `bool`. @@ -158,3 +167,85 @@ where for<'i> E: ReadError<'i, Cursor<&'i str>> Cow::Borrowed(&["true", "false"]) } } + +/// An integer argument. +#[derive(Copy, Clone, PartialEq, Eq, Debug, PartialOrd, Ord, Hash, Default)] +pub struct IntegerArgumentType<T, R: RangeBounds<T>> { + /// The valid range for this argument. + pub range: R, + /// PhantomData for the type. + pub _ty: PhantomData<T>, +} + +/// An `ArgumentType` for integer types. +impl<S, E, T, R> ArgumentType<S, E> for IntegerArgumentType<T, R> +where + for<'i> E: ReadError<'i, Cursor<&'i str>>, + for<'i> E: RangeError<'i, Cursor<&'i str>, T, R>, + R: RangeBounds<T>, + T: PartialOrd<T> + FromStr<Err=ParseIntError> + Any, +{ + /// An `IntegerArgumentType` parses an integer type. + type Result = T; + + /// Attempts to parse an integer from the `reader`. + fn parse<'i>( + &self, + reader: &mut Cursor<&'i str>, + ) -> Result<T, E> where E: 'i { + let start = reader.position(); + let value = reader.read_integer()?; + if self.range.contains(&value) { + Ok(value) + } else { + reader.set_position(start); + Err(E::value_not_in_range(reader, &value, &self.range)) + } + } + + /// Returns examples + fn get_examples(&self) -> Cow<'static, [&str]> { + Cow::Borrowed(&["0", "123", "-123"]) + } +} + +/// A float argument. +#[derive(Copy, Clone, PartialEq, Debug, PartialOrd, Default)] +pub struct FloatArgumentType<T, R: RangeBounds<T>> { + /// The valid range for this argument. + pub range: R, + /// PhantomData for the type. + pub _ty: PhantomData<T>, +} + +/// An `ArgumentType` for float types. +impl<S, E, T, R> ArgumentType<S, E> for FloatArgumentType<T, R> +where + for<'i> E: ReadError<'i, Cursor<&'i str>>, + for<'i> E: RangeError<'i, Cursor<&'i str>, T, R>, + R: RangeBounds<T>, + T: PartialOrd<T> + FromStr<Err=ParseFloatError> + Any, +{ + /// A `FloatArgumentType` parses a float type. + type Result = T; + + /// Attempts to parse a float from the `reader`. + fn parse<'i>( + &self, + reader: &mut Cursor<&'i str>, + ) -> Result<T, E> where E: 'i { + let start = reader.position(); + let value = reader.read_float()?; + if self.range.contains(&value) { + Ok(value) + } else { + reader.set_position(start); + Err(E::value_not_in_range(reader, &value, &self.range)) + } + } + + /// Returns examples + fn get_examples(&self) -> Cow<'static, [&str]> { + Cow::Borrowed(&["0", "1.2", ".5", "-1", "-.5", "-1234.56"]) + } +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..74ba2fd --- /dev/null +++ b/src/error.rs @@ -0,0 +1,42 @@ +// Copyright (c) 2021 Soni L. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +// Documentation and comments licensed under CC BY-SA 4.0. + +//! Built-in error traits. +//! +//! Iosonism uses a trait-based approach because that's just far more flexible. + +use ::std::error::Error as StdError; + +use crate::strcursor::StringReader; + +/// Built-in `StringReader` errors. +pub trait ReadError<'a, C: StringReader<'a>>: Sized + StdError { + /// Creates an error that indicates an invalid integer was found. + fn invalid_integer(context: &C, from: &str) -> Self; + /// Creates an error that indicates an integer was expected. + fn expected_integer(context: &C) -> Self; + /// Creates an error that indicates an invalid float was found. + fn invalid_float(context: &C, from: &str) -> Self; + /// Creates an error that indicates a float was expected. + fn expected_float(context: &C) -> Self; + /// Creates an error that indicates an invalid bool was found. + fn invalid_bool(context: &C, from: &str) -> Self; + /// Creates an error that indicates a bool was expected. + fn expected_bool(context: &C) -> Self; + /// Creates an error that indicates the start of a quote was expected. + fn expected_start_of_quote(context: &C) -> Self; + /// Creates an error that indicates the end of a quote was expected. + fn expected_end_of_quote(context: &C) -> Self; + /// Creates an error that indicates an invalid escape was found. + fn invalid_escape(context: &C, from: &str) -> Self; + /// Creates an error that indicates a symbol was expected. + fn expected_symbol(context: &C, from: &str) -> Self; +} + +/// Built-in errors for `IntegerArgumentType` and `FloatArgumentType`. +pub trait RangeError<'a, C: StringReader<'a>, T, R>: Sized + StdError { + /// Creates an error that indicates a value was outside the required bounds. + fn value_not_in_range(context: &C, from: &T, range: &R) -> Self; +} diff --git a/src/lib.rs b/src/lib.rs index 6921bbb..1895244 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -19,9 +19,10 @@ // - brigadier.suggestion.Suggestions -> iosonism::suggestion::Suggestions; // - brigadier.suggestion.SuggestionsBuilder -> iosonism::suggestion::SuggestionsBuilder; +pub mod args; +pub mod error; pub mod strcursor; pub mod suggestion; -pub mod args; pub mod tree; use crate::args::CommandContext; diff --git a/src/strcursor.rs b/src/strcursor.rs index b9bf626..d733ade 100644 --- a/src/strcursor.rs +++ b/src/strcursor.rs @@ -8,29 +8,7 @@ use ::std::io::Cursor; use ::std::str::FromStr; -/// Built-in `StringReader` errors. -pub trait ReadError<'a, C: StringReader<'a>>: Sized + std::error::Error { - /// Creates an error that indicates an invalid integer was found. - fn invalid_integer(context: &C, from: &str) -> Self; - /// Creates an error that indicates an integer was expected. - fn expected_integer(context: &C) -> Self; - /// Creates an error that indicates an invalid float was found. - fn invalid_float(context: &C, from: &str) -> Self; - /// Creates an error that indicates a float was expected. - fn expected_float(context: &C) -> Self; - /// Creates an error that indicates an invalid bool was found. - fn invalid_bool(context: &C, from: &str) -> Self; - /// Creates an error that indicates a bool was expected. - fn expected_bool(context: &C) -> Self; - /// Creates an error that indicates the start of a quote was expected. - fn expected_start_of_quote(context: &C) -> Self; - /// Creates an error that indicates the end of a quote was expected. - fn expected_end_of_quote(context: &C) -> Self; - /// Creates an error that indicates an invalid escape was found. - fn invalid_escape(context: &C, from: &str) -> Self; - /// Creates an error that indicates a symbol was expected. - fn expected_symbol(context: &C, from: &str) -> Self; -} +use crate::error::ReadError; /// Extension trait on [`Cursor`]s to help with command parsing. /// @@ -130,14 +108,14 @@ pub trait StringReader<'a>: Sized { /// /// Panics if this cursor is not on an UTF-8 character boundary. fn read_integer<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E> - where T: FromStr<Err=std::num::ParseIntError>; + where T: FromStr<Err=::std::num::ParseIntError>; /// Reads a float. /// /// # Panics /// /// Panics if this cursor is not on an UTF-8 character boundary. fn read_float<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E> - where T: FromStr<Err=std::num::ParseFloatError>; + where T: FromStr<Err=::std::num::ParseFloatError>; /// Reads a bool. /// /// # Panics @@ -202,7 +180,7 @@ impl<'a> StringReader<'a> for Cursor<&'a str> { } fn read_integer<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E> - where T: FromStr<Err=std::num::ParseIntError> { + where T: FromStr<Err=::std::num::ParseIntError> { // see read_unquoted_str for rationale let start = self.position() as usize; let total = self.get_ref().len(); @@ -223,7 +201,7 @@ impl<'a> StringReader<'a> for Cursor<&'a str> { } } fn read_float<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E> - where T: FromStr<Err=std::num::ParseFloatError> { + where T: FromStr<Err=::std::num::ParseFloatError> { // see read_unquoted_str for rationale let start = self.position() as usize; let total = self.get_ref().len(); diff --git a/src/tree.rs b/src/tree.rs index b534754..daac8d3 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -3,6 +3,7 @@ // Licensed under the MIT license. // Documentation and comments licensed under CC BY-SA 4.0. +#![allow(dead_code, unused_imports)] // for now //! Command syntax tree. use ::std::borrow::Cow; diff --git a/tests/arguments.rs b/tests/arguments.rs new file mode 100644 index 0000000..4b5a2a0 --- /dev/null +++ b/tests/arguments.rs @@ -0,0 +1,158 @@ +// Copyright (c) 2021 Soni L. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. +// Documentation and comments licensed under CC BY-SA 4.0. + +// because we wanna use double underscore (__) for test names +#![allow(non_snake_case)] + +use ::std::io::Cursor; +use ::std::marker::PhantomData; + +use ::iosonism::args::ArgumentType; +use ::iosonism::args::BoolArgumentType; +use ::iosonism::args::FloatArgumentType; +use ::iosonism::args::IntegerArgumentType; +use ::iosonism::strcursor::StringReader; + +mod common; + +use self::common::ErrorCall; +use self::common::ErrorFunc; +use self::common::ErrorPanic; +use self::common::ErrorType; + +#[test] +fn test_bool__parse() { + assert_eq!( + ArgumentType::<(), ErrorPanic>::parse( + &BoolArgumentType, + &mut Cursor::new("true"), + ), + Ok(true), + ); + assert_eq!( + ArgumentType::<(), ErrorPanic>::parse( + &BoolArgumentType, + &mut Cursor::new("false"), + ), + Ok(false), + ); +} + +#[test] +fn test_i32__parse() { + let mut reader = Cursor::new("15"); + assert_eq!( + ArgumentType::<(), ErrorPanic>::parse( + &IntegerArgumentType { range: .., _ty: PhantomData::<i32> }, + &mut reader, + ), + Ok(15), + ); + assert!(!reader.can_read()); +} + +#[test] +fn test_i32__parse__range() { + let mut reader = Cursor::new("-5"); + assert!(ArgumentType::<(), ErrorCall<ErrFn>>::parse( + &IntegerArgumentType { range: 0..=100, _ty: PhantomData::<i32> }, + &mut reader, + ).is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert!(matches!(ty, ErrorType::RangeErrori32(..))); + assert_eq!(context.position(), 0); + } + } +} + +#[test] +fn test_i64__parse() { + let mut reader = Cursor::new("15"); + assert_eq!( + ArgumentType::<(), ErrorPanic>::parse( + &IntegerArgumentType { range: .., _ty: PhantomData::<i64> }, + &mut reader, + ), + Ok(15), + ); + assert!(!reader.can_read()); +} + +#[test] +fn test_i64__parse__range() { + let mut reader = Cursor::new("-5"); + assert!(ArgumentType::<(), ErrorCall<ErrFn>>::parse( + &IntegerArgumentType { range: 0..=100, _ty: PhantomData::<i64> }, + &mut reader, + ).is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert!(matches!(ty, ErrorType::RangeErrori64(..))); + assert_eq!(context.position(), 0); + } + } +} + +#[test] +fn test_f32__parse() { + let mut reader = Cursor::new("15"); + assert_eq!( + ArgumentType::<(), ErrorPanic>::parse( + &FloatArgumentType { range: .., _ty: PhantomData::<f32> }, + &mut reader, + ), + Ok(15.0), + ); + assert!(!reader.can_read()); +} + +#[test] +fn test_f32__parse__range() { + let mut reader = Cursor::new("-5"); + assert!(ArgumentType::<(), ErrorCall<ErrFn>>::parse( + &FloatArgumentType { range: 0.0..=100.0, _ty: PhantomData::<f32> }, + &mut reader, + ).is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert!(matches!(ty, ErrorType::RangeErrorf32(..))); + assert_eq!(context.position(), 0); + } + } +} + +#[test] +fn test_f64__parse() { + let mut reader = Cursor::new("15"); + assert_eq!( + ArgumentType::<(), ErrorPanic>::parse( + &FloatArgumentType { range: .., _ty: PhantomData::<f64> }, + &mut reader, + ), + Ok(15.0), + ); + assert!(!reader.can_read()); +} + +#[test] +fn test_f64__parse__range() { + let mut reader = Cursor::new("-5"); + assert!(ArgumentType::<(), ErrorCall<ErrFn>>::parse( + &FloatArgumentType { range: 0.0..=100.0, _ty: PhantomData::<f64> }, + &mut reader, + ).is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert!(matches!(ty, ErrorType::RangeErrorf64(..))); + assert_eq!(context.position(), 0); + } + } +} + diff --git a/tests/common/errorfunc.rs b/tests/common/errorfunc.rs index 926c94c..5fb5fb6 100644 --- a/tests/common/errorfunc.rs +++ b/tests/common/errorfunc.rs @@ -4,8 +4,11 @@ // Documentation and comments licensed under CC BY-SA 4.0. use ::std::marker::PhantomData; +use ::std::ops::Bound; +use ::std::ops::RangeBounds; -use ::iosonism::strcursor::ReadError; +use ::iosonism::error::RangeError; +use ::iosonism::error::ReadError; use ::iosonism::strcursor::StringReader; /// An error callback. @@ -17,7 +20,7 @@ pub trait ErrorFunc<'a, C: StringReader<'a>> { pub struct ErrorCall<T>(PhantomData<T>); #[non_exhaustive] -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Debug)] pub enum ErrorType<'a> { InvalidInteger(&'a str), ExpectedInteger, @@ -29,6 +32,17 @@ pub enum ErrorType<'a> { ExpectedEndOfQuote, InvalidEscape(&'a str), ExpectedSymbol(&'a str), + RangeErrori32(RangeErrorT<i32>), + RangeErrori64(RangeErrorT<i64>), + RangeErrorf32(RangeErrorT<f32>), + RangeErrorf64(RangeErrorT<f64>), +} + +#[derive(PartialEq, Debug)] +pub struct RangeErrorT<T> { + pub value: T, + pub start: Bound<T>, + pub end: Bound<T>, } impl<T> ::std::fmt::Display for ErrorCall<T> { @@ -46,6 +60,54 @@ impl<T> ::std::fmt::Debug for ErrorCall<T> { impl<T> ::std::error::Error for ErrorCall<T> { } +impl<'a, C, R, T> RangeError<'a, C, i32, R> for ErrorCall<T> +where C: StringReader<'a>, R: RangeBounds<i32>, T: ErrorFunc<'a, C> { + fn value_not_in_range(context: &C, from: &i32, range: &R) -> Self { + T::call(context, ErrorType::RangeErrori32(RangeErrorT { + value: *from, + start: range.start_bound().cloned(), + end: range.end_bound().cloned(), + })); + Self(PhantomData) + } +} + +impl<'a, C, R, T> RangeError<'a, C, i64, R> for ErrorCall<T> +where C: StringReader<'a>, R: RangeBounds<i64>, T: ErrorFunc<'a, C> { + fn value_not_in_range(context: &C, from: &i64, range: &R) -> Self { + T::call(context, ErrorType::RangeErrori64(RangeErrorT { + value: *from, + start: range.start_bound().cloned(), + end: range.end_bound().cloned(), + })); + Self(PhantomData) + } +} + +impl<'a, C, R, T> RangeError<'a, C, f32, R> for ErrorCall<T> +where C: StringReader<'a>, R: RangeBounds<f32>, T: ErrorFunc<'a, C> { + fn value_not_in_range(context: &C, from: &f32, range: &R) -> Self { + T::call(context, ErrorType::RangeErrorf32(RangeErrorT { + value: *from, + start: range.start_bound().cloned(), + end: range.end_bound().cloned(), + })); + Self(PhantomData) + } +} + +impl<'a, C, R, T> RangeError<'a, C, f64, R> for ErrorCall<T> +where C: StringReader<'a>, R: RangeBounds<f64>, T: ErrorFunc<'a, C> { + fn value_not_in_range(context: &C, from: &f64, range: &R) -> Self { + T::call(context, ErrorType::RangeErrorf64(RangeErrorT { + value: *from, + start: range.start_bound().cloned(), + end: range.end_bound().cloned(), + })); + Self(PhantomData) + } +} + impl<'a, C: StringReader<'a>, T> ReadError<'a, C> for ErrorCall<T> where T: ErrorFunc<'a, C> { fn invalid_integer(context: &C, from: &str) -> Self { diff --git a/tests/common/errorpanic.rs b/tests/common/errorpanic.rs index faef603..6b4254b 100644 --- a/tests/common/errorpanic.rs +++ b/tests/common/errorpanic.rs @@ -3,11 +3,12 @@ // Licensed under the MIT license. // Documentation and comments licensed under CC BY-SA 4.0. -use ::iosonism::strcursor::ReadError; +use ::iosonism::error::RangeError; +use ::iosonism::error::ReadError; use ::iosonism::strcursor::StringReader; /// An implementation of various Iosonism errors that just panics. -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub enum ErrorPanic { // uninhabitable! } @@ -22,6 +23,22 @@ impl ::std::fmt::Display for ErrorPanic { impl ::std::error::Error for ErrorPanic { } +impl<'a, C, T, R> RangeError<'a, C, T, R> for ErrorPanic +where C: StringReader<'a>, T: ::std::fmt::Display, R: ::std::fmt::Debug { + fn value_not_in_range(context: &C, from: &T, range: &R) -> Self { + if !context.get_remaining().is_empty() { + panic!( + "value ({}) not in range: {:?} at ...{}", + from, + range, + context.get_remaining(), + ); + } else { + panic!("value ({}) not in range: {:?}", from, range); + } + } +} + impl<'a, C: StringReader<'a>> ReadError<'a, C> for ErrorPanic { fn invalid_integer(context: &C, from: &str) -> Self { if !context.get_remaining().is_empty() { |