So I think I've had an idea to make possible having the deserializer be built-in into RubyType that sidesteps the double mutable borrow issues I had with it (see https://discord.com/channels/273534239310479360/1058530213585236019/1058758259428839425 on the Rust community Discord server).
Really the problem with the approach I took before is two-fold:
- On one hand, there's value in providing an ergonomic API to deserialize elements taking advantage of lifetimes, so having the
RubyType be bound to 'de: 'deser, 'deser is not without merit
- On the other hand, the internal facilities (such as
RubyArrayIter, RubyMapIter that use it (by having a &'deser mut Deserializer<'de> field) cannot really use any function that returnsRubyType<'de, 'deser> without binding the lifetime of & (mut) self to 'deser, which makes a lot of design space just impossible to explore; for example, Drop implementations.
However, this week I thought about it and I think there's a way to solve that particular problem. For this to work, I think you need both a RubyType<'de, 'deser> and a RubyType<'de> we will call the latter "RawRubyType<'de>".
RubyType<'de, 'deser> is what end-users use. There is no footguns here, and this would be the most ergonomic way to perform deserialization.
RawRubyType<'de> is what internal facilities use. There are some footguns to keep in mind when using this, but it allows the most fine control. A RawRubyType can be upgraded to a RubyType by providing a deserializer.
What this implies is that you have two ways of getting the next element to deserialize:
-
fn next_element<'deser>(&'deser mut self) -> RubyType<'de, 'deser>
-
fn next_raw_element(&mut self) -> RawRubyType<'de>
And implementations can mix and match depending on what they do. Consider RubyArrayIter for example:
- The public API to advance to the next element would be a call to
Deserializer#next_element, because here it's acceptable to bind the lifetime of the returned element to its parent RubyArrayIter.
- Its
Drop implementation would involve calling Deserializer#next_raw_element in a loop until the iterator is exhausted. I mean, you aren't really passing those elements up to the end-user; in fact the whole point is to drop them!
I think that with this I'd be able to solve all footguns the library has so far:
- Having a
Drop implementation for RubyArrayIter/RubyMapIter that safely skips the iterator on drop would be possible.
- You could go so far as to "make the
Frame built-in" into the returned RubyType (probably through a wrapper and some Deref/DerefMut magic), and therefore get rid of let mut deserializer = deserializer.prepare()? entirely, which right now I believe is a footgun because for it to work correctly you really want to call it every time you take the next element.
So I think I've had an idea to make possible having the deserializer be built-in into
RubyTypethat sidesteps the double mutable borrow issues I had with it (see https://discord.com/channels/273534239310479360/1058530213585236019/1058758259428839425 on the Rust community Discord server).Really the problem with the approach I took before is two-fold:
RubyTypebe bound to'de: 'deser,'deseris not without meritRubyArrayIter,RubyMapIterthat use it (by having a&'deser mut Deserializer<'de>field) cannot really use any function that returnsRubyType<'de, 'deser>without binding the lifetime of& (mut) selfto'deser, which makes a lot of design space just impossible to explore; for example,Dropimplementations.However, this week I thought about it and I think there's a way to solve that particular problem. For this to work, I think you need both a
RubyType<'de, 'deser>and aRubyType<'de>we will call the latter "RawRubyType<'de>".RubyType<'de, 'deser>is what end-users use. There is no footguns here, and this would be the most ergonomic way to perform deserialization.RawRubyType<'de>is what internal facilities use. There are some footguns to keep in mind when using this, but it allows the most fine control. ARawRubyTypecan be upgraded to aRubyTypeby providing a deserializer.What this implies is that you have two ways of getting the next element to deserialize:
And implementations can mix and match depending on what they do. Consider
RubyArrayIterfor example:Deserializer#next_element, because here it's acceptable to bind the lifetime of the returned element to its parentRubyArrayIter.Dropimplementation would involve callingDeserializer#next_raw_elementin a loop until the iterator is exhausted. I mean, you aren't really passing those elements up to the end-user; in fact the whole point is to drop them!I think that with this I'd be able to solve all footguns the library has so far:
Dropimplementation forRubyArrayIter/RubyMapIterthat safely skips the iterator on drop would be possible.Framebuilt-in" into the returnedRubyType(probably through a wrapper and someDeref/DerefMutmagic), and therefore get rid oflet mut deserializer = deserializer.prepare()?entirely, which right now I believe is a footgun because for it to work correctly you really want to call it every time you take the next element.