Skip to content
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

Validation for multiple possible values or notValues #716

Open
is-jonreeves opened this issue Jul 14, 2024 · 5 comments
Open

Validation for multiple possible values or notValues #716

is-jonreeves opened this issue Jul 14, 2024 · 5 comments
Assignees
Labels
enhancement New feature or request workaround Workaround fixes problem

Comments

@is-jonreeves
Copy link

I was looking through the validators to see if there was an easy way to blacklist or whitelist a set of strings/numbers (all in one go). I see that .value and .notValue are there, but didn't see any ones that would allow for an array of items to check against.

I'm aware this could be done with a custom .check or .regex, but wondered if it was a common enough usecase where we might consider adding .values and .notValues?

@is-jonreeves
Copy link
Author

On a related note, NaN doesn't play well with .value or .notValue...

This is more of a JS issue though, as NaN === NaN is always false. Dunno if its a scenario worth handling, but had me scratching my head for a moment. I'm just using v.check(i => !Number.isNaN(i), () => I18n.t('validation.notNumber')) instead.

@fabian-hiller
Copy link
Owner

fabian-hiller commented Jul 15, 2024

Thanks for sharing that idea. Yes, we can add values and notValues. Feel free to create a PR. Instead of using values you may want to consider using picklist or literal with union first as it leads to a more precise TypeScript type.

Can you share your entire !NaN schema? v.number() already excludes NaN for you.

@fabian-hiller fabian-hiller self-assigned this Jul 15, 2024
@fabian-hiller fabian-hiller added enhancement New feature or request workaround Workaround fixes problem labels Jul 15, 2024
@is-jonreeves
Copy link
Author

Can you share your entire !NaN schema? v.number() already excludes NaN for you.

This likely occurred for me because I'm also allowing string as a valid input and coercing to number. The intention here was to first conform every valid input to the expected type allowing them to share the same validation logic:

v.pipe(
  // Input
  v.union([
    // Expected (intended "input" type)
    v.number(),
    // Coerced (supported alternative "input" types)
    v.pipe(v.string(), v.trim(), v.transform(i => Number(i || NaN))),
  ]),
  // Validation
  // v.notValue(NaN, () => 'Not a Number'), // <--- couldn't use this
  v.check(i => !Number.isNaN(i), () => 'Not a Number'),
  v.integer(() => 'Not an Integer'),
  v.minValue(1, () => 'Too Small'),
  v.maxValue(65535, () => 'Too Large'),
);

The tricky thing with Number(string) is that an empty string evaluates to 0, so it makes more sense to force blank to NaN. I could instead force to null, but the number validations would then misbehave.

The bigger picture is that I was trying to recreate a Zod usage we had, where we needed "optional numbers", and I recalled it being a bit of a pain... Essentially, a form input (string) where the value should be coerced to a number, is within a range, but can also be optional (with no fallback). Here is an example:

import * as v from 'valibot';

/** Schemas */
const AsNumberSchema = v.union([
  // Expected (intended "input" type)
  v.number(),
  // Coerced (supported alternative "input" types)
  v.pipe(v.string(), v.trim(), v.transform(i => Number(i || NaN))),
]);

const AsUndefinedSchema = v.union([
  // Optional (coercing null as undefined)
  v.pipe(v.null(), v.transform(() => undefined)),
  v.undefined(),
]);

const PortSchema = v.pipe(
  // Supported Types
  AsNumberSchema,
  // Validation
  v.check(i => !Number.isNaN(i), () => 'Not a Number'),
  v.integer(() => 'Not an Integer'),
  v.minValue(1, () => 'Too Small'),
  v.maxValue(65535, () => 'Too Large'),
);

const ConfigurationSchema = v.object({
  port: v.union([ PortSchema, AsUndefinedSchema ]),
});

/** Types */
type ConfigurationSchema = v.InferOutput<typeof ConfigurationSchema>;

/** Debug */
const result = v.safeParse(ConfigurationSchema, { port: undefined }, { abortPipeEarly: true });
console.log(result);

There might be a better (more repeatable) approach to this, as I would also need to do similar string/undefined support for booleans (who don't have an "invalid" state to work with). So maybe using null could make more sense, but I'd need to find a way to restructure it to bypass these validations.

Alternatively, perhaps the better option is to just create a separate dedicated Form/UserInput version of the schemas (that always assumes a string as input) rather than one that caters for everything. I was just trying to avoid repetition, but potentially separating out these concerns makes more sense.

Instead of using values you may want to consider using picklist or literal with union first as it leads to a more precise TypeScript type.

With the above in mind, the scenario I was curious about, was if I needed to exclude a set of reserved ports from those available. If it was always a non-contiguous list then something like .notValues([], ...) would have been handy, but in this case its more likey to be a collection of ranges, so a custom .check probably makes more sense anyways. It just occurred to me that a whitelist/blacklist validation could be handy in other situations.

@fabian-hiller
Copy link
Owner

Yes, let's keep this open. Maybe someone in the community will implement it, or I will do it at a later date.

@EltonLobo07
Copy link
Contributor

@fabian-hiller I'll work on adding values and notValues actions.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request workaround Workaround fixes problem
Projects
None yet
Development

No branches or pull requests

3 participants