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.
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 :
and this is how it is used:
This implementation has some limitations which are useful for my use-case:
Generic iterator to cover more generic use-cases should be a bit more complex:
Itemshould still beResult<T, IterError>.IterErrorshould 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:
A few open questions to settle with this approach:
has_next()returned false, but not due to seeing a closing delimiter, but something else weird), I think it should.next()should return?NoneorSome(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.
I am not completely sure whether adding this functionality is a good idea. My biggest concern is that this makes it easy to accidentally discard errors or not abort on them, in which case the Struson behavior is unspecified.
Especially methods like
Iterator::skiporIterator::lastseem problematic. Dropping the iterator before exhausting it would also be error-prone, but then the behavior is at least not unspecified; most likely the user would get (confusing) errors when they are unexpectedly still inside the array. (Skipping the remaining elements onDropis something I would like to avoid, due to being unable to properly handle errors.)Could you please describe your use case a bit more…