-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Flat error representation #67
Conversation
Doesn't seem to be that much of a pain to upgrade, especially since I can just throw the native |
Updated it to use |
Closes #64
This PR changes the error type from an recursive enum to a flat list. Each
Error
has an associatedPath
.The
Path
is implemented as a reverse singly-linked list with reference counted nodes. This ensures thatlist.clone()
isO(1)
and does not allocate, and that concatentation of paths is alsoO(1)
and only allocates space for the new node. These are really important performance guarantees.There is a significant downside, which is that we can only iterate this linked list in reverse. This means that anywhere iteration is needed, such in various
Display
impls, we first have to push all the items onto a separate dynamically allocated array, and only then can we iterate in the correct direction.I opted to use
smallvec::SmallVec
with an inline storage of8
items instead of a plainVec
, as it's unlikely that the extra stack space requirement will be problematic for anyone, and most paths are sure to be under 8 items in length.To get rid of even more allocations, I replaced usage of
Cow<'static, str>
withcompact_str::CompactString
. This means significantly fewer allocations for any field or list index whose representation takes up less than 24 bytes, which as it turns out, is most fields and list indices! In the entire test suite, there is only one field (byte_length_min1_u8_slice
onrules::option::Test
) which takes up more than that.I found that despite the immutability and cheap clone/concatenation,
Path::join
still made up a large chunk of the time spent running validation, which made everything ~25% slower than onmain
. This is becausePath::join
was called eagerly on every nested field,inner::apply
, etc.The solution was to join paths lazily, and cache them. The
crate::util::nested_path
does this by creating a closure which captures anOption<Path>
. TheOption<Path>
is lazily initialized using a parentpath()
(which itself is lazy) and acomponent
, such as an index or a key. This means that by default, the validation logic performs zero allocations, instead trading them for branches. This turned out to be a win in the common case where most data is valid, and a slight loss when it isn't, which is exactly the trade-off we want to make here.Breaking changes
garde::error
has been completely overhauled.garde::Validate
has a newvalidate_into
method, which is the primary entrypoint for custom implementations now.garde::Validate::validate
now has a default implementation which is simply a shorthand for initializing agarde::Report
, passing it intovalidate_into
, and then returning it.New features
You can now easily
select!
errors for specific fields on a givenReport
:Performance
Measured on my M2 Macbook Air
This is in line with what I expected.
Display
became significantly cheaper,validate
is roughly the same.