summary refs log tree commit diff stats
diff options
context:
space:
mode:
authorSoniEx2 <endermoneymod@gmail.com>2021-12-04 17:46:00 -0300
committerSoniEx2 <endermoneymod@gmail.com>2021-12-04 17:46:00 -0300
commit36395c7437218b86c832cc041f7a58f325a007bb (patch)
tree83c3e2a5c1fc265583c4b8e560b4ba732742f70d
parent6eee8e415fb2361a91cbc1151f37003bb2cd449b (diff)
Add integer and float arguments
-rw-r--r--src/args.rs95
-rw-r--r--src/error.rs42
-rw-r--r--src/lib.rs3
-rw-r--r--src/strcursor.rs32
-rw-r--r--src/tree.rs1
-rw-r--r--tests/arguments.rs158
-rw-r--r--tests/common/errorfunc.rs66
-rw-r--r--tests/common/errorpanic.rs21
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() {