fabian-hiller / valibot

The modular and type safe schema library for validating structural data 🤖
https://valibot.dev
MIT License
6.23k stars 201 forks source link

Pipe for actions/checks without schema #835

Open IlyaSemenov opened 1 month ago

IlyaSemenov commented 1 month ago

As a library developer, I want to extend some existing action/check with new behavior. Let's say I want to extend url so that it validates that URL is safe to use in browser links (prevent javascript:alert("pwn3d") alike input).

Is there a way to do so with minimal boilerplate? What I mean is, I can't use something like:

const safeUrl = (message?: string) => v.pipe(v.url(message), v.check(isSafeUrl, message))

because pipe() expects a schema as the first argument. Is there a similar counterpart for actions?

Originally posted by @IlyaSemenov in https://github.com/fabian-hiller/valibot/discussions/833

Approved by @fabian-hiller to create an issue so others can join and discuss.

fabian-hiller commented 1 month ago

Technically, this should be possible somehow, but the bigger question is how to implement it. pipe is a method, and a method usually returns a modified copy of the object passed as its first argument (according to Valibot's mental model). So pipe basically extends the functionality of a schema.

I don't think we should allow adding a pipeline to an action as well, as this will complicate our internal code and probably confuse our users, as they will miss the TS error if they use pipe incorrectly. The best way is probably to introduce a super action (the name is just a first idea) that allows you to combine multiple actions. Here is some sample code:

import * as v from 'valibot';

const safeUrl = (message?: string) => v.superAction(v.url(message), v.check(isSafeUrl, message));

const Schema = v.pipe(v.string(), v.safeUrl('Invalid URL'));

The same behavior can already be achieved with a little more effort by spreading a tuple in a pipeline. Perhaps another idea would be to provide a utility function to just improve the DX for this approach.

import * as v from 'valibot';

function safeUrl<TMessage extends string>(message: TMessage) {
  return [
    v.url<string, TMessage>(message),
    v.check<string, TMessage>(() => true, message),
  ] as const;
}

const Schema = v.pipe(v.string(), ...safeUrl('Schema must be a valid URL'));
IlyaSemenov commented 1 month ago

The same behavior can already be achieved with a little more effort by spreading a tuple in a pipeline.

Not exactly so. In your example, the spread pipe will emit duplicate errors for non-URL strings, because pipes are not abortEarly by default. However, logically (from the schema developer point of view) safeUrl is a single check, not two checks, and as such should report a single error.

In a way, the proposed ’superAction’ could be killing two birds with one stone, allowing both combination of checks for the sake of reuse and also allowing to merge related ad-hoc checks into a single abort-early group for better error reporting (which I've been thinking about for some time).

fabian-hiller commented 1 month ago

Do you have any alternative name ideas for this super action?

IlyaSemenov commented 1 month ago

Not really, at least not something that sounded super good.

Honeslty I was thinking something trivial like pipeActions or mergeActions, but superAction makes not less sense to me.

fabian-hiller commented 1 month ago

Something like combine could work too

fabian-hiller commented 1 week ago

I will probably come back to this when v1 RC or our stable release is out.