Skip to content

Commit

Permalink
Add tests for reader and writer
Browse files Browse the repository at this point in the history
  • Loading branch information
kerristrasz committed Aug 26, 2024
1 parent 4c12260 commit 629ca5e
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 9 deletions.
1 change: 1 addition & 0 deletions src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ pub mod reader;
pub mod writer;

pub use event::{Event, QuoteStyle, Scalar, Tag};
pub use read::Read;
pub use reader::Reader;
pub use writer::Writer;
66 changes: 64 additions & 2 deletions src/stream/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,15 @@ pub enum Event<'a> {
}

/// A string. This is used for both keys and values.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub struct Scalar<'a> {
tag: Option<Tag<'a>>,
value: Cow<'a, str>,
quote_style: QuoteStyle,
}

/// A conditional tag, which may be present in a [`Scalar`] key.
#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug)]
pub struct Tag<'a> {
value: Cow<'a, str>,
quote_style: QuoteStyle,
Expand Down Expand Up @@ -88,21 +88,50 @@ impl<'a> Scalar<'a> {
}

/// Returns the scalar's tag. This only makes sense for keys; values completely ignore this field.
#[inline]
pub fn tag(&self) -> Option<&Tag<'a>> {
self.tag.as_ref()
}

/// Returns the scalar's value.
#[inline]
pub fn value(&self) -> &str {
&self.value
}

/// Returns the scalar's quote style.
#[inline]
pub fn quote_style(&self) -> QuoteStyle {
self.quote_style
}
}

impl<'a> PartialEq for Scalar<'a> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.tag == other.tag && self.value == other.value
}
}

impl<'a> From<Cow<'a, str>> for Scalar<'a> {
fn from(value: Cow<'a, str>) -> Self {
// Safety: Cannot panic unless quote_style is Unquoted.
Scalar::new(None, value, QuoteStyle::Unspecified).unwrap()
}
}

impl<'a> From<&'a str> for Scalar<'a> {
fn from(value: &'a str) -> Self {
Cow::Borrowed(value).into()
}
}

impl<'a> From<String> for Scalar<'a> {
fn from(value: String) -> Self {
Cow::<'a, str>::Owned(value).into()
}
}

impl<'a> Tag<'a> {
/// Creates a new tag. Tags values must begin with `[`, end with `]`, and be at least three
/// characters long.
Expand All @@ -125,16 +154,49 @@ impl<'a> Tag<'a> {
}

/// Returns the tag's value. This includes the enclosing square brackets.
#[inline]
pub fn value(&self) -> &str {
&self.value
}

/// Returns the tag's quote style.
#[inline]
pub fn quote_style(&self) -> QuoteStyle {
self.quote_style
}
}

impl<'a> PartialEq for Tag<'a> {
#[inline]
fn eq(&self, other: &Self) -> bool {
self.value == other.value
}
}

impl<'a> TryFrom<Cow<'a, str>> for Tag<'a> {
type Error = ();

fn try_from(value: Cow<'a, str>) -> Result<Self, Self::Error> {
Self::new(value, QuoteStyle::Unspecified).ok_or(())
}
}

impl<'a> TryFrom<&'a str> for Tag<'a> {
type Error = ();

fn try_from(value: &'a str) -> Result<Self, Self::Error> {
Cow::Borrowed(value).try_into()
}
}

impl<'a> TryFrom<String> for Tag<'a> {
type Error = ();

fn try_from(value: String) -> Result<Self, Self::Error> {
Cow::<'a, str>::Owned(value).try_into()
}
}

fn requires_quotes(value: &str) -> bool {
value.is_empty()
|| value.contains(|c: char| c == '{' || c == '}' || c == '"' || c.is_whitespace())
Expand Down
22 changes: 22 additions & 0 deletions src/stream/read.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@ mod sealed {
/// [`serde_json`]: https://docs.rs/serde_json/latest/serde_json/de/trait.Read.html
#[allow(private_bounds)]
pub trait Read<'a>: sealed::Sealed {
type Inner;

fn peek_char(&mut self, i: usize) -> Result<Option<char>>;

fn next_char(&mut self) -> Result<Option<char>>;

fn parse_str(&mut self) -> Result<Option<Cow<'a, str>>>;

fn into_inner(self) -> Self::Inner;
}

#[derive(Debug)]
Expand All @@ -40,6 +44,8 @@ impl<R: io::Read> IoReader<R> {
impl<R> sealed::Sealed for IoReader<R> {}

impl<'a, R: io::Read> Read<'a> for IoReader<R> {
type Inner = io::Bytes<R>;

fn peek_char(&mut self, i: usize) -> Result<Option<char>> {
todo!()
}
Expand All @@ -51,6 +57,10 @@ impl<'a, R: io::Read> Read<'a> for IoReader<R> {
fn parse_str(&mut self) -> Result<Option<Cow<'a, str>>> {
todo!()
}

fn into_inner(self) -> Self::Inner {
self.bytes
}
}

#[derive(Debug, Clone)]
Expand All @@ -67,6 +77,8 @@ impl<'a> SliceReader<'a> {
impl<'a> sealed::Sealed for SliceReader<'a> {}

impl<'a> Read<'a> for SliceReader<'a> {
type Inner = &'a [u8];

fn peek_char(&mut self, i: usize) -> Result<Option<char>> {
todo!()
}
Expand All @@ -78,6 +90,10 @@ impl<'a> Read<'a> for SliceReader<'a> {
fn parse_str(&mut self) -> Result<Option<Cow<'a, str>>> {
todo!()
}

fn into_inner(self) -> Self::Inner {
self.slice
}
}

#[derive(Debug, Clone)]
Expand All @@ -94,6 +110,8 @@ impl<'a> StrReader<'a> {
impl<'a> sealed::Sealed for StrReader<'a> {}

impl<'a> Read<'a> for StrReader<'a> {
type Inner = &'a str;

fn peek_char(&mut self, i: usize) -> Result<Option<char>> {
todo!()
}
Expand All @@ -105,4 +123,8 @@ impl<'a> Read<'a> for StrReader<'a> {
fn parse_str(&mut self) -> Result<Option<Cow<'a, str>>> {
todo!()
}

fn into_inner(self) -> Self::Inner {
self.str
}
}
76 changes: 73 additions & 3 deletions src/stream/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ impl<R> Reader<R> {

impl<R: io::Read> Reader<read::IoReader<R>> {
/// Creates a KeyValues reader from an [`io::Read`].
///
///
/// This may be less efficient than creating a `Reader` from a [`&[u8]`][Self::from_slice()] or
/// [`&str`][Self::from_str()], as those implementations do not need to buffer input.
pub fn from_reader(reader: R) -> Self {
Expand All @@ -28,7 +28,7 @@ impl<R: io::Read> Reader<read::IoReader<R>> {

impl<'a> Reader<read::SliceReader<'a>> {
/// Creates a KeyValues reader from a `&[u8]`.
///
///
/// If the input is known to be UTF-8, use [`Reader::from_str()`] instead to prevent redundant
/// UTF-8 validation.
pub fn from_slice(slice: &'a [u8]) -> Self {
Expand All @@ -44,7 +44,8 @@ impl<'a> Reader<read::StrReader<'a>> {
}

impl<'a, R: read::Read<'a>> Reader<R> {
/// Reads the next event from the input.
/// Reads the next event from the input. If the end of the file has been reached, each
/// subsequent invocation will return [`Event::EndDocument`].
///
/// # Errors
///
Expand All @@ -54,4 +55,73 @@ impl<'a, R: read::Read<'a>> Reader<R> {
pub fn read(&mut self) -> Result<Event<'a>> {
todo!()
}

/// Unwraps the inner reader from the `Reader`.
pub fn into_inner(self) -> R::Inner {
self.inner.into_inner()
}
}

#[cfg(test)]
mod tests {
use crate::stream::{read, Event, Reader};
use crate::Result;
use indoc::indoc;

const CHILL_ANIMALS: &str = indoc! {r#"
// The comments here are meant to test comment parsing.
// I think these animals are nice!
"ChillAnimals"
{
// These guys just seem happy to exist
"Animal" "Quokka"
"Animal"
// Possums are underrated, by the way
"Possum"
"Animal" "Capybara" // capybaras can coexist with anything
}
"#};

fn simple<'a, R: read::Read<'a>>(reader: &mut Reader<R>) -> Result<()> {
// The whole document is read
assert_eq!(reader.read()?, Event::StartDocument);
assert_eq!(reader.read()?, Event::Scalar("ChillAnimals".into()));
assert_eq!(reader.read()?, Event::StartObject);
assert_eq!(reader.read()?, Event::Scalar("Animal".into()));
assert_eq!(reader.read()?, Event::Scalar("Quokka".into()));
assert_eq!(reader.read()?, Event::Scalar("Animal".into()));
assert_eq!(reader.read()?, Event::Scalar("Possum".into()));
assert_eq!(reader.read()?, Event::Scalar("Animal".into()));
assert_eq!(reader.read()?, Event::Scalar("Capybara".into()));
assert_eq!(reader.read()?, Event::EndObject);
assert_eq!(reader.read()?, Event::EndDocument);

// Subsequent reads should still produce EndDocument
assert_eq!(reader.read()?, Event::EndDocument);
assert_eq!(reader.read()?, Event::EndDocument);
assert_eq!(reader.read()?, Event::EndDocument);

Ok(())
}

#[test]
pub fn simple_str() -> Result<()> {
let mut reader = Reader::from_str(CHILL_ANIMALS);
simple(&mut reader)
}

#[test]
pub fn simple_slice() -> Result<()> {
let mut reader = Reader::from_slice(CHILL_ANIMALS.as_bytes());
simple(&mut reader)
}

#[test]
pub fn simple_reader() -> Result<()> {
let mut reader = Reader::from_reader(CHILL_ANIMALS.as_bytes());
simple(&mut reader)
}
}
59 changes: 55 additions & 4 deletions src/stream/writer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,75 @@ use std::io;
/// A low-level streaming writer that generates text from a sequence of [`Event`]s representing the
/// KeyValues format.
pub struct Writer<W> {
writer: W,
inner: W,
}

impl<W> Writer<W> {
/// Creates a KeyValues writer from an [`io::Write`].
pub fn new(writer: W) -> Self {
Self { writer }
Self { inner: writer }
}
}

impl<W: io::Write> Writer<W> {
/// Writes the `event` to the output.
/// Writes the `event` to the output. After writing [`Event::EndDocument`], subsequent events
/// will be ignored.
///
/// # Errors
///
/// This function returns an error if an `io::Error` occurs. Note that this function does not
/// check semantics (is this a valid document?).
pub fn write(event: Event<'_>) -> Result<()> {
pub fn write(&mut self, _event: Event<'_>) -> Result<()> {
todo!()
}

/// Unwraps the inner writer from the `Writer`.
pub fn into_inner(self) -> W {
self.inner
}
}

#[cfg(test)]
mod tests {
use crate::stream::{Event, Writer};
use crate::Result;
use indoc::indoc;

const CHILL_ANIMALS: &str = indoc! {r#"
"ChillAnimals"
{
"Animal" "Quokka"
"Animal" "Possum"
"Animal" "Capybara"
}
"#};

#[test]
fn simple() -> Result<()> {
let mut writer = Writer::new(Vec::new());

writer.write(Event::StartDocument)?;
writer.write(Event::Scalar("ChillAnimals".into()))?;
writer.write(Event::StartObject)?;
writer.write(Event::Scalar("Animal".into()))?;
writer.write(Event::Scalar("Quokka".into()))?;
writer.write(Event::Scalar("Animal".into()))?;
writer.write(Event::Scalar("Possum".into()))?;
writer.write(Event::Scalar("Animal".into()))?;
writer.write(Event::Scalar("Capybara".into()))?;
writer.write(Event::EndObject)?;
writer.write(Event::EndDocument)?;

// Subsequent writes after EndDocument should be ignored
writer.write(Event::EndDocument)?;
writer.write(Event::EndDocument)?;
writer.write(Event::StartDocument)?;
writer.write(Event::Scalar("yoink".into()))?;
writer.write(Event::EndDocument)?;

let str = String::from_utf8(writer.into_inner()).unwrap();
assert_eq!(str, CHILL_ANIMALS);

Ok(())
}
}

0 comments on commit 629ca5e

Please sign in to comment.