Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
94 changes: 75 additions & 19 deletions src/compiler/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,13 @@ import { createRecordField } from './fields/record_field.js'
import { defineInlineFunctions } from '../scripts/define_inline_functions.js'
import { defineInlineErrorMessages } from '../scripts/define_error_messages.js'
import type {
Refs,
RootNode,
CompilerField,
CompilerNodes,
CompilerParent,
CompilerOptions,
ErrorReporterContract,
MessagesProviderContact,
CompiledValidator,
CompiledSyncValidator,
} from '../types.js'

/**
Expand Down Expand Up @@ -64,6 +63,18 @@ export class Compiler {
*/
#buffer: CompilerBuffer = new CompilerBuffer()

/**
* Whether the current compilation targets a synchronous function
*/
#synchronous: boolean = false

/**
* Read by node compilers to emit synchronous validation calls
*/
get synchronous() {
return this.#synchronous
}

constructor(rootNode: RootNode, options?: CompilerOptions) {
this.#rootNode = rootNode
this.#options = options || { convertEmptyStringsToNull: false }
Expand All @@ -81,7 +92,7 @@ export class Compiler {
...this.#options.messages,
})
)
this.#buffer.writeStatement(defineInlineFunctions(this.#options))
this.#buffer.writeStatement(defineInlineFunctions(this.#options, this.#synchronous))
this.#buffer.writeStatement('let out;')
}

Expand Down Expand Up @@ -109,13 +120,7 @@ export class Compiler {
/**
* Returns compiled output as a function
*/
#toAsyncFunction<T extends Record<string, any>>(): (
data: any,
meta: Record<string, any>,
refs: Refs,
messagesProvider: MessagesProviderContact,
errorReporter: ErrorReporterContract
) => Promise<T> {
#toAsyncFunction(): CompiledValidator {
return new AsyncFunction(
'root',
'meta',
Expand All @@ -126,6 +131,36 @@ export class Compiler {
)
}

/**
* Returns compiled output as a synchronous function
*/
#toFunction(): CompiledSyncValidator {
return new Function(
'root',
'meta',
'refs',
'messagesProvider',
'errorReporter',
this.#buffer.toString()
) as CompiledSyncValidator
}

/**
* Ensures async validation rules are not part of synchronous compilation
*/
#ensureCanCompileSync(node: CompilerNodes) {
if (!this.#synchronous || !('validations' in node)) {
return
}

const asyncRule = node.validations.find((validation) => validation.isAsync)
if (asyncRule) {
throw new Error(
`Cannot compile validation rule "${asyncRule.name || asyncRule.ruleFnId}" for field "${node.fieldName}" synchronously because it is async`
)
}
}

/**
* Converts a node to a field. Optionally accepts a parent node to create
* a field for a specific parent type.
Expand Down Expand Up @@ -154,6 +189,8 @@ export class Compiler {
parent: CompilerParent,
parentField?: CompilerField
) {
this.#ensureCanCompileSync(node)

switch (node.type) {
case 'literal':
return new LiteralNodeCompiler(node, buffer, this, parent, parentField).compile()
Expand All @@ -173,16 +210,35 @@ export class Compiler {
/**
* Compile schema nodes to an async function
*/
compile() {
this.#initiateJSOutput()
this.#compileNodes()
this.#finishJSOutput()
compile(): CompiledValidator {
return this.#compile(false)
}

const outputFunction = this.#toAsyncFunction()
/**
* Compile schema nodes to a synchronous function
*/
compileSync(): CompiledSyncValidator {
return this.#compile(true)
}

/**
* Compile schema nodes to a function using the requested execution mode
*/
#compile(synchronous: false): CompiledValidator
#compile(synchronous: true): CompiledSyncValidator
#compile(synchronous: boolean) {
this.#synchronous = synchronous
this.#initiateJSOutput()

this.variablesCounter = 0
this.#buffer.flush()
try {
this.#compileNodes()
this.#finishJSOutput()

return outputFunction
return synchronous ? this.#toFunction() : this.#toAsyncFunction()
} finally {
this.#synchronous = false
this.variablesCounter = 0
this.#buffer.flush()
}
}
}
1 change: 1 addition & 0 deletions src/compiler/nodes/array.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class ArrayNodeCompiler extends BaseNode {
*/
this.#buffer.writeStatement(
defineFieldValidations({
synchronous: this.#compiler.synchronous,
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
Expand Down
3 changes: 3 additions & 0 deletions src/compiler/nodes/literal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { validateLiteralField } from '../../scripts/field/variables.js'
export class LiteralNodeCompiler extends BaseNode {
#node: LiteralNode
#buffer: CompilerBuffer
#compiler: Compiler

constructor(
node: LiteralNode,
Expand All @@ -34,6 +35,7 @@ export class LiteralNodeCompiler extends BaseNode {
super(node, compiler, parent, parentField)
this.#node = node
this.#buffer = buffer
this.#compiler = compiler
}

compile() {
Expand Down Expand Up @@ -70,6 +72,7 @@ export class LiteralNodeCompiler extends BaseNode {
*/
this.#buffer.writeStatement(
defineFieldValidations({
synchronous: this.#compiler.synchronous,
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
Expand Down
1 change: 1 addition & 0 deletions src/compiler/nodes/object.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ export class ObjectNodeCompiler extends BaseNode {
*/
this.#buffer.writeStatement(
defineFieldValidations({
synchronous: this.#compiler.synchronous,
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
Expand Down
1 change: 1 addition & 0 deletions src/compiler/nodes/record.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ export class RecordNodeCompiler extends BaseNode {
*/
this.#buffer.writeStatement(
defineFieldValidations({
synchronous: this.#compiler.synchronous,
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
Expand Down
1 change: 1 addition & 0 deletions src/compiler/nodes/tuple.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export class TupleNodeCompiler extends BaseNode {
*/
this.#buffer.writeStatement(
defineFieldValidations({
synchronous: this.#compiler.synchronous,
variableName: this.field.variableName,
validations: this.#node.validations,
bail: this.#node.bail,
Expand Down
17 changes: 16 additions & 1 deletion src/scripts/define_inline_functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@
* Returns JS fragment for inline function needed by the
* validation runtime code.
*/
export function defineInlineFunctions(options: { convertEmptyStringsToNull: boolean }) {
export function defineInlineFunctions(
options: { convertEmptyStringsToNull: boolean },
synchronous: boolean = false
) {
return `function report(message, rule, field, args) {
field.isValid = false;
errorReporter.report(messagesProvider.getMessage(message, rule, field, args), rule, field, args);
Expand Down Expand Up @@ -56,6 +59,18 @@ function ensureIsArray(field) {
field.report(NOT_AN_ARRAY, 'array', field);
return false;
};
${
synchronous
? `function ensureIsNotPromise(result, rule, field) {
if (result !== null && (typeof result === 'object' || typeof result === 'function') && typeof result.then === 'function') {
try {
result.then(undefined, function () {});
} catch {}
throw new Error(\`Validation rule "\${rule}" for field "\${field.getFieldPath()}" returned a promise or thenable while marked as synchronous\`);
}
};`
: ''
}
function copyProperties(val) {
let k, out, tmp;

Expand Down
22 changes: 18 additions & 4 deletions src/scripts/field/validations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { type ValidationNode } from '../../types.js'
*/
type ValidationOptions = {
bail: boolean
synchronous?: boolean
variableName: string
validations: ValidationNode[]

Expand Down Expand Up @@ -60,14 +61,15 @@ function wrapInConditional(conditions: [string, string], wrappingCode: string) {
* Emits code for executing a validation function
*/
function emitValidationSnippet(
{ isAsync, implicit, ruleFnId }: ValidationNode,
{ name, isAsync, implicit, ruleFnId }: ValidationNode,
variableName: string,
bail: boolean,
synchronous: boolean,
dropMissingCheck: boolean,
existenceCheckExpression?: string
) {
const rule = `refs['${ruleFnId}']`
const callable = `${rule}.validator(${variableName}.value, ${rule}.options, ${variableName});`
const callable = `${rule}.validator(${variableName}.value, ${rule}.options, ${variableName})`
existenceCheckExpression = existenceCheckExpression || `${variableName}.isDefined`

/**
Expand All @@ -86,7 +88,11 @@ function emitValidationSnippet(
*/
return wrapInConditional(
[bailCondition, implicitCondition],
isAsync ? `await ${callable}` : `${callable}`
isAsync
? `await ${callable};`
: synchronous
? `ensureIsNotPromise(${callable}, ${JSON.stringify(name || ruleFnId)}, ${variableName});`
: `${callable};`
)
}

Expand All @@ -95,14 +101,22 @@ function emitValidationSnippet(
*/
export function defineFieldValidations({
bail,
synchronous = false,
validations,
variableName,
dropMissingCheck,
existenceCheckExpression,
}: ValidationOptions) {
return `${validations
.map((one) =>
emitValidationSnippet(one, variableName, bail, dropMissingCheck, existenceCheckExpression)
emitValidationSnippet(
one,
variableName,
bail,
synchronous,
dropMissingCheck,
existenceCheckExpression
)
)
.join('\n')}`
}
29 changes: 23 additions & 6 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,7 @@ export type RootNode = {
* Known tree nodes accepted by the compiler
*/
export type CompilerNodes =
| LiteralNode
| ObjectNode
| ArrayNode
| UnionNode
| RecordNode
| TupleNode
LiteralNode | ObjectNode | ArrayNode | UnionNode | RecordNode | TupleNode

/**
* Properties of a parent node as the compiler loops through the
Expand Down Expand Up @@ -526,6 +521,28 @@ export interface MessagesProviderContact {
): string
}

/**
* A validator produced by Compiler.compile
*/
export type CompiledValidator<Output = Record<string, any>> = (
data: any,
meta: Record<string, any>,
refs: Refs,
messagesProvider: MessagesProviderContact,
errorReporter: ErrorReporterContract
) => Promise<Output>

/**
* A validator produced by Compiler.compileSync
*/
export type CompiledSyncValidator<Output = Record<string, any>> = (
data: any,
meta: Record<string, any>,
refs: Refs,
messagesProvider: MessagesProviderContact,
errorReporter: ErrorReporterContract
) => Output

/**
* Options accepted by the compiler
*/
Expand Down
Loading
Loading