diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | COPYRIGHT | 1 | ||||
-rw-r--r-- | Cargo.toml | 14 | ||||
-rw-r--r-- | LICENSE-Mojang | 21 | ||||
-rw-r--r-- | README.md | 7 | ||||
-rw-r--r-- | src/lib.rs | 10 | ||||
-rw-r--r-- | src/strcursor.rs | 368 | ||||
-rw-r--r-- | tests/common/errorfunc.rs | 89 | ||||
-rw-r--r-- | tests/common/errorpanic.rs | 114 | ||||
-rw-r--r-- | tests/common/mod.rs | 12 | ||||
-rw-r--r-- | tests/strcursor.rs | 592 |
11 files changed, 1230 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..e2d3fb0 --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1 @@ +Iosonism is a port of Brigadier (https://github.com/Mojang/brigadier) to Rust. diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..92ff3ba --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "iosonism" +version = "0.1.0" +authors = ["SoniEx2 <endermoneymod@gmail.com>"] +license = "MIT" +description = "An advanced command parser" +edition = "2021" +repository = "https://soniex2.autistic.space/git-repos/iosonism.git" +readme = "README.md" +publish = false # for now + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/LICENSE-Mojang b/LICENSE-Mojang new file mode 100644 index 0000000..88040d8 --- /dev/null +++ b/LICENSE-Mojang @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) Microsoft Corporation. All rights reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..894e6d2 --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +Iosonism +======== + +Iosonism is a command parsing library, not to be confused with an argument +parsing library. It's basically a port of [Brigadier] to Rust. + +[Brigadier]: https://github.com/Mojang/brigadier diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2900669 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +pub mod strcursor; + +#[cfg(test)] +mod tests { + #[test] + fn it_works() { + let result = 2 + 2; + assert_eq!(result, 4); + } +} diff --git a/src/strcursor.rs b/src/strcursor.rs new file mode 100644 index 0000000..f8b95c6 --- /dev/null +++ b/src/strcursor.rs @@ -0,0 +1,368 @@ +// Copyright (c) 2021 Soni L. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +//! String Cursor (sorta). + +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; +} + +/// Extension trait on [`Cursor`]s to help with command parsing. +/// +/// All `read_*` methods reset the cursor on error. +/// +/// Note that, compared to Brigadier, this lacks methods such as +/// `getRemainingLength` (use `get_remaining().len()` or +/// `remaining_slice().len()`) and `getTotalLength` (use `get_ref().len()`). +pub trait StringReader<'a>: Sized { + /// Returns the part of the string that has been read so far. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + //#[inline] + fn get_read(&self) -> &'a str; + /// Returns the part of the string that has yet to be read. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + //#[inline] + fn get_remaining(&self) -> &'a str; + /// Returns whether there's anything left to read. + #[inline] + fn can_read(&self) -> bool { + self.can_read_n(1) + } + /// Returns whether there's enough left to read, based on the passed length. + //#[inline] + fn can_read_n(&self, len: usize) -> bool; + /// Returns the next char. + /// + /// # Panics + /// + /// Panics if there's nothing left to read, or if this cursor is not on an + /// UTF-8 character boundary. + #[inline] + fn peek(&self) -> char { + self.peek_n(0) + } + /// Returns the next nth **byte** (and, if needed, subsequent bytes) as a + /// char. + /// + /// # Panics + /// + /// Panics if the offset is beyond the boundaries of the buffer, or if the + /// offset is not on an UTF-8 character boundary. + //#[inline] + fn peek_n(&self, offset: usize) -> char; + + /// Advances to the next char. + /// + /// # Panics + /// + /// Panics if there's nothing left to read, or if this cursor is not on an + /// UTF-8 character boundary. + //#[inline] + fn skip(&mut self); + /// Attempts to read the next char. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + //#[inline] + fn read_char(&mut self) -> Option<char>; + /// Checks the next char. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + fn expect<E: ReadError<'a, Self>>(&mut self, c: char) -> Result<(), E> { + if !self.can_read() || self.peek() != c { + // because we want the error constructors to take &str. + let mut buf = [0u8; 4]; + Err(E::expected_symbol(self, c.encode_utf8(&mut buf))) + } else { + Ok(self.skip()) + } + } + /// Skips whitespace. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + fn skip_whitespace(&mut self) { + // FIXME figure out if we wanna use the same whitespace rules as + // brigadier, because rust uses unicode whereas java uses java rules. + while self.can_read() && self.peek().is_whitespace() { + self.skip(); + } + } + + /// Reads an integer. + /// + /// # Panics + /// + /// 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>; + /// 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>; + /// Reads a bool. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + fn read_bool<E: ReadError<'a, Self>>(&mut self) -> Result<bool, E>; + /// Reads an unquoted string. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + // this is a bit of a weird one in that it can't error. + fn read_unquoted_str(&mut self) -> &'a str; + /// Reads a quoted string. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + fn read_quoted_string<E: ReadError<'a, Self>>( + &mut self, + ) -> Result<String, E>; + /// Reads a quoted or an unquoted string. + /// + /// # Panics + /// + /// Panics if this cursor is not on an UTF-8 character boundary. + fn read_string<E: ReadError<'a, Self>>(&mut self) -> Result<String, E>; +} + +impl<'a> StringReader<'a> for Cursor<&'a str> { + #[inline] + fn get_read(&self) -> &'a str { + &self.get_ref()[..(self.position() as usize)] + } + #[inline] + fn get_remaining(&self) -> &'a str { + &self.get_ref()[(self.position() as usize)..] + } + #[inline] + fn can_read_n(&self, len: usize) -> bool { + // NOTE: NOT overflow-aware! + self.position() as usize + len <= self.get_ref().len() + } + #[inline] + fn peek_n(&self, offset: usize) -> char { + // NOTE: NOT overflow-aware! + self.get_ref()[(self.position() as usize + offset)..] + .chars().next().unwrap() + } + + #[inline] + fn skip(&mut self) { + self.set_position(self.position() + self.peek().len_utf8() as u64); + } + #[inline] + fn read_char(&mut self) -> Option<char> { + let res = self.get_ref()[(self.position() as usize)..].chars().next(); + if let Some(c) = res { + self.set_position(self.position() + c.len_utf8() as u64); + } + res + } + + fn read_integer<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E> + 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(); + let end = total - { + self.get_remaining().trim_start_matches(number_chars).len() + }; + self.set_position(end as u64); + + let number = &self.get_ref()[start..end]; + if number.is_empty() { + // don't need to set_position here, we haven't moved + Err(E::expected_integer(self)) + } else { + number.parse().map_err(|_| { + self.set_position(start as u64); + E::invalid_integer(self, number) + }) + } + } + fn read_float<T, E: ReadError<'a, Self>>(&mut self) -> Result<T, E> + 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(); + let end = total - { + self.get_remaining().trim_start_matches(number_chars).len() + }; + self.set_position(end as u64); + + let number = &self.get_ref()[start..end]; + if number.is_empty() { + // don't need to set_position here, we haven't moved + Err(E::expected_float(self)) + } else { + number.parse().map_err(|_| { + self.set_position(start as u64); + E::invalid_float(self, number) + }) + } + } + fn read_bool<E: ReadError<'a, Self>>(&mut self) -> Result<bool, E> { + let pos = self.position(); + // NOTE: brigadier also allows quoted strings for bools. + // we consider that a bug, so we don't. + let res = match self.read_unquoted_str() { + "true" => Ok(true), + "false" => Ok(false), + "" => Err(E::expected_bool(self)), + value => { + self.set_position(pos); + Err(E::invalid_bool(self, value)) + }, + }; + res + } + fn read_unquoted_str(&mut self) -> &'a str { + // there's no easy way to grab start matches, so we have to do something + // a bit more involved. + let start = self.position() as usize; + let total = self.get_ref().len(); + let end = total - { + self.get_remaining().trim_start_matches(unquoted_chars).len() + }; + self.set_position(end as u64); + &self.get_ref()[start..end] + } + fn read_quoted_string<E: ReadError<'a, Self>>( + &mut self, + ) -> Result<String, E> { + if !self.can_read() { + Ok("".into()) + } else if quote_chars(self.peek()) { + let start = self.position() as usize; + let terminator = self.read_char().unwrap(); + let res = read_string_until(self, terminator); + if res.is_err() { + self.set_position(start as u64); + } + res + } else { + Err(E::expected_start_of_quote(self)) + } + } + fn read_string<E: ReadError<'a, Self>>(&mut self) -> Result<String, E> { + if !self.can_read() { + Ok("".into()) + } else if quote_chars(self.peek()) { + let start = self.position() as usize; + let terminator = self.read_char().unwrap(); + let res = read_string_until(self, terminator); + if res.is_err() { + self.set_position(start as u64); + } + res + } else { + Ok(self.read_unquoted_str().into()) + } + } +} + +fn read_string_until<'a, E: ReadError<'a, Cursor<&'a str>>>( + this: &mut Cursor<&'a str>, + terminator: char, +) -> Result<String, E> { + let mut result = String::new(); + let mut escaped = false; + + while let Some(c) = this.read_char() { + if escaped { + if c == terminator || escape_char(c) { + result.push(c); + escaped = false; + } else { + let mut buf = [0u8; 4]; + // NOTE: brigadier unskips the escape. we don't bother. + return Err(E::invalid_escape(this, c.encode_utf8(&mut buf))); + } + } else if escape_char(c) { + escaped = true; + } else if c == terminator { + return Ok(result); + } else { + result.push(c); + } + } + + Err(E::expected_end_of_quote(this)) +} + +/// Symbols allowed in unquoted strings. +#[inline] +fn unquoted_chars(c: char) -> bool { + matches!( + c, + '0' ..= '9' | 'A' ..= 'Z' | 'a' ..= 'z' | '_' | '-' | '.' | '+', + ) +} + +/// Symbols allowed in numbers. +#[inline] +fn number_chars(c: char) -> bool { + matches!( + c, + '0' ..= '9' | '-' | '.', + ) +} + +/// Symbols allowed to start/end a quoted string. +#[inline] +fn quote_chars(c: char) -> bool { + matches!( + c, + '"' | '\'', + ) +} + +/// Symbol allowed to escape other symbols. +#[inline] +fn escape_char(c: char) -> bool { + matches!( + c, + '\\', + ) +} diff --git a/tests/common/errorfunc.rs b/tests/common/errorfunc.rs new file mode 100644 index 0000000..99ba641 --- /dev/null +++ b/tests/common/errorfunc.rs @@ -0,0 +1,89 @@ +// Copyright (c) 2021 Soni L. + +use ::std::marker::PhantomData; + +use ::iosonism::strcursor::ReadError; +use ::iosonism::strcursor::StringReader; + +/// An error callback. +pub trait ErrorFunc<'a, C: StringReader<'a>> { + fn call<'b>(context: &C, ty: ErrorType<'b>); +} + +/// An implementation of various Iosonism errors that calls T. +pub struct ErrorCall<T>(PhantomData<T>); + +#[non_exhaustive] +#[derive(PartialEq, Eq, Debug)] +pub enum ErrorType<'a> { + InvalidInteger(&'a str), + ExpectedInteger, + InvalidFloat(&'a str), + ExpectedFloat, + InvalidBool(&'a str), + ExpectedBool, + ExpectedStartOfQuote, + ExpectedEndOfQuote, + InvalidEscape(&'a str), + ExpectedSymbol(&'a str), +} + +impl<T> ::std::fmt::Display for ErrorCall<T> { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "error!") + } +} + +impl<T> ::std::fmt::Debug for ErrorCall<T> { + fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + write!(f, "ErrorCall") + } +} + +impl<T> ::std::error::Error for ErrorCall<T> { +} + +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 { + T::call(context, ErrorType::InvalidInteger(from)); + Self(PhantomData) + } + fn expected_integer(context: &C) -> Self { + T::call(context, ErrorType::ExpectedInteger); + Self(PhantomData) + } + fn invalid_float(context: &C, from: &str) -> Self { + T::call(context, ErrorType::InvalidFloat(from)); + Self(PhantomData) + } + fn expected_float(context: &C) -> Self { + T::call(context, ErrorType::ExpectedFloat); + Self(PhantomData) + } + fn invalid_bool(context: &C, from: &str) -> Self { + T::call(context, ErrorType::InvalidBool(from)); + Self(PhantomData) + } + fn expected_bool(context: &C) -> Self { + T::call(context, ErrorType::ExpectedBool); + Self(PhantomData) + } + fn expected_start_of_quote(context: &C) -> Self { + T::call(context, ErrorType::ExpectedStartOfQuote); + Self(PhantomData) + } + fn expected_end_of_quote(context: &C) -> Self { + T::call(context, ErrorType::ExpectedEndOfQuote); + Self(PhantomData) + } + fn invalid_escape(context: &C, from: &str) -> Self { + T::call(context, ErrorType::InvalidEscape(from)); + Self(PhantomData) + } + fn expected_symbol(context: &C, from: &str) -> Self { + T::call(context, ErrorType::ExpectedSymbol(from)); + Self(PhantomData) + } +} + diff --git a/tests/common/errorpanic.rs b/tests/common/errorpanic.rs new file mode 100644 index 0000000..202c4be --- /dev/null +++ b/tests/common/errorpanic.rs @@ -0,0 +1,114 @@ +// Copyright (c) 2021 Soni L. + +use ::iosonism::strcursor::ReadError; +use ::iosonism::strcursor::StringReader; + +/// An implementation of various Iosonism errors that just panics. +#[derive(Debug)] +pub enum ErrorPanic { + // uninhabitable! +} + +impl ::std::fmt::Display for ErrorPanic { + fn fmt(&self, _: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { + match *self { + } + } +} + +impl ::std::error::Error for ErrorPanic { +} + +impl<'a, C: StringReader<'a>> ReadError<'a, C> for ErrorPanic { + fn invalid_integer(context: &C, from: &str) -> Self { + if !context.get_remaining().is_empty() { + panic!( + "invalid integer: {} at ...{}", + from, + context.get_remaining(), + ); + } else { + panic!("invalid integer: {}", from); + } + } + fn expected_integer(context: &C) -> Self { + if !context.get_remaining().is_empty() { + panic!("expected integer at ...{}", context.get_remaining()); + } else { + panic!("expected integer"); + } + } + fn invalid_float(context: &C, from: &str) -> Self { + if !context.get_remaining().is_empty() { + panic!( + "invalid float: {} at ...{}", + from, + context.get_remaining(), + ); + } else { + panic!("invalid float: {}", from); + } + } + fn expected_float(context: &C) -> Self { + if !context.get_remaining().is_empty() { + panic!("expected float at ...{}", context.get_remaining()); + } else { + panic!("expected float"); + } + } + fn invalid_bool(context: &C, from: &str) -> Self { + if !context.get_remaining().is_empty() { + panic!( + "invalid bool: {} at ...{}", + from, + context.get_remaining(), + ); + } else { + panic!("invalid bool: {}", from); + } + } + fn expected_bool(context: &C) -> Self { + if !context.get_remaining().is_empty() { + panic!("expected bool at ...{}", context.get_remaining()); + } else { + panic!("expected bool"); + } + } + fn expected_start_of_quote(context: &C) -> Self { + if !context.get_remaining().is_empty() { + panic!("expected start of quote at ...{}", context.get_remaining()); + } else { + panic!("expected start of quote"); + } + } + fn expected_end_of_quote(context: &C) -> Self { + if !context.get_remaining().is_empty() { + panic!("expected end of quote at ...{}", context.get_remaining()); + } else { + panic!("expected end of quote"); + } + } + fn invalid_escape(context: &C, from: &str) -> Self { + if !context.get_remaining().is_empty() { + panic!( + "invalid escape: {} at ...{}", + from, + context.get_remaining(), + ); + } else { + panic!("invalid escape: {}", from); + } + } + fn expected_symbol(context: &C, from: &str) -> Self { + if !context.get_remaining().is_empty() { + panic!( + "expected symbol: {} at ...{}", + from, + context.get_remaining(), + ); + } else { + panic!("expected symbol: {}", from); + } + } +} + diff --git a/tests/common/mod.rs b/tests/common/mod.rs new file mode 100644 index 0000000..6760da1 --- /dev/null +++ b/tests/common/mod.rs @@ -0,0 +1,12 @@ +// Copyright (c) 2021 Soni L. + +// see rationale in tests/*.rs +#![warn(non_snake_case)] + +pub mod errorfunc; +pub mod errorpanic; + +pub use self::errorfunc::ErrorCall; +pub use self::errorfunc::ErrorFunc; +pub use self::errorfunc::ErrorType; +pub use self::errorpanic::ErrorPanic; diff --git a/tests/strcursor.rs b/tests/strcursor.rs new file mode 100644 index 0000000..3ec876e --- /dev/null +++ b/tests/strcursor.rs @@ -0,0 +1,592 @@ +// Copyright (c) 2021 Soni L. +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +// because we wanna use double underscore (__) for test names +#![allow(non_snake_case)] + +use ::std::io::Cursor; + +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_can_read() { + let mut reader = Cursor::new("abc"); + assert_eq!(reader.can_read(), true); + reader.skip(); // 'a' + assert_eq!(reader.can_read(), true); + reader.skip(); // 'b' + assert_eq!(reader.can_read(), true); + reader.skip(); // 'c' + assert_eq!(reader.can_read(), false); +} + +#[test] +fn test_get_remaining_len() { + let mut reader = Cursor::new("abc"); + assert_eq!(reader.get_remaining().len(), 3); + reader.set_position(1); + assert_eq!(reader.get_remaining().len(), 2); + reader.set_position(2); + assert_eq!(reader.get_remaining().len(), 1); + reader.set_position(3); + assert_eq!(reader.get_remaining().len(), 0); +} + +#[test] +fn test_can_read_n() { + let reader = Cursor::new("abc"); + assert_eq!(reader.can_read_n(1), true); + assert_eq!(reader.can_read_n(2), true); + assert_eq!(reader.can_read_n(3), true); + assert_eq!(reader.can_read_n(4), false); + assert_eq!(reader.can_read_n(5), false); +} + +#[test] +fn test_peek() { + let mut reader = Cursor::new("abc"); + assert_eq!(reader.peek(), 'a'); + assert_eq!(reader.position(), 0); + reader.set_position(2); + assert_eq!(reader.peek(), 'c'); + assert_eq!(reader.position(), 2); +} + +#[test] +fn test_peek_n() { + let mut reader = Cursor::new("abc"); + assert_eq!(reader.peek_n(0), 'a'); + assert_eq!(reader.peek_n(2), 'c'); + assert_eq!(reader.position(), 0); + reader.set_position(1); + assert_eq!(reader.peek_n(1), 'c'); + assert_eq!(reader.position(), 1); +} + +#[test] +fn test_read_char() { + let mut reader = Cursor::new("abc"); + assert_eq!(reader.read_char(), Some('a')); + assert_eq!(reader.read_char(), Some('b')); + assert_eq!(reader.read_char(), Some('c')); + assert_eq!(reader.position(), 3); +} + +#[test] +fn test_skip() { + let mut reader = Cursor::new("abc"); + reader.skip(); + assert_eq!(reader.position(), 1); +} + +#[test] +fn test_get_remaining() { + let mut reader = Cursor::new("Hello!"); + assert_eq!(reader.get_remaining(), "Hello!"); + reader.set_position(3); + assert_eq!(reader.get_remaining(), "lo!"); + reader.set_position(6); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_get_read() { + let mut reader = Cursor::new("Hello!"); + assert_eq!(reader.get_read(), ""); + reader.set_position(3); + assert_eq!(reader.get_read(), "Hel"); + reader.set_position(6); + assert_eq!(reader.get_read(), "Hello!"); +} + +#[test] +fn test_skip_whitespace__none() { + let mut reader = Cursor::new("Hello!"); + reader.skip_whitespace(); + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_skip_whitespace__mixed() { + let mut reader = Cursor::new(" \t \t\nHello!"); + reader.skip_whitespace(); + assert_eq!(reader.position(), 5); +} + +#[test] +fn test_skip_whitespace__empty() { + let mut reader = Cursor::new(""); + reader.skip_whitespace(); + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_unquoted_str() { + let mut reader = Cursor::new("hello world"); + assert_eq!(reader.read_unquoted_str(), "hello"); + assert_eq!(reader.get_read(), "hello"); + assert_eq!(reader.get_remaining(), " world"); +} + +#[test] +fn test_read_unquoted_str__empty() { + let mut reader = Cursor::new(""); + assert_eq!(reader.read_unquoted_str(), ""); + assert_eq!(reader.get_read(), ""); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_unquoted_str__empty_with_remaining() { + let mut reader = Cursor::new(" hello world"); + assert_eq!(reader.read_unquoted_str(), ""); + assert_eq!(reader.get_read(), ""); + assert_eq!(reader.get_remaining(), " hello world"); +} + +#[test] +fn test_read_quoted_string() { + let mut reader = Cursor::new("\"hello world\""); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "hello world", + ); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_quoted_string__single() { + let mut reader = Cursor::new("'hello world'"); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "hello world", + ); + assert_eq!(reader.get_read(), "'hello world'"); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_quoted_string__double_inside_single() { + let mut reader = Cursor::new("'hello \"world\"'"); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "hello \"world\"", + ); + assert_eq!(reader.get_read(), "'hello \"world\"'"); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_quoted_string__single_inside_double() { + let mut reader = Cursor::new("\"hello 'world'\""); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "hello 'world'", + ); + assert_eq!(reader.get_read(), "\"hello 'world'\""); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_quoted_string__empty() { + let mut reader = Cursor::new(""); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "", + ); + assert_eq!(reader.get_read(), ""); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_quoted_string__empty_quoted() { + let mut reader = Cursor::new("\"\""); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "", + ); + assert_eq!(reader.get_read(), "\"\""); + assert_eq!(reader.get_remaining(), ""); +} + + +#[test] +fn test_read_quoted_string__empty_quoted_with_remaining() { + let mut reader = Cursor::new("\"\" hello world"); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "", + ); + assert_eq!(reader.get_read(), "\"\""); + assert_eq!(reader.get_remaining(), " hello world"); +} + +#[test] +fn test_read_quoted_string__with_escaped_quote() { + let mut reader = Cursor::new("\"hello \\\"world\\\"\""); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "hello \"world\"", + ); + assert_eq!(reader.get_read(), "\"hello \\\"world\\\"\""); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_quoted_string__with_escaped_escapes() { + let mut reader = Cursor::new("\"\\\\o/\""); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "\\o/", + ); + assert_eq!(reader.get_read(), "\"\\\\o/\""); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_quoted_string__with_remaining() { + let mut reader = Cursor::new("\"hello world\" foo bar"); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "hello world", + ); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.get_remaining(), " foo bar"); +} + +#[test] +fn test_read_quoted_string__with_immediate_remaining() { + let mut reader = Cursor::new("\"hello world\"foo bar"); + assert_eq!( + reader.read_quoted_string::<ErrorPanic>().unwrap(), + "hello world", + ); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.get_remaining(), "foo bar"); +} + +#[test] +fn test_read_quoted_string__no_open() { + let mut reader = Cursor::new("hello world\""); + assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::ExpectedStartOfQuote); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_quoted_string__no_close() { + let mut reader = Cursor::new("\"hello world"); + assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::ExpectedEndOfQuote); + assert_eq!(context.position(), 12); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_quoted_string__invalid_escape() { + let mut reader = Cursor::new("\"hello\\nworld\""); + assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::InvalidEscape("n")); + // NOTE: brigadier makes this 7. we make this 8. + // FIXME: maybe do the same as brigadier? + assert_eq!(context.position(), 8); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_quoted_string__invalid_quote_escape() { + let mut reader = Cursor::new("'hello\\\"'world"); + assert!(reader.read_quoted_string::<ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::InvalidEscape("\"")); + assert_eq!(context.position(), 8); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_string__no_quotes() { + let mut reader = Cursor::new("hello world"); + assert_eq!( + reader.read_string::<ErrorPanic>().unwrap(), + "hello", + ); + assert_eq!(reader.get_read(), "hello"); + assert_eq!(reader.get_remaining(), " world"); +} + +#[test] +fn test_read_string__single_quotes() { + let mut reader = Cursor::new("'hello world'"); + assert_eq!( + reader.read_string::<ErrorPanic>().unwrap(), + "hello world", + ); + assert_eq!(reader.get_read(), "'hello world'"); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_string__double_quotes() { + let mut reader = Cursor::new("\"hello world\""); + assert_eq!( + reader.read_string::<ErrorPanic>().unwrap(), + "hello world", + ); + assert_eq!(reader.get_read(), "\"hello world\""); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_integer() { + let mut reader = Cursor::new("1234567890"); + assert_eq!( + reader.read_integer::<i32, ErrorPanic>().unwrap(), + 1234567890, + ); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_integer__negative() { + let mut reader = Cursor::new("-1234567890"); + assert_eq!( + reader.read_integer::<i32, ErrorPanic>().unwrap(), + -1234567890, + ); + assert_eq!(reader.get_read(), "-1234567890"); + assert_eq!(reader.get_remaining(), ""); +} + + +#[test] +fn test_read_integer__invalid() { + let mut reader = Cursor::new("12.34"); + assert!(reader.read_integer::<i32, ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::InvalidInteger("12.34")); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_integer__none() { + let mut reader = Cursor::new(""); + assert!(reader.read_integer::<i32, ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::ExpectedInteger); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_integer__with_remaining() { + let mut reader = Cursor::new("1234567890 foo bar"); + assert_eq!( + reader.read_integer::<i32, ErrorPanic>().unwrap(), + 1234567890, + ); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.get_remaining(), " foo bar"); +} + +#[test] +fn test_read_integer__with_remaining_immediate() { + let mut reader = Cursor::new("1234567890foo bar"); + assert_eq!( + reader.read_integer::<i32, ErrorPanic>().unwrap(), + 1234567890, + ); + assert_eq!(reader.get_read(), "1234567890"); + assert_eq!(reader.get_remaining(), "foo bar"); +} + +#[test] +fn test_read_float() { + let mut reader = Cursor::new("123"); + assert_eq!( + reader.read_float::<f32, ErrorPanic>().unwrap(), + 123.0, + ); + assert_eq!(reader.get_read(), "123"); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_float__with_decimal() { + let mut reader = Cursor::new("12.34"); + assert_eq!( + reader.read_float::<f32, ErrorPanic>().unwrap(), + 12.34, + ); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_float__negative() { + let mut reader = Cursor::new("-123"); + assert_eq!( + reader.read_float::<f32, ErrorPanic>().unwrap(), + -123.0, + ); + assert_eq!(reader.get_read(), "-123"); + assert_eq!(reader.get_remaining(), ""); +} + +#[test] +fn test_read_float__invalid() { + let mut reader = Cursor::new("12.34.56"); + assert!(reader.read_float::<f32, ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::InvalidFloat("12.34.56")); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_float__none() { + let mut reader = Cursor::new(""); + assert!(reader.read_float::<f32, ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::ExpectedFloat); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_float__with_remaining() { + let mut reader = Cursor::new("12.34 foo bar"); + assert_eq!( + reader.read_float::<f32, ErrorPanic>().unwrap(), + 12.34, + ); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.get_remaining(), " foo bar"); +} + +#[test] +fn test_read_float__with_remaining_immediate() { + let mut reader = Cursor::new("12.34foo bar"); + assert_eq!( + reader.read_float::<f32, ErrorPanic>().unwrap(), + 12.34, + ); + assert_eq!(reader.get_read(), "12.34"); + assert_eq!(reader.get_remaining(), "foo bar"); +} + +#[test] +fn test_expect__correct() { + let mut reader = Cursor::new("abc"); + assert!(reader.expect::<ErrorPanic>('a').is_ok()); + assert_eq!(reader.position(), 1); +} + +#[test] +fn test_expect__incorrect() { + let mut reader = Cursor::new("bcd"); + assert!(reader.expect::<ErrorCall<ErrFn>>('a').is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::ExpectedSymbol("a")); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_expect__none() { + let mut reader = Cursor::new(""); + assert!(reader.expect::<ErrorCall<ErrFn>>('a').is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::ExpectedSymbol("a")); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_bool__correct() { + let mut reader = Cursor::new("true"); + assert_eq!(reader.read_bool::<ErrorPanic>().unwrap(), true); + assert_eq!(reader.get_read(), "true"); +} + +#[test] +fn test_read_bool__incorrect() { + let mut reader = Cursor::new("tuesday"); + assert!(reader.read_bool::<ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::InvalidBool("tuesday")); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} + +#[test] +fn test_read_bool__none() { + let mut reader = Cursor::new(""); + assert!(reader.read_bool::<ErrorCall<ErrFn>>().is_err()); + struct ErrFn; + impl<'a> ErrorFunc<'a, Cursor<&'a str>> for ErrFn { + fn call(context: &Cursor<&'a str>, ty: ErrorType) { + assert_eq!(ty, ErrorType::ExpectedBool); + assert_eq!(context.position(), 0); + } + } + assert_eq!(reader.position(), 0); +} |