Skip to content

Expose iterators for JSON collections with uniform data types #171

Description

@DarkFenX

This suggestion relies on having errors classified between recoverable and non-recoverable, as well as having handling of recoverable errors in the lib.


When you have a collection (array, object) where values are of consistent type, it would be convenient to have an iterator interface over those. In my experience it is a relatively typical use-case for JSON reading/writing.

To demonstrate what I mean, this is a simple iterator over arrays using the low-level API I've built for my project :

use struson::reader::{JsonReader, JsonStreamReader};

use crate::phb::parsing::ReadParseFailReason;

pub(in crate::phb) struct ArrayIter<T, R>
where
    T: serde::de::DeserializeOwned,
    R: std::io::Read,
{
    reader: JsonStreamReader<R>,
    opened: bool,
    closed: bool,
    phantom: std::marker::PhantomData<T>,
}
impl<T, R> ArrayIter<T, R>
where
    T: serde::de::DeserializeOwned,
    R: std::io::Read,
{
    pub(in crate::phb) fn new(reader: R) -> Self {
        Self {
            reader: JsonStreamReader::new(reader),
            opened: false,
            closed: false,
            phantom: Default::default(),
        }
    }
}
impl<T, R> Iterator for ArrayIter<T, R>
where
    T: serde::de::DeserializeOwned,
    R: std::io::Read,
{
    type Item = Result<T, ReadParseFailReason>;

    fn next(&mut self) -> Option<Self::Item> {
        if self.closed {
            return None;
        }
        if !self.opened {
            if let Err(error) = self.reader.begin_array() {
                return Some(Err(error.into()));
            }
            self.opened = true;
        }
        loop {
            match self.reader.has_next() {
                Ok(has_next) => match has_next {
                    true => match self.reader.deserialize_next::<serde_json::Value>() {
                        Ok(raw_value) => match serde_json::from_value::<T>(raw_value) {
                            Ok(value) => return Some(Ok(value)),
                            // Silently skip malformed entries
                            Err(_) => continue,
                        },
                        Err(error) => return Some(Err(error.into())),
                    },
                    false => break,
                },
                Err(e) => return Some(Err(e.into())),
            }
        }
        if !self.closed {
            if let Err(error) = self.reader.end_array() {
                return Some(Err(error.into()));
            }
            self.closed = true;
        }
        None
    }
}

and this is how it is used:

    fn get_data_version(&self) -> Result<String, Box<dyn std::error::Error>> {
        let addr = Address::new("phobos", "metadata");
        let reader = self.get_reader(&addr)?;
        for metadata_result in ArrayIter::new(reader) {
            let metadata: PMetadata =
                metadata_result.map_err(|e| PhbFileEdhError::from_read_parse(e, addr.get_part_str()))?;
            if metadata.field_name == "client_build" {
                return Ok(metadata.field_value.to_string());
            }
        }
        Err(PhbFileEdhError::NoClientBuild.into())
    }

This implementation has some limitations which are useful for my use-case:

  • it is not generic over type-stored-in-the-collection, or over collection type, which makes it much less complicated
  • it does not parse directly into T, so does not need failure recovery code (but pays with performance for that)
  • it skips serde parse errors (the only recoverable error here) silently; any error returned is an unrecoverable error, which makes error handling super easy on caller side

Generic iterator to cover more generic use-cases should be a bit more complex:

  • iterators could be generic over type-stored-in-the-collection (u64 / serde-backed deserializable / string etc), or maybe even over collection type (but my guess is that it'd be tricky to implement, since processing JSON objects should yield some errors you will never see when working with JSON arrays)
  • iterator's Item should still be Result<T, IterError>. IterError should provide a way for user to see if error is unrecoverable (iteration cannot proceed), or is recoverable (calling next() another time might yield an item)

Basic flow is:

  • read opening delimiter: if any error, it is unrecoverable
  • read a value (and its name if iterating over JSON object), and parse it using serde if necessary:
    • in case of various read errors, unrecoverable error is returned
    • if value is not what iterator expected it to be (e.g. it expected a number but read not a number; or it expected some data to be deserializable into T, but got something else), error recovery code is ran, and error of recoverable kind is returned, to notify that an item is of unexpected type. For arrays, it can mention index; for objects, errors should be different for field names and field values - if field name was not string, it returns one error; if value is of unexpected kind, it exposes field name under which unexpected object resides
  • read closing delimiter: if any error, it is unrecoverable

A few open questions to settle with this approach:

  • if reading of closing delimiter failed, should it really be unrecoverable error? If in some cases it might signify that data stream was corrupted (has_next() returned false, but not due to seeing a closing delimiter, but something else weird), I think it should.
  • if an unrecoverable error occurs, what consequent calls to next() should return? None or Some(Err(unrecoverable))?

I don't have any strong opinions on those.

But overall, just having an iterator, and classification of errors gives me enough flexibility to decide if I want to break iteration, or skip malformed elements in calling code, and it is way better than current API for uniform collections imo.

Pinned by Marcono1234

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions