class-validator icon indicating copy to clipboard operation
class-validator copied to clipboard

feature: make `NotMatches()`, a negation of `Matches()`

Open lsloan opened this issue 2 years ago • 2 comments

Description

I'm working on a project where the contents of a field must NOT match a regular expression. Making a negative regular expression to use with Matches() would be too complex.

Proposed solution

Add a negation of Matches() called NotMatches().

lsloan avatar Feb 14 '22 18:02 lsloan

Until this is added to class-validator natively, feel free to use my implementation in class-validator-extended.

pigulla avatar Apr 11 '23 10:04 pigulla

For additional context, in both #331 and #527 (both of which are about the same thing) the rejection reason is "just write the negated regexp". Without context, that'd be a sensible reason with which I'd agree. However, from my (arguably, rich) experience with regular expressions, it is often the case that a "negative" version is hard or even impossible to come by.

The reason is that there is no notion of a string "opposite" to a given one.

For example, if the pattern is /abc/, it matches literally only "abc". That's a very simple pattern and a very simple case. But what would be the opposite pattern? Is it /cba/? Or /xyz/? Or maybe/[^a][^b][^c]/? Most people would say that the "opposite" string would be any string that's not "abc", – but there's no pattern for that. The only way to express "any string other than abc" (with a regex) is:

!input.match(/abc/)

… and the only way currently to make this kind of logic with class-validator is to define a custom @NotMatches(…) decorator:

import { registerDecorator, type ValidationOptions, matches } from 'class-validator'

/** @private */
type ArgsRegExp = [pattern: RegExp, options?: ValidationOptions]

/** @private */
type ArgsString = [pattern: string, modifiers?: string, options?: ValidationOptions]

/** @private */
type Args = ArgsRegExp | ArgsString

/** @private */
interface NotMatchesParams {
  readonly pattern: RegExp
  readonly options: ValidationOptions | undefined
}

/** @private */
function parseParams([arg0, arg1, arg2]: Args): NotMatchesParams {
  if (arg0 instanceof RegExp) {
    return {
      pattern: arg0,
      options: arg1 as ValidationOptions,
    }
  }

  return {
    pattern: new RegExp(arg0, arg1 as string),
    options: arg2,
  }
}

export function NotMatches(...args: ArgsRegExp): PropertyDecorator
export function NotMatches(...args: ArgsString): PropertyDecorator
export function NotMatches(...args: Args): PropertyDecorator {
  const { pattern, options } = parseParams(args)

  return (target, key): void => {
    const propertyName = String(key)

    registerDecorator({
      async: false,
      name: 'notMatches',
      propertyName,
      target: target.constructor,
      options,
      validator: {
        validate(value: unknown) {
          return typeof value === 'string' && !matches(value, pattern)
        },
        defaultMessage() {
          return `${propertyName} must not match ${pattern} regular expression`
        },
      },
    })
  }
}

parzhitsky avatar Oct 24 '23 12:10 parzhitsky