Skip to content

Commit

Permalink
feat: add support for running implicit validations on array and objects
Browse files Browse the repository at this point in the history
  • Loading branch information
thetutlage committed Nov 30, 2024
1 parent abe7375 commit 1257d44
Show file tree
Hide file tree
Showing 14 changed files with 629 additions and 118 deletions.
56 changes: 32 additions & 24 deletions src/compiler/nodes/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { defineFieldValidations } from '../../scripts/field/validations.js'
import type { CompilerField, CompilerParent, ArrayNode } from '../../types.js'
import { defineArrayInitialOutput } from '../../scripts/array/initial_output.js'
import { defineFieldExistenceValidations } from '../../scripts/field/existence_validations.js'
import { defineArrayVariables } from '../../scripts/array/variables.js'

/**
* Compiles an array schema node to JS string output.
Expand Down Expand Up @@ -84,43 +85,50 @@ export class ArrayNodeCompiler extends BaseNode {
)

/**
* Wrapping initialization of output + array elements
* validation inside `if array field is valid` block.
*
* Pre step: 3
* Step 3: Define the code to validate the field is an array
*/
const isArrayValidBlock = defineIsValidGuard({
variableName: this.field.variableName,
bail: this.#node.bail,
guardedCodeSnippet: `${defineArrayInitialOutput({
this.#buffer.writeStatement(
defineArrayVariables({
variableName: this.field.variableName,
outputExpression: this.field.outputExpression,
outputValueExpression: `[]`,
})}${this.#buffer.newLine}${this.#compileArrayElements()}`,
})
})
)

/**
* Wrapping field validations + "isArrayValidBlock" inside
* `if value is array` check.
*
* Pre step: 3
* Step 4: Execute array validations
*/
const isValueAnArrayBlock = defineArrayGuard({
variableName: this.field.variableName,
guardedCodeSnippet: `${defineFieldValidations({
this.#buffer.writeStatement(
defineFieldValidations({
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
dropMissingCheck: true,
})}${this.#buffer.newLine}${isArrayValidBlock}`,
dropMissingCheck: false,
existenceCheckExpression: `${this.field.variableName}_is_array`,
})
)

/**
* Step 5: If value is an array and array is valid, then
* we must validate the children and write the output
*/
const isArrayValidBlock = defineArrayGuard({
variableName: this.field.variableName,
guardedCodeSnippet: `${this.#buffer.newLine}${defineIsValidGuard({
variableName: this.field.variableName,
bail: this.#node.bail,
guardedCodeSnippet: `${defineArrayInitialOutput({
variableName: this.field.variableName,
outputExpression: this.field.outputExpression,
outputValueExpression: `[]`,
})}${this.#buffer.newLine}${this.#compileArrayElements()}`,
})}`,
})

/**
* Step 3: Define `if value is an array` block and `else if value is null`
* block.
* Step 6: Define `if value is an array + valid` block
* `else if value is null` block.
*/
this.#buffer.writeStatement(
`${isValueAnArrayBlock}${this.#buffer.newLine}${defineFieldNullOutput({
`${isArrayValidBlock}${this.#buffer.newLine}${defineFieldNullOutput({
allowNull: this.#node.allowNull,
outputExpression: this.field.outputExpression,
variableName: this.field.variableName,
Expand Down
36 changes: 27 additions & 9 deletions src/compiler/nodes/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { defineObjectInitialOutput } from '../../scripts/object/initial_output.j
import { defineMoveProperties } from '../../scripts/object/move_unknown_properties.js'
import { defineFieldExistenceValidations } from '../../scripts/field/existence_validations.js'
import type { CompilerField, CompilerParent, ObjectNode, ObjectGroupNode } from '../../types.js'
import { defineObjectVariables } from '../../scripts/object/variables.js'

/**
* Compiles an object schema node to JS string output.
Expand Down Expand Up @@ -148,11 +149,33 @@ export class ObjectNodeCompiler extends BaseNode {
})
)

/**
* Step 3: Define the code to validate the field is an object
*/
this.#buffer.writeStatement(
defineObjectVariables({
variableName: this.field.variableName,
})
)

/**
* Step 4: Execute object validations
*/
this.#buffer.writeStatement(
defineFieldValidations({
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
dropMissingCheck: false,
existenceCheckExpression: `${this.field.variableName}_is_object`,
})
)

/**
* Wrapping initialization of output + object children validations
* validation inside `if object field is valid` block.
*
* Pre step: 3
* Pre step: 5
*/
const isObjectValidBlock = defineIsValidGuard({
variableName: this.field.variableName,
Expand All @@ -174,20 +197,15 @@ export class ObjectNodeCompiler extends BaseNode {
* Wrapping field validations + "isObjectValidBlock" inside
* `if value is object` check.
*
* Pre step: 3
* Pre step: 5
*/
const isValueAnObject = defineObjectGuard({
variableName: this.field.variableName,
guardedCodeSnippet: `${defineFieldValidations({
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
dropMissingCheck: true,
})}${isObjectValidBlock}`,
guardedCodeSnippet: `${isObjectValidBlock}`,
})

/**
* Step 3: Define `if value is an object` block and `else if value is null`
* Step 5: Define `if value is an object` block and `else if value is null`
* block.
*/
this.#buffer.writeStatement(
Expand Down
30 changes: 24 additions & 6 deletions src/compiler/nodes/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { defineFieldValidations } from '../../scripts/field/validations.js'
import type { CompilerField, CompilerParent, RecordNode } from '../../types.js'
import { defineObjectInitialOutput } from '../../scripts/object/initial_output.js'
import { defineFieldExistenceValidations } from '../../scripts/field/existence_validations.js'
import { defineObjectVariables } from '../../scripts/object/variables.js'

/**
* Compiles a record schema node to JS string output.
Expand Down Expand Up @@ -83,6 +84,28 @@ export class RecordNodeCompiler extends BaseNode {
})
)

/**
* Step 3: Define the code to validate the field is an object
*/
this.#buffer.writeStatement(
defineObjectVariables({
variableName: this.field.variableName,
})
)

/**
* Step 4: Execute object validations
*/
this.#buffer.writeStatement(
defineFieldValidations({
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
dropMissingCheck: false,
existenceCheckExpression: `${this.field.variableName}_is_object`,
})
)

/**
* Wrapping initialization of output + tuple validation + array elements
* validation inside `if array field is valid` block.
Expand All @@ -107,12 +130,7 @@ export class RecordNodeCompiler extends BaseNode {
*/
const isValueAnObjectBlock = defineObjectGuard({
variableName: this.field.variableName,
guardedCodeSnippet: `${defineFieldValidations({
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
dropMissingCheck: true,
})}${this.#buffer.newLine}${isObjectValidBlock}`,
guardedCodeSnippet: `${this.#buffer.newLine}${isObjectValidBlock}`,
})

/**
Expand Down
60 changes: 34 additions & 26 deletions src/compiler/nodes/tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { defineFieldValidations } from '../../scripts/field/validations.js'
import type { CompilerField, CompilerParent, TupleNode } from '../../types.js'
import { defineArrayInitialOutput } from '../../scripts/array/initial_output.js'
import { defineFieldExistenceValidations } from '../../scripts/field/existence_validations.js'
import { defineArrayVariables } from '../../scripts/array/variables.js'

/**
* Compiles a tuple schema node to JS string output.
Expand Down Expand Up @@ -77,45 +78,52 @@ export class TupleNodeCompiler extends BaseNode {
)

/**
* Wrapping initialization of output + tuple validation
* validation inside `if array field is valid` block.
*
* Pre step: 3
* Step 3: Define the code to validate the field is an array
*/
const isArrayValidBlock = defineIsValidGuard({
variableName: this.field.variableName,
bail: this.#node.bail,
guardedCodeSnippet: `${defineArrayInitialOutput({
this.#buffer.writeStatement(
defineArrayVariables({
variableName: this.field.variableName,
outputExpression: this.field.outputExpression,
outputValueExpression: this.#node.allowUnknownProperties
? `copyProperties(${this.field.variableName}.value)`
: `[]`,
})}${this.#compileTupleChildren()}`,
})
})
)

/**
* Wrapping field validations + "isArrayValidBlock" inside
* `if value is array` check.
*
* Pre step: 3
* Step 4: Execute tuple (aka array) validations
*/
const isValueAnArrayBlock = defineArrayGuard({
variableName: this.field.variableName,
guardedCodeSnippet: `${defineFieldValidations({
this.#buffer.writeStatement(
defineFieldValidations({
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
dropMissingCheck: true,
})}${this.#buffer.newLine}${isArrayValidBlock}`,
dropMissingCheck: false,
existenceCheckExpression: `${this.field.variableName}_is_array`,
})
)

/**
* Step 5: If value is an array and array is valid, then
* we must validate the children and write the output
*/
const isArrayValidBlock = defineArrayGuard({
variableName: this.field.variableName,
guardedCodeSnippet: `${this.#buffer.newLine}${defineIsValidGuard({
variableName: this.field.variableName,
bail: this.#node.bail,
guardedCodeSnippet: `${defineArrayInitialOutput({
variableName: this.field.variableName,
outputExpression: this.field.outputExpression,
outputValueExpression: this.#node.allowUnknownProperties
? `copyProperties(${this.field.variableName}.value)`
: `[]`,
})}${this.#buffer.newLine}${this.#compileTupleChildren()}`,
})}`,
})

/**
* Step 3: Define `if value is an array` block and `else if value is null`
* block.
* Step 6: Define `if value is an array + valid` block
* `else if value is null` block.
*/
this.#buffer.writeStatement(
`${isValueAnArrayBlock}${this.#buffer.newLine}${defineFieldNullOutput({
`${isArrayValidBlock}${this.#buffer.newLine}${defineFieldNullOutput({
allowNull: this.#node.allowNull,
outputExpression: this.field.outputExpression,
variableName: this.field.variableName,
Expand Down
2 changes: 1 addition & 1 deletion src/scripts/array/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ArrayGuardOptions = {
* Returns JS fragment to wrap code inside an array conditional
*/
export function defineArrayGuard({ variableName, guardedCodeSnippet }: ArrayGuardOptions) {
return `if (ensureIsArray(${variableName})) {
return `if (${variableName}_is_array) {
${guardedCodeSnippet}
}`
}
19 changes: 19 additions & 0 deletions src/scripts/array/variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* @vinejs/compiler
*
* (c) VineJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

type FieldOptions = {
variableName: string
}

/**
* Returns JS fragment for defining the array variables
*/
export function defineArrayVariables({ variableName }: FieldOptions) {
return `const ${variableName}_is_array = ensureIsArray(${variableName});`
}
17 changes: 14 additions & 3 deletions src/scripts/field/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ type ValidationOptions = {
* rule is implicit or not
*/
dropMissingCheck: boolean

/**
* The expression to use for performing the existence check.
* Defaults to "item.isDefined"
*/
existenceCheckExpression?: string
}

/**
Expand Down Expand Up @@ -57,10 +63,12 @@ function emitValidationSnippet(
{ isAsync, implicit, ruleFnId }: ValidationNode,
variableName: string,
bail: boolean,
dropMissingCheck: boolean
dropMissingCheck: boolean,
existenceCheckExpression?: string
) {
const rule = `refs['${ruleFnId}']`
const callable = `${rule}.validator(${variableName}.value, ${rule}.options, ${variableName});`
existenceCheckExpression = existenceCheckExpression || `${variableName}.isDefined`

/**
* Add "isValid" condition when the bail flag is turned on.
Expand All @@ -70,7 +78,7 @@ function emitValidationSnippet(
/**
* Add the "!is_[variableName]_missing" conditional when the rule is not implicit.
*/
const implicitCondition = implicit || dropMissingCheck ? '' : `${variableName}.isDefined`
const implicitCondition = implicit || dropMissingCheck ? '' : existenceCheckExpression

/**
* Wrapping the validation invocation inside conditionals based upon
Expand All @@ -90,8 +98,11 @@ export function defineFieldValidations({
validations,
variableName,
dropMissingCheck,
existenceCheckExpression,
}: ValidationOptions) {
return `${validations
.map((one) => emitValidationSnippet(one, variableName, bail, dropMissingCheck))
.map((one) =>
emitValidationSnippet(one, variableName, bail, dropMissingCheck, existenceCheckExpression)
)
.join('\n')}`
}
2 changes: 1 addition & 1 deletion src/scripts/object/guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ type ObjectGuardOptions = {
* Returns JS fragment to wrap code inside an object conditional
*/
export function defineObjectGuard({ variableName, guardedCodeSnippet }: ObjectGuardOptions) {
return `if (ensureIsObject(${variableName})) {
return `if (${variableName}_is_object) {
${guardedCodeSnippet}
}`
}
19 changes: 19 additions & 0 deletions src/scripts/object/variables.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* @vinejs/compiler
*
* (c) VineJS
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

type FieldOptions = {
variableName: string
}

/**
* Returns JS fragment for defining the object variables
*/
export function defineObjectVariables({ variableName }: FieldOptions) {
return `const ${variableName}_is_object = ensureIsObject(${variableName});`
}
Loading

0 comments on commit 1257d44

Please sign in to comment.