davidmdm / myzod

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Move predicate to the Type level

pierreis opened this issue · comments

Currently, predicates are handled at the String level. Is there any specific reason not to handle these at the Type level, so they can be used to validate any type?

Hey @pierreis,

The StringType predicate is an option, and will then be called within the parse/try if present. I don’t really think it can be set at the Type level.

For many types it doesn’t really make sense to have a predicate function option. I’m thinking of notably, boolean, undefined, null, literal, enum, number and bigint.

I thought that it was important for string to have a predicate function because I didn’t want to reimplement isUUID or isEmail, or introduce a dependency.

Which other types would benefit from having a predicate function option? It’s pretty low effort for me to add it to another type. So if you can highlight some use cases I have no problem doing it!

Hey -- I think that any non-trivial type could benefit from this. While I agree that it doesn't make much sense for boolean, undefined, null, enum or literal, it could make sense for virtually all the rest (numbers included):

– On numeric types, check that the number is odd, even, a power of two, ...
– On object, check that complex field dependencies are satisfied (a password equals its confirmation, exactly one of [A, B or C keys are defined, etc]
– On array, check that the number of elements is odd, even, ...
– On date, check that it does fall on a weekday, that the time is within working hours, ...
– On a tuple, check that the elements are related in some way, ...

This is IMO so useful across the board that the cases for which it does not make sense are outnumbered by these for which it does. Which is why the request relates to moving it to the type level.

@pierreis,

Good points. For what you are describing though, I feel like I may even need to upgrade the predicate... I wrote StringType.predicate as a one-off because I knew it would be useful and because I knew that should it pass it would return a value of type string.

Ok. I think I will start work on this. I am going to ask your opinion on an api change I've been considering for the predicate function. You could easily imagine a scenario where you have more than one predicate and you would want different error messages associated to each condition.

Would you like to see the api to support something like this?

schema
  .addPredicate(condFunc1, 'condition 1 failed')
  .addPredicate(condFunc2, 'condition 2 failed'); 

Sounds good. Two things I would add here:

First, the error message should be either a string or a function taking the object as parameter and returning a string. That would enable more descriptive error messages.

Second, in order to simplify the whole logic, I would actually suggest to switch the implementation of built-in functions min, max, ... as predicates. This would probably make the parser a tad simpler, and re-use the same logic for error processing in both cases.

The two would then be identical:

numberSchema.min(5, "number must be greater than 5")
numberSchema.addPredicate(minValue(5), "number must be greater than 5")

That is interesting. But would myzod provide minValue predicates? I think for now I will keep the default min/max/length/unique validators as they apply to specific types.

I will start by enabling multi-predicates in String, and then port this behaviour over to the other types that warrant it.

Sounds good.

I think the predicates could be internal functions not exposed as part of the external API. The goal here is not necessarily to increase the API size, more to consolidate its internal implementation.

I am not suggesting to implement it this way, but technically, one could see the whole normalization process as a series of steps Array<(obj: unknown) => T | undefined that throws on error. This caters for everything, including type casting, type fitting and predicate processing, and make the actual core parser merely a middleware stack.

It just clicked. Yeah I think that is a really good idea. I think I will move in that direction incrementally.
it would allow me to easily accept user defined error message for min/max which I currently do not do by piggybacking off of what I do for predicates.

Then any type that have myzod provided validators (min/max/etc) would by default support the predicates API.

@pierreis I have started a new branch where I have replaced the old predicate API with withPredicate for StringType. I opened a PR but was not able to add you as a reviewer...

Anyways here is the branch: https://github.com/davidmdm/myzod/tree/withPredicate-api

I was wondering if you had any feedback?

I won't merge until I port it to the other types.

Looks good.

Made a comment about implementation simplification ; I don't think predicates should be either an array or a single predicate that can itself take multiple shapes. It makes the code harder to follow IMO.
In addition, I think it could also hinder performance by adding unnecessary runtime checks in potentially tight loops and not leveraging JIT compilation as we otherwise could.

Instead of manually adding predicates to each types, why not consolidate this in a common abstract class from which all types accepting predicates would inherit?

@pierreis,
It is a little annoying to do it manually but the types already inherit from Type, and I don't want to have it definied on Type because I do not want it as part of the API for all types.

I don't mind doing it manually once. If I see that the api constantly needs changing and I should create some abstraction I will do it. In the meantime I don't know what the right abstraction would be so I am not doing it.

I am also not a fan of inheritance in general.

If I wanted to use inheritance I would need to redefine my current parse methods as _internalParse so the wrapper could call it, than apply predicates after, which would be a lot of changes that I am not sure I want.

Also it's not long for me to add the boilerplate needed as I have already abstracted predicate handling with normalizePredicates and appendPredicate. What is long is writing the tests, the builtin validators and updating the docs.

TLDR: might abstract further later, right now unsure what that would look like or if I want to do it

Got it, thanks for the update 🙂
Your call.

Note that I don’t necessarily agree that doing it on a base class would impact all types ; I was proposing some abstraction of the type:

Type -> PredicatableType -> String

This would obviate the need for code duplication, while isolating predicate handling a layer higher than the base class.

@pierreis Thanks for all your help and for this issue. I think this has really added some value to myzod.

The StringType.predicate is gone and has been replaced by the new withPredicate API that supports multiple predicates, custom error message and functions that return custom messages.

The types that support withPredicate are: string, number, bigint, date, object, array and tuple.

I am going to close the issue now, and the code has been released to myzod v1.0.0-alpha.6

Hopefully we will have version 1.0.0 soon!
thanks again.