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:
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.
...yeah no. I realized way too late that Rust doesn't have linear types, so having a Drop implementation for RubyArrayIter/RubyMapIter that safely skips the iterator on drop is not really possible.
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:
RubyType
be bound to'de: 'deser
,'deser
is not without meritRubyArrayIter
,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 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. ARawRubyType
can be upgraded to aRubyType
by 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
RubyArrayIter
for example:Deserializer#next_element
, because here it's acceptable to bind the lifetime of the returned element to its parentRubyArrayIter
.Drop
implementation would involve callingDeserializer#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:
Drop
implementation forRubyArrayIter
/RubyMapIter
that safely skips the iterator on drop would be possible.Frame
built-in" into the returnedRubyType
(probably through a wrapper and someDeref
/DerefMut
magic), 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.