From 3014ba18a73e0f4d386704e36c2c438a60270035 Mon Sep 17 00:00:00 2001 From: Massimiliano Ziccardi Date: Fri, 2 Oct 2020 12:59:14 +0200 Subject: [PATCH] feat: added a new 'requiredBy' rule to configure field dependencies --- src/rules/RequiredByRule.ts | 19 ++++++ src/rules/RuleBuilder.ts | 2 + src/rules/builders/RequiredByRuleBuilder.ts | 16 ++++++ src/rules/index.ts | 2 + test/builder/RequiredBy.test.ts | 64 +++++++++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 src/rules/RequiredByRule.ts create mode 100644 src/rules/builders/RequiredByRuleBuilder.ts create mode 100644 test/builder/RequiredBy.test.ts diff --git a/src/rules/RequiredByRule.ts b/src/rules/RequiredByRule.ts new file mode 100644 index 00000000..eb0ca275 --- /dev/null +++ b/src/rules/RequiredByRule.ts @@ -0,0 +1,19 @@ +import {RuleConfig} from '../config/RuleConfig'; +import {GenericValidator} from './GenericValidator'; +import {Data} from '../Data'; +import {get} from 'lodash'; + +export const rule = (config: RuleConfig) => + new GenericValidator( + config, + (value: string, config: RuleConfig, data: Data) => { + const isEmpty = (val: string): boolean => !val || val.length === 0; + + const parentValue = get(data, config.parent as string) as string; + return isEmpty(parentValue) || !isEmpty(value); + }, + `'%s' is required by ${config.parent}`, + true + ); + +export const NAME = 'REQUIREDBY'; diff --git a/src/rules/RuleBuilder.ts b/src/rules/RuleBuilder.ts index d5006bdf..7fca2f09 100644 --- a/src/rules/RuleBuilder.ts +++ b/src/rules/RuleBuilder.ts @@ -6,6 +6,7 @@ import {builder as LengthRuleBuilder} from './builders/LengthRuleBuilder'; import {builder as MatchesRuleBuilder} from './builders/MatchesRuleBuilder'; import {builder as IsBase64Builder} from './builders/IsBase64Builder'; import {builder as IsValidUrlBuilder} from './builders/IsValidUrlBuilder'; +import {builder as RequiredByBuilder} from './builders/RequiredByRuleBuilder'; // tslint:disable-next-line:variable-name export const RuleBuilder = { @@ -17,4 +18,5 @@ export const RuleBuilder = { matches: MatchesRuleBuilder.withPattern, isBase64: IsBase64Builder, isValidUrl: IsValidUrlBuilder.isValidUrl, + isRequiredBy: RequiredByBuilder, }; diff --git a/src/rules/builders/RequiredByRuleBuilder.ts b/src/rules/builders/RequiredByRuleBuilder.ts new file mode 100644 index 00000000..f63c36e9 --- /dev/null +++ b/src/rules/builders/RequiredByRuleBuilder.ts @@ -0,0 +1,16 @@ +import {RuleConfig} from '../../config/RuleConfig'; +import * as RequiredByRule from '../RequiredByRule'; + +export class Builder { + private readonly config: RuleConfig; + constructor(config: RuleConfig) { + this.config = config; + } + + public readonly build = () => this.config; +} + +export const builder = { + withParent: (parent: string, errorMessage?: string): Builder => + new Builder({type: RequiredByRule.NAME, parent, errorMessage}), +}; diff --git a/src/rules/index.ts b/src/rules/index.ts index 4c03dcda..d3a2ecc3 100644 --- a/src/rules/index.ts +++ b/src/rules/index.ts @@ -4,6 +4,7 @@ import * as RequiredRule from './RequiredRule'; import * as ValidUrlPathRule from './ValidUrlPathRule'; import * as ValidUrlRule from './ValidUrlRule'; import * as LengthRule from './LengthRule'; +import * as RequiredByRule from './RequiredByRule'; import {CompositeRule} from './CompositeRule'; import {RuleConfig} from '../config/RuleConfig'; @@ -15,4 +16,5 @@ export const RULE_DICTIONARY = { [ValidUrlPathRule.NAME]: ValidUrlPathRule.rule, [ValidUrlRule.NAME]: ValidUrlRule.rule, [LengthRule.NAME]: LengthRule.rule, + [RequiredByRule.NAME]: RequiredByRule.rule, }; diff --git a/test/builder/RequiredBy.test.ts b/test/builder/RequiredBy.test.ts new file mode 100644 index 00000000..e6abca6a --- /dev/null +++ b/test/builder/RequiredBy.test.ts @@ -0,0 +1,64 @@ +import {testValidator} from '../utils'; +import {RuleBuilder, Validator, validatorBuilder} from '../../src'; + +const validator: Validator = validatorBuilder() + .newRule() + .withField('webpushVapidPublicKey') + .validate(RuleBuilder.matches('^[A-Za-z0-9_-]*$')) + .withField('webpushVapidPrivateKey') + .validate( + RuleBuilder.isRequiredBy.withParent('webpushVapidPublicKey').build() + ) + .validate(RuleBuilder.matches('^[A-Za-z0-9_-]*$')) + .withField('webpushAlias') + .validate( + RuleBuilder.required() + .withErrorMessage('Please enter a valid URL or mailto address') + .build() + ) + .validate( + RuleBuilder.composite + .any() + .withSubRule( + RuleBuilder.matches( + '^mailto: ?(?:[a-z0-9!#$%&\'*+\\/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&\'*+\\/=?^_`{|}~-]+)*|"(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21\x23-\x5b\x5d-\x7f]|\\\\[\x01-\x09\x0b\x0c\x0e-\x7f])*")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\x01-\x08\x0b\x0c\x0e-\x1f\x21-\x5a\x53-\x7f]|\\\\[\x01-\x09\x0b\x0c\x0e-\x7f])+)\\])$' + ) + ) + .withSubRule( + RuleBuilder.isValidUrl() + .requireTld(true) + .requireHost(true) + .requireProtocol(true) + .build() + ) + .build('Please enter a valid URL or mailto address') + ) + .build(); + +describe('RequiredBy Validator Builder', () => { + it('Only name and alias', () => { + testValidator(validator, { + valid: [{name: 'test', webpushAlias: 'mailto:mziccard@redhat.com'}], + invalid: [{name: 'test'}], + }); + }); + it('Only name and alias and public key', () => { + testValidator(validator, { + valid: [ + { + name: 'test', + webpushAlias: 'mailto:mziccard@redhat.com', + webpushVapidPublicKey: 'aaa', + webpushVapidPrivateKey: 'aaa', + }, + ], + invalid: [ + { + name: 'test', + webpushAlias: 'mailto:mziccard@redhat.com', + webpushVapidPublicKey: 'aaa', + }, + ], + }); + }); +});