Skip to content

Commit

Permalink
feat: implement requiredIf rules
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Mar 11, 2024
1 parent 81beff7 commit 0737dcf
Show file tree
Hide file tree
Showing 5 changed files with 541 additions and 8 deletions.
138 changes: 133 additions & 5 deletions src/schema/base/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,13 @@ import type {
Validation,
RuleBuilder,
Transformer,
FieldContext,
FieldOptions,
ParserOptions,
ConstructableSchema,
} from '../../types.js'
import { requiredWhen } from './rules.js'
import { helpers } from '../../vine/helpers.js'

/**
* Base schema type with only modifiers applicable on all the schema types.
Expand Down Expand Up @@ -51,8 +54,8 @@ abstract class BaseModifiersType<Output, CamelCaseOutput>
* Mark the field under validation as optional. An optional
* field allows both null and undefined values.
*/
optional(): OptionalModifier<this> {
return new OptionalModifier(this)
optional(validations?: Validation<any>[]): OptionalModifier<this> {
return new OptionalModifier(this, validations)
}

/**
Expand Down Expand Up @@ -85,9 +88,16 @@ class NullableModifier<Schema extends BaseModifiersType<any, any>> extends BaseM
Schema[typeof COTYPE] | null
> {
#parent: Schema
constructor(parent: Schema) {

/**
* Optional modifier validations list
*/
validations: Validation<any>[]

constructor(parent: Schema, validations?: Validation<any>[]) {
super()
this.#parent = parent
this.validations = validations || []
}

/**
Expand Down Expand Up @@ -116,17 +126,134 @@ class OptionalModifier<Schema extends BaseModifiersType<any, any>> extends BaseM
Schema[typeof COTYPE] | undefined
> {
#parent: Schema
constructor(parent: Schema) {

/**
* Optional modifier validations list
*/
validations: Validation<any>[]

constructor(parent: Schema, validations?: Validation<any>[]) {
super()
this.#parent = parent
this.validations = validations || []
}

/**
* Shallow clones the validations. Since, there are no API's to mutate
* the validation options, we can safely copy them by reference.
*/
protected cloneValidations(): Validation<any>[] {
return this.validations.map((validation) => {
return {
options: validation.options,
rule: validation.rule,
}
})
}

/**
* Compiles validations
*/
protected compileValidations(refs: RefsStore) {
return this.validations.map((validation) => {
return {
ruleFnId: refs.track({
validator: validation.rule.validator,
options: validation.options,
}),
implicit: validation.rule.implicit,
isAsync: validation.rule.isAsync,
}
})
}

/**
* Push a validation to the validations chain.
*/
use(validation: Validation<any> | RuleBuilder): this {
this.validations.push(VALIDATION in validation ? validation[VALIDATION]() : validation)
return this
}

/**
* Define a callback to conditionally require a field at
* runtime.
*
* The callback method should return "true" to mark the
* field as required, or "false" to skip the required
* validation
*/
requiredWhen(callback: (field: FieldContext) => boolean) {
return this.use(requiredWhen(callback))
}

/**
* Mark the field under validation as required when all
* the other fields are present with value other
* than `undefined` or `null`.
*/
requiredIfExists(fields: string | string[]) {
const fieldsToExist = Array.isArray(fields) ? fields : [fields]
return this.use(
requiredWhen((field) => {
return fieldsToExist.every((otherField) =>
helpers.exists(helpers.getNestedValue(otherField, field))
)
})
)
}

/**
* Mark the field under validation as required when any
* one of the other fields are present with non-nullable
* value.
*/
requiredIfExistsAny(fields: string[]) {
return this.use(
requiredWhen((field) => {
return fields.some((otherField) =>
helpers.exists(helpers.getNestedValue(otherField, field))
)
})
)
}

/**
* Mark the field under validation as required when all
* the other fields are missing or their value is
* `undefined` or `null`.
*/
requiredIfMissing(fields: string | string[]) {
const fieldsToExist = Array.isArray(fields) ? fields : [fields]
return this.use(
requiredWhen((field) => {
return fieldsToExist.every((otherField) =>
helpers.isMissing(helpers.getNestedValue(otherField, field))
)
})
)
}

/**
* Mark the field under validation as required when any
* one of the other fields are missing.
*/
requiredIfMissingAny(fields: string[]) {
return this.use(
requiredWhen((field) => {
return fields.some((otherField) =>
helpers.isMissing(helpers.getNestedValue(otherField, field))
)
})
)
}

/**
* Creates a fresh instance of the underlying schema type
* and wraps it inside the optional modifier
*/
clone(): this {
return new OptionalModifier(this.#parent.clone()) as this
return new OptionalModifier(this.#parent.clone(), this.cloneValidations()) as this
}

/**
Expand All @@ -135,6 +262,7 @@ class OptionalModifier<Schema extends BaseModifiersType<any, any>> extends BaseM
[PARSE](propertyName: string, refs: RefsStore, options: ParserOptions): LiteralNode {
const output = this.#parent[PARSE](propertyName, refs, options)
output.isOptional = true
output.validations = output.validations.concat(this.compileValidations(refs))
return output
}
}
Expand Down
28 changes: 28 additions & 0 deletions src/schema/base/rules.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* @vinejs/vine
*
* (c) VineJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

import { messages } from '../../defaults.js'
import type { FieldContext } from '../../types.js'
import { createRule } from '../../vine/create_rule.js'

/**
* Validates the value to be required when a certain condition
* is matched
*/
export const requiredWhen = createRule<(field: FieldContext) => boolean>(
(_, checker, field) => {
const shouldBeRequired = checker(field)
if (!field.isDefined && shouldBeRequired) {
field.report(messages.required, 'required', field)
}
},
{
implicit: true,
}
)
Loading

0 comments on commit 0737dcf

Please sign in to comment.