fabian-hiller / valibot

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

Pipe to emit a single issue #618

Closed IlyaSemenov closed 1 month ago

IlyaSemenov commented 1 month ago

As a developer, I would like to create reusable schemas, presumably with the new pipe function, that only emit a single issue to the parse result.

export function naturalNumber(message = "Natural value expected.") {
  return v.pipe(v.number(message), v.integer(message), v.minValue(1, message))
}

const schema = v.object({
  foo: naturalNumber(),
  bar: naturalNumber(),
  baz: naturalNumber(),
})

console.log(v.flatten(v.safeParse(schema, { foo: -1, bar: "nonsense", baz: -1.5 }).issues!))

delivers:

{
  nested: {
    foo: [ 'Natural value expected.' ],
    bar: [ 'Natural value expected.' ],
    baz: [ 'Natural value expected.', 'Natural value expected.' ]
  }
}

I'd somehow like baz to have only 1 issue. Note that parse(..., { abortPipeEarly: true }) is not really an option, as it's global to the whole parse and also it's controlled by the user (and not by the schema author).

Of course I could always fallback to custom() but then pipe() kinda loses its value.

Some ideas:

// new method:
v.abortEarlyPipe(schema, pipe1, ...)

// pipe config:
v.pipe(schema, pipe1, ..., config?: { abortEarly?: boolean })

// composition of pipe elements:
v.pipe(
  v.number(message),
  v.pipeItem(v.integer(message), v.minValue(1, message)),
  possiblyOtherPipeItemWithOwnMessage()
)
fabian-hiller commented 1 month ago

We could introduce an abortPipeEarly method that overwrites this config for all nested schemas:

export function naturalNumber(message = "Natural value expected.") {
  return v.abortPipeEarly(v.pipe(v.number(message), v.integer(message), v.minValue(1, message)));
}

An alternative could be a general config method that merges the new config into the previously applied config for all nested schemas:

export function naturalNumber(message = "Natural value expected.") {
  return v.config(v.pipe(v.number(message), v.integer(message), v.minValue(1, message)), { abortPipeEarly: true });
}

What do you think is best? Until this functionality is part of the library, you can write this method yourself:

export function abortPipeEarly<TSchema extends v.GenericSchema>(
  schema: TSchema
): TSchema {
  return {
    ...schema,
    _run(dataset, config) {
      return schema._run(dataset, { ...config, abortPipeEarly: true });
    },
  };
}

export function config<TSchema extends v.GenericSchema>(
  schema: TSchema,
  config: Omit<v.Config<v.InferIssue<TSchema>>, 'skipPipe'>
): TSchema {
  return {
    ...schema,
    _run(dataset, config_) {
      return schema._run(dataset, { ...config_, ...config });
    },
  };
}
IlyaSemenov commented 1 month ago

Thanks for confirming that the use case is valid.

I suppose between the two options you suggest, v.config is better as being more flexible (and presumably abortPipeEarly can be then built on top of it).

fabian-hiller commented 1 month ago

v0.31.0-rc.10 with config method is available