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

feat: implement requiredIf rules #42

Merged
merged 4 commits into from
Mar 13, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 126 additions & 4 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,6 +88,7 @@ class NullableModifier<Schema extends BaseModifiersType<any, any>> extends BaseM
Schema[typeof COTYPE] | null
> {
#parent: Schema

constructor(parent: Schema) {
super()
this.#parent = parent
Expand Down Expand Up @@ -116,17 +120,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) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe it would be better to directly use structuredClone()?

return structuredClone(this.validations)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ohh did not know about that. Will do in a separate PR

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 +256,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
Loading