@compas/code-gen
Version:
Generate various boring parts of your server
1,636 lines (1,367 loc) • 43 kB
JavaScript
import { isNil } from "@compas/stdlib";
import { fileBlockEnd, fileBlockStart } from "../file/block.js";
import {
fileContextAddLinePrefix,
fileContextCreateGeneric,
fileContextGetOptional,
fileContextRemoveLinePrefix,
fileContextSetIndent,
} from "../file/context.js";
import { fileFormatInlineComment } from "../file/format.js";
import { fileWrite, fileWriteInline } from "../file/write.js";
import { fileImplementations } from "../processors/file-implementations.js";
import { referenceUtilsGetProperty } from "../processors/reference-utils.js";
import { structureResolveReference } from "../processors/structure.js";
import { JavascriptImportCollector } from "../target/javascript.js";
import { typesCacheGet } from "../types/cache.js";
import { typesGeneratorUseTypeName } from "../types/generator.js";
import { validatorGeneratorGenerateBody } from "./generator.js";
/**
* Format the input value path for the current value that is being validated.
*
* @param {import("./generator").ValidatorState} validatorState
* @returns {string}
*/
function formatValuePath(validatorState) {
let result = "";
for (const path of validatorState.validatedValuePath) {
switch (path.type) {
case "root":
result += validatorState.inputVariableName;
break;
case "stringKey":
result += `["${path.key}"]`;
break;
case "dynamicKey":
result += `[${path.key}]`;
break;
}
}
return result;
}
/**
* Format the input result path for the current value that is being validated.
*
* @param {import("./generator").ValidatorState} validatorState
* @returns {string}
*/
function formatResultPath(validatorState) {
let result = "";
for (const path of validatorState.validatedValuePath) {
if (path.type === "root") {
result += validatorState.outputVariableName;
} else if (path.type === "stringKey") {
result += `["${path.key}"]`;
} else if (path.type === "dynamicKey") {
result += `[${path.key}]`;
}
}
return result;
}
/**
* Format the error key for the current value that is being validated.
*
* @param {import("./generator").ValidatorState} validatorState
* @returns {string}
*/
function formatErrorKey(validatorState) {
let result = `${validatorState.errorMapVariableName}[\``;
for (const path of validatorState.validatedValuePath) {
if (path.type === "root") {
result += `$`;
} else if (path.type === "stringKey") {
result += `.${path.key}`;
} else if (path.type === "dynamicKey") {
result += `.$\{${path.key}}`;
}
}
result += "`]";
return result;
}
/**
* Get or create a Javascript validation file for the group that the type belongs to.
*
* @param {import("../generate").GenerateContext} generateContext
* @param {import("../types").NamedType<
* import("../generated/common/types").ExperimentalTypeSystemDefinition
* >} type
* @returns {import("../file/context").GenerateFile}
*/
export function validatorJavascriptGetFile(generateContext, type) {
const relativePath = `${type.group}/validators.js`;
const existingFile = fileContextGetOptional(generateContext, relativePath);
if (existingFile) {
return existingFile;
}
const file = fileContextCreateGeneric(generateContext, relativePath, {
importCollector: new JavascriptImportCollector(),
});
fileWrite(
file,
`/**
* @template T, E
* @typedef {{ value: T, error?: never}|{ value?: never, error: E }} Either
*/
/**
* @typedef {Record<string, any|undefined>} ValidatorErrorMap
*/
`,
);
return file;
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../types").NamedType<
* import("../generated/common/types").ExperimentalTypeSystemDefinition
* >} type
* @param {string} outputTypeName
* @returns {string}
*/
export function validatorJavascriptGetNameAndImport(
file,
type,
outputTypeName,
) {
const importCollector = JavascriptImportCollector.getImportCollector(file);
importCollector.destructure(
`../${type.group}/validators.js`,
`validate${outputTypeName}`,
);
return `validate${outputTypeName}`;
}
/**
* Write docs and declare the validator function for the provided type.
*
* @param {import("../generate").GenerateContext} generateContext
* @param {import("../file/context").GenerateFile} file
* @param {import("../types").NamedType<
* import("../generated/common/types").ExperimentalTypeSystemDefinition
* >} type
* @param {import("../validators/generator").ValidatorState} validatorState
*/
export function validatorJavascriptStartValidator(
generateContext,
file,
type,
validatorState,
) {
// Write JSDoc block
fileWrite(file, "/**");
fileContextAddLinePrefix(file, " *");
if (type.docString) {
fileWrite(file, ` ${type.docString}`);
fileWrite(file, "");
}
fileWrite(
file,
` @param {${typesGeneratorUseTypeName(
generateContext,
file,
validatorState.inputTypeName,
)}|any} ${validatorState.inputVariableName}`,
);
fileWrite(
file,
` @returns {Either<${typesGeneratorUseTypeName(
generateContext,
file,
validatorState.outputTypeName,
)}, ValidatorErrorMap>}`,
);
// Finish the JSDoc block
fileWrite(file, "/");
fileContextRemoveLinePrefix(file, 2);
// Initialize the function
fileBlockStart(
file,
`export function validate${validatorState.outputTypeName}(${validatorState.inputVariableName})`,
);
// We also initialize the error handling and result variable
if (validatorState.jsHasInlineTypes) {
fileWrite(
file,
`const ${validatorState.errorMapVariableName}: ValidatorErrorMap = {};`,
);
fileWrite(
file,
`let ${validatorState.outputVariableName}: any = undefined;\n`,
);
} else {
fileWrite(file, `/** @type {ValidatorErrorMap} */`);
fileWrite(file, `const ${validatorState.errorMapVariableName} = {};`);
fileWrite(file, `/** @type {any} */`);
fileWrite(file, `let ${validatorState.outputVariableName} = undefined;\n`);
}
}
/**
* Exit validation function, determines if an error or the value is returned.
*
* @param {import("../generate").GenerateContext} generateContext
* @param {import("../file/context").GenerateFile} file
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptStopValidator(
generateContext,
file,
validatorState,
) {
fileBlockStart(
file,
`if (Object.keys(${validatorState.errorMapVariableName}).length > 0)`,
);
fileWrite(file, `return { error: ${validatorState.errorMapVariableName} };`);
fileBlockEnd(file);
fileWrite(file, `return { value: ${validatorState.outputVariableName} }`);
fileBlockEnd(file);
fileWrite(file, "\n\n");
}
/**
* Do the nil check, allowNull and defaultValue handling.
*
* @param {import("../file/context").GenerateFile} file
* @param {import("./generator").ValidatorState} validatorState
* @param {{ isOptional: boolean, allowNull: boolean, defaultValue?: string }} options
*/
export function validatorJavascriptNilCheck(file, validatorState, options) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
fileBlockStart(
file,
`if (${valuePath} === null || ${valuePath} === undefined)`,
);
if (options.defaultValue) {
fileWrite(file, `${resultPath} = ${options.defaultValue};`);
} else if (options.allowNull) {
fileWrite(file, `${resultPath} = ${valuePath};`);
} else if (options.isOptional) {
// Normalizes `null` to `undefined`
fileWrite(file, `${resultPath} = undefined;`);
} else {
fileWrite(
file,
`${errorKey} = {
key: "validator.undefined",
};`,
);
}
fileBlockEnd(file);
fileBlockStart(file, "else");
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalAnyDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptAny(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
let didWrite = false;
if (type.targets) {
for (
let i = validatorState.outputTypeOptions.targets.length - 1;
i >= 0;
--i
) {
const target = type.targets[validatorState.outputTypeOptions.targets[i]];
if (target?.validatorExpression) {
fileBlockStart(
file,
`if (${target.validatorExpression.replaceAll(`$value$`, valuePath)})`,
);
if (target.validatorImport) {
// Add the necessary imports for the used expression.
const importCollector =
JavascriptImportCollector.getImportCollector(file);
importCollector.raw(target.validatorImport);
}
fileWrite(file, `${resultPath} = ${valuePath};`);
fileBlockEnd(file);
fileBlockStart(file, `else`);
fileWrite(
file,
`${errorKey} = {
key: "validator.any",
message: "Custom validator error. See the input type for more information.",
};`,
);
fileBlockEnd(file);
didWrite = true;
break;
}
}
}
if (!didWrite) {
fileWrite(file, `${resultPath} = ${valuePath};`);
}
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalAnyOfDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptAnyOf(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
// Fast track validators with discriminant. This also gives much better error objects.
if (type.validator.discriminant) {
const matchableValues = [];
for (const subType of type.values) {
/** @type {import("../generated/common/types.js").ExperimentalObjectDefinition} */
// @ts-expect-error
const resolvedSubType =
subType.type === "reference"
? structureResolveReference(
validatorState.generateContext.structure,
subType,
)
: subType;
const oneOf = referenceUtilsGetProperty(
validatorState.generateContext,
resolvedSubType.keys[type.validator.discriminant],
["oneOf", 0],
);
matchableValues.push(oneOf);
fileBlockStart(
file,
`if (${valuePath}.${type.validator.discriminant} === "${oneOf}")`,
);
validatorState.skipFirstNilCheck = true;
validatorGeneratorGenerateBody(
validatorState.generateContext,
file,
// @ts-ignore-error
//
// Ref is always a system type here
subType,
validatorState,
);
fileBlockEnd(file);
fileWriteInline(file, `else `);
}
fileBlockStart(file, ``);
fileWrite(
file,
`${errorKey} = {
key: "validator.anyOf",
discriminant: "${type.validator.discriminant}",
foundValue: ${valuePath}.${type.validator.discriminant},
allowedValues: ${JSON.stringify(matchableValues)},
};`,
);
fileBlockEnd(file);
return;
}
const anyOfMatchVariable = `hasAnyOfMatch${validatorState.reusedVariableIndex++}`;
fileWrite(file, `let ${anyOfMatchVariable} = false;`);
fileWrite(
file,
`${errorKey} = {
key: "validator.anyOf",
errors: [],
};`,
);
for (const subType of type.values) {
fileBlockStart(file, `if (!${anyOfMatchVariable})`);
validatorState.reusedVariableIndex++;
if (validatorState.jsHasInlineTypes) {
fileWrite(
file,
`const intermediateErrorMap${validatorState.reusedVariableIndex}: ValidatorErrorMap = {};`,
);
fileWrite(
file,
`let intermediateResult${validatorState.reusedVariableIndex}: any = undefined;`,
);
fileWrite(
file,
`let intermediateValue${validatorState.reusedVariableIndex}: any = ${valuePath};\n`,
);
} else {
fileWrite(file, `/** @type {ValidatorErrorMap} */`);
fileWrite(
file,
`const intermediateErrorMap${validatorState.reusedVariableIndex} = {};`,
);
fileWrite(file, `/** @type {any} */`);
fileWrite(
file,
`let intermediateResult${validatorState.reusedVariableIndex} = undefined;`,
);
fileWrite(file, `/** @type {any} */`);
fileWrite(
file,
`let intermediateValue${validatorState.reusedVariableIndex} = ${valuePath};\n`,
);
}
/** @type {import("./generator").ValidatorState} */
const validatorStateCopy = {
...validatorState,
validatedValuePath: [{ type: "root" }],
inputVariableName: `intermediateValue${validatorState.reusedVariableIndex}`,
outputVariableName: `intermediateResult${validatorState.reusedVariableIndex}`,
errorMapVariableName: `intermediateErrorMap${validatorState.reusedVariableIndex}`,
};
const nestedResultPath = formatResultPath(validatorStateCopy);
validatorGeneratorGenerateBody(
validatorState.generateContext,
file,
// @ts-ignore-error
//
// Ref is always a system type here
subType,
validatorStateCopy,
);
fileBlockStart(
file,
`if (Object.keys(${validatorStateCopy.errorMapVariableName}).length > 0)`,
);
fileWrite(
file,
`${errorKey}.errors.push(${validatorStateCopy.errorMapVariableName});`,
);
fileBlockEnd(file);
fileBlockStart(file, "else");
fileWrite(file, `${anyOfMatchVariable} = true;`);
fileWrite(file, `delete ${errorKey};`);
fileWrite(file, `${resultPath} = ${nestedResultPath};`);
fileBlockEnd(file);
fileBlockEnd(file);
validatorState.reusedVariableIndex--;
}
validatorState.reusedVariableIndex--;
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalArrayDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptArray(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
validatorState.reusedVariableIndex++;
if (validatorState.jsHasInlineTypes) {
fileWrite(
file,
`const intermediateErrorMap${validatorState.reusedVariableIndex}: ValidatorErrorMap = {};`,
);
fileWrite(
file,
`let intermediateResult${validatorState.reusedVariableIndex}: any[] = [];`,
);
fileWrite(
file,
`let intermediateValue${validatorState.reusedVariableIndex}: any|any[] = ${valuePath};\n`,
);
} else {
fileWrite(file, `/** @type {ValidatorErrorMap} */`);
fileWrite(
file,
`const intermediateErrorMap${validatorState.reusedVariableIndex} = {};`,
);
fileWrite(file, `/** @type {any[]} */`);
fileWrite(
file,
`let intermediateResult${validatorState.reusedVariableIndex} = [];`,
);
fileWrite(file, `/** @type {any|any[]} */`);
fileWrite(
file,
`let intermediateValue${validatorState.reusedVariableIndex} = ${valuePath};\n`,
);
}
/** @type {import("./generator").ValidatorState} */
const validatorStateCopy = {
...validatorState,
validatedValuePath: [{ type: "root" }],
inputVariableName: `intermediateValue${validatorState.reusedVariableIndex}`,
outputVariableName: `intermediateResult${validatorState.reusedVariableIndex}`,
errorMapVariableName: `intermediateErrorMap${validatorState.reusedVariableIndex}`,
};
const currentValuePath = formatValuePath(validatorStateCopy);
const currentResultPath = formatResultPath(validatorStateCopy);
fileBlockStart(file, `if (!Array.isArray(${currentValuePath}))`);
fileWrite(file, `${currentValuePath} = [${currentValuePath}];`);
fileBlockEnd(file);
if (!isNil(type.validator.min)) {
fileBlockStart(
file,
`if (${currentValuePath}.length < ${type.validator.min})`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.length",
minLength: ${type.validator.min},
foundLength: ${currentValuePath}.length,
};`,
);
fileBlockEnd(file);
}
if (!isNil(type.validator.max)) {
fileBlockStart(
file,
`if (${currentValuePath}.length > ${type.validator.max})`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.length",
maxLength: ${type.validator.max},
foundLength: ${currentValuePath}.length,
};`,
);
fileBlockEnd(file);
}
// Initialize result array
fileWrite(
file,
`${resultPath} = Array.from({ length: ${currentValuePath}.length });`,
);
// Loop through array
const idxVariable = `i${validatorState.reusedVariableIndex++}`;
validatorStateCopy.validatedValuePath.push({
type: "dynamicKey",
key: idxVariable,
});
fileBlockStart(
file,
`for (let ${idxVariable} = 0; ${idxVariable} < ${currentValuePath}.length; ++${idxVariable})`,
);
validatorGeneratorGenerateBody(
validatorState.generateContext,
file,
// @ts-ignore-error
//
// Ref is always a system type here
type.values,
validatorStateCopy,
);
fileBlockEnd(file);
fileBlockStart(
file,
`if (Object.keys(${validatorStateCopy.errorMapVariableName}).length)`,
);
fileBlockStart(
file,
`for (const errorKey of Object.keys(${validatorStateCopy.errorMapVariableName}))`,
);
fileWrite(
file,
`${errorKey.substring(
0,
errorKey.length - 2,
)}$\{errorKey.substring(1)}\`] = ${
validatorStateCopy.errorMapVariableName
}[errorKey];`,
);
fileBlockEnd(file);
fileBlockEnd(file);
fileBlockStart(file, "else");
fileWrite(file, `${resultPath} = ${currentResultPath};`);
fileBlockEnd(file);
// Reset state;
validatorState.reusedVariableIndex--;
validatorState.reusedVariableIndex--;
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalBooleanDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptBoolean(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
if (!isNil(type.oneOf)) {
fileBlockStart(
file,
`if (${valuePath} === ${type.oneOf} || ${valuePath} === "${
type.oneOf
}" || ${valuePath} === ${type.oneOf ? 1 : 0} || ${valuePath} === "${
type.oneOf ? 1 : 0
}")`,
);
fileWrite(file, `${resultPath} = ${type.oneOf};`);
fileBlockEnd(file);
fileBlockStart(file, `else`);
fileWrite(
file,
`${errorKey} = {
key: "validator.oneOf",
allowedValues: [${type.oneOf}],
foundValue: ${valuePath},
};`,
);
fileBlockEnd(file);
} else {
fileBlockStart(
file,
`if (${valuePath} === true || ${valuePath} === "true" || ${valuePath} === 1 || ${valuePath} === "1")`,
);
fileWrite(file, `${resultPath} = true;`);
fileBlockEnd(file);
fileBlockStart(
file,
`else if (${valuePath} === false || ${valuePath} === "false" || ${valuePath} === 0 || ${valuePath} === "0")`,
);
fileWrite(file, `${resultPath} = false;`);
fileBlockEnd(file);
fileBlockStart(file, "else");
fileWrite(
file,
`${errorKey} = {
key: "validator.type",
expectedType: "boolean",
};`,
);
fileBlockEnd(file);
}
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalDateDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptDate(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
if (type.specifier === "dateOnly") {
fileWrite(file, fileFormatInlineComment(file, `yyyy-MM-dd`));
fileBlockStart(
file,
`if (typeof ${valuePath} !== "string" || !(/^\\d{4}-((0[1-9])|(1[0-2]))-((0[1-9])|([1-2][0-9])|(3[0-1]))$/gi).test(${valuePath}))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.pattern",
patternExplanation: "yyyy-MM-dd",
};`,
);
fileBlockEnd(file);
fileBlockStart(file, "else");
fileWrite(file, `${resultPath} = ${valuePath};`);
fileBlockEnd(file);
} else if (type.specifier === "timeOnly") {
fileWrite(file, fileFormatInlineComment(file, `HH:mm(:ss(.SSS)`));
fileBlockStart(
file,
`if (typeof ${valuePath} !== "string" || !(/^(([0-1][0-9])|(2[0-3])):[0-5][0-9](:[0-5][0-9](\\.\\d{3})?)?$/gi).test(${valuePath}))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.pattern",
patternExplanation: "HH:mm(:ss(.SSS))",
};`,
);
fileBlockEnd(file);
fileBlockStart(file, "else");
fileWrite(file, `${resultPath} = ${valuePath};`);
fileBlockEnd(file);
} else {
fileBlockStart(
file,
`if (typeof ${valuePath} === "string" || typeof ${valuePath} === "number")`,
);
fileWrite(file, `${resultPath} = new Date(${valuePath});`);
fileBlockEnd(file);
fileBlockStart(
file,
`else if (Object.prototype.toString.call(${valuePath}) === "[object Date]")`,
);
fileWrite(file, `${resultPath} = ${valuePath};`);
fileBlockEnd(file);
fileBlockStart(file, "else");
fileWrite(
file,
`${errorKey} = {
key: "validator.type",
expectedType: "Date|string",
};`,
);
fileBlockEnd(file);
// Note that `null` and `undefined` behave differently when passed to `isNaN`.
fileBlockStart(file, `if (isNaN(${resultPath}?.getTime() ?? undefined))`);
fileWrite(
file,
`${errorKey} = {
key: "validator.date.invalid",
};`,
);
fileBlockEnd(file);
if (
!isNil(type.validator.min) ||
!isNil(type.validator.max) ||
type.validator.inFuture ||
type.validator.inPast
) {
fileBlockStart(file, "else");
}
if (!isNil(type.validator.min)) {
fileBlockStart(
file,
`if (${valuePath} < new Date("${type.validator.min}"))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.range",
minValue: new Date("${type.validator.min}"),
};`,
);
fileBlockEnd(file);
}
if (!isNil(type.validator.max)) {
fileBlockStart(
file,
`if (${valuePath} > new Date("${type.validator.max}"))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.range",
maxValue: new Date("${type.validator.max}")
};`,
);
fileBlockEnd(file);
}
if (type.validator.inFuture) {
fileBlockStart(file, `if (${valuePath} < new Date())`);
fileWrite(
file,
`${errorKey} = {
key: "validator.range",
minValue: new Date(),
};`,
);
fileBlockEnd(file);
}
if (type.validator.inPast) {
fileBlockStart(file, `if (${valuePath} > new Date())`);
fileWrite(
file,
`${errorKey} = {
key: "validator.range",
maxValue: new Date(),
};`,
);
fileBlockEnd(file);
}
if (
!isNil(type.validator.min) ||
!isNil(type.validator.max) ||
type.validator.inFuture ||
type.validator.inPast
) {
fileBlockEnd(file);
}
}
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalFileDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptFile(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
let didWrite = false;
for (
let i = validatorState.outputTypeOptions.targets.length - 1;
i >= 0;
--i
) {
const target =
fileImplementations[validatorState.outputTypeOptions.targets[i]];
if (target?.validatorExpression) {
fileBlockStart(
file,
`if (${target.validatorExpression.replaceAll(`$value$`, valuePath)})`,
);
if (target.validatorImport) {
// Add the necessary imports for the used expression.
const importCollector =
JavascriptImportCollector.getImportCollector(file);
importCollector.raw(target.validatorImport);
}
fileWrite(file, `${resultPath} = ${valuePath};`);
fileBlockEnd(file);
fileBlockStart(file, `else`);
fileWrite(
file,
`${errorKey} = {
key: "validator.file",
message: "Invalid file input. See the input type for more information.",
};`,
);
fileBlockEnd(file);
didWrite = true;
break;
}
}
if (!didWrite) {
fileWrite(file, `${resultPath} = ${valuePath};`);
} else if (
validatorState.outputTypeOptions.targets.includes("jsKoaReceive")
) {
if (!isNil(type.validator.mimeTypes)) {
fileBlockStart(file, `if (${resultPath}?.mimetype)`);
fileBlockStart(
file,
`if (!${JSON.stringify(
type.validator.mimeTypes,
)}.includes(${resultPath}?.mimetype))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.mimeType",
foundMimeType: ${resultPath}.mimetype,
allowedMimeTypes: ${JSON.stringify(type.validator.mimeTypes)},
};`,
);
fileBlockEnd(file);
fileBlockEnd(file);
}
}
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalGenericDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptGeneric(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
fileBlockStart(
file,
`if (typeof ${valuePath} !== "object" || Array.isArray(${valuePath}))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.generic",
};`,
);
fileBlockEnd(file);
fileBlockStart(file, `else`);
const keyVariable = `genericKeyInput${validatorState.reusedVariableIndex++}`;
const resultVariable = `genericKeyResult${validatorState.reusedVariableIndex++}`;
const errorMapVariable = `genericKeyErrorMap${validatorState.reusedVariableIndex++}`;
/** @type {import("./generator").ValidatorState} */
const nestedKeyState = {
...validatorState,
inputVariableName: keyVariable,
outputVariableName: resultVariable,
errorMapVariableName: errorMapVariable,
validatedValuePath: [{ type: "root" }],
};
// Already set the nested validator path for generic values
validatorState.validatedValuePath.push({
type: "dynamicKey",
key: resultVariable,
});
fileWrite(file, `${resultPath} = {};`);
fileBlockStart(file, `for (let ${keyVariable} of Object.keys(${valuePath}))`);
if (validatorState.jsHasInlineTypes) {
fileWrite(file, `let ${resultVariable}: any = undefined;`);
fileWrite(file, `const ${errorMapVariable}: ValidatorErrorMap = {};`);
} else {
fileWrite(file, `/** @type {any} */`);
fileWrite(file, `let ${resultVariable} = undefined;`);
fileWrite(file, `/** @type {ValidatorErrorMap} */`);
fileWrite(file, `const ${errorMapVariable} = {};`);
}
validatorGeneratorGenerateBody(
validatorState.generateContext,
file,
// @ts-expect-error
type.keys,
nestedKeyState,
);
fileBlockStart(file, `if (Object.keys(${errorMapVariable}).length !== 0)`);
fileBlockStart(file, `if (${errorKey})`);
fileWrite(
file,
`${errorKey}.inputs.push({ key: ${keyVariable}, errors: ${errorMapVariable} });`,
);
fileBlockEnd(file);
fileBlockStart(file, "else");
fileWrite(
file,
`${errorKey} = {
key: "validator.generic",
inputs: [{ key: ${keyVariable}, errors: ${errorMapVariable} }],
};`,
);
fileBlockEnd(file);
fileBlockEnd(file);
fileBlockStart(file, "else");
validatorGeneratorGenerateBody(
validatorState.generateContext,
file,
// @ts-expect-error
type.values,
validatorState,
);
fileBlockEnd(file);
fileBlockEnd(file);
fileBlockEnd(file);
validatorState.validatedValuePath.pop();
validatorState.reusedVariableIndex--;
validatorState.reusedVariableIndex--;
validatorState.reusedVariableIndex--;
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalNumberDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptNumber(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
const intermediateVariable = `convertedNumber${validatorState.reusedVariableIndex++}`;
fileWrite(file, `let ${intermediateVariable} = ${valuePath};`);
fileBlockStart(
file,
`if (typeof ${intermediateVariable} !== "number" && typeof ${intermediateVariable} === "string")`,
);
fileWrite(file, `${intermediateVariable} = Number(${intermediateVariable});`);
fileBlockEnd(file);
const conditionPartial = !type.validator.floatingPoint
? `|| !Number.isInteger(${intermediateVariable})`
: "";
const subType = type.validator.floatingPoint ? "float" : "int";
fileBlockStart(
file,
`if (typeof ${intermediateVariable} !== "number" || isNaN(${intermediateVariable}) || !isFinite(${intermediateVariable}) ${conditionPartial})`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.number",
subType: "${subType}",
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, "} else ");
if (!isNil(type.validator.min)) {
fileWrite(file, `if (${intermediateVariable} < ${type.validator.min}) {`);
fileContextSetIndent(file, 1);
fileWrite(
file,
`${errorKey} = {
key: "validator.range",
minValue: ${type.validator.min},
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
if (!isNil(type.validator.max)) {
fileWrite(file, `if (${intermediateVariable} > ${type.validator.max}) {`);
fileContextSetIndent(file, 1);
fileWrite(
file,
`${errorKey} = {
key: "validator.range",
maxValue: ${type.validator.max}
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
if (type.oneOf) {
const condition = type.oneOf
.map((it) => `${intermediateVariable} !== ${it}`)
.join(" && ");
fileWrite(file, `if (${condition}) {`);
fileContextSetIndent(file, 1);
fileWrite(
file,
`${errorKey} = {
key: "validator.oneOf",
allowedValues: [${type.oneOf.join(", ")}],
foundValue: ${intermediateVariable},
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
fileBlockStart(file, ``);
fileWrite(file, `${resultPath} = ${intermediateVariable};`);
fileBlockEnd(file);
validatorState.reusedVariableIndex--;
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalObjectDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptObject(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
fileBlockStart(
file,
`if (typeof ${valuePath} !== "object" || Array.isArray(${valuePath}))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.object",
value: ${valuePath},
foundType: typeof ${valuePath},
};`,
);
fileBlockEnd(file);
fileBlockStart(file, "else");
const isApiClientValidator =
validatorState.outputTypeOptions.targets.includes("jsAxios") ||
validatorState.outputTypeOptions.targets.includes("tsAxios");
// Allows api clients to skip strict validation, even if the type enforces it to prevent unnecessary breaking changes when new keys are added to the response.
if (
type.validator.strict &&
(!isApiClientValidator ||
validatorState.generateContext.options.generators.apiClient
?.responseValidation.looseObjectValidation === false)
) {
const setVariable = `knownKeys${validatorState.reusedVariableIndex++}`;
if (validatorState.jsHasInlineTypes) {
fileWrite(file, `const ${setVariable}: Set<string> = new Set([`);
} else {
fileWrite(file, `/** @type {Set<string>} */`);
fileWrite(file, `const ${setVariable} = new Set([`);
}
fileContextSetIndent(file, 1);
for (const key of Object.keys(type.keys)) {
fileWrite(file, `"${key}",`);
}
fileContextSetIndent(file, -1);
fileWrite(file, `]);`);
fileBlockStart(file, `for (const key of Object.keys(${valuePath}))`);
fileBlockStart(
file,
`if (!${setVariable}.has(key) && ${valuePath}[key] !== null && ${valuePath}[key] !== undefined)`,
);
fileWrite(file, `const expectedKeys = [...${setVariable}];`);
fileWrite(file, `const foundKeys = Object.keys(${valuePath});`);
fileWrite(
file,
`const unknownKeys = foundKeys.filter(it => !${setVariable}.has(it));`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.keys",
unknownKeys,
expectedKeys,
foundKeys,
};`,
);
fileWrite(file, `break;`);
fileBlockEnd(file);
fileBlockEnd(file);
validatorState.reusedVariableIndex--;
}
fileWrite(file, `${resultPath} = Object.create(null);\n`);
let variableIndex = 0;
for (const key of Object.keys(type.keys)) {
validatorState.validatedValuePath.push({ type: "stringKey", key });
variableIndex++;
validatorState.reusedVariableIndex++;
validatorGeneratorGenerateBody(
validatorState.generateContext,
file,
// @ts-ignore-error
//
// Ref is always a system type here
type.keys[key],
validatorState,
);
validatorState.validatedValuePath.pop();
}
validatorState.reusedVariableIndex -= variableIndex;
fileBlockEnd(file);
}
/**
* @param {import("../generate").GenerateContext} generateContext
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalReferenceDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptReference(
generateContext,
file,
type,
validatorState,
) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
const ref = structureResolveReference(
validatorState.generateContext.structure,
type,
);
const referredTypeName = typesCacheGet(
generateContext,
// @ts-ignore-error
//
// Ref is always a system type here
ref,
validatorState.outputTypeOptions,
);
if (generateContext.options.targetLanguage === "js") {
if (file.relativePath !== `${type.reference.group}/validators.js`) {
const importCollector =
JavascriptImportCollector.getImportCollector(file);
importCollector.destructure(
`../${type.reference.group}/validators.js`,
`validate${referredTypeName}`,
);
}
} else if (generateContext.options.targetLanguage === "ts") {
if (file.relativePath !== `${type.reference.group}/validators.ts`) {
const importCollector =
JavascriptImportCollector.getImportCollector(file);
importCollector.destructure(
`../${type.reference.group}/validators`,
`validate${referredTypeName}`,
);
}
}
const intermediateVariable = `refResult${validatorState.reusedVariableIndex++}`;
fileWrite(
file,
`const ${intermediateVariable} = validate${referredTypeName}(${valuePath});\n`,
);
fileBlockStart(file, `if (${intermediateVariable}.error)`);
fileBlockStart(
file,
`for (const errorKey of Object.keys(${intermediateVariable}.error))`,
);
fileWrite(
file,
`${errorKey.substring(
0,
errorKey.length - 2,
)}$\{errorKey.substring(1)}\`] = ${intermediateVariable}.error[errorKey];`,
);
fileBlockEnd(file);
fileBlockEnd(file);
fileWrite(file, `${resultPath} = ${intermediateVariable}.value;`);
validatorState.reusedVariableIndex--;
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalStringDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptString(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
const intermediateVariable = `convertedString${validatorState.reusedVariableIndex++}`;
if (validatorState.jsHasInlineTypes) {
fileWrite(
file,
`let ${intermediateVariable}: string = ${valuePath} as any;`,
);
} else {
fileWrite(file, `/** @type {string} */`);
fileWrite(file, `let ${intermediateVariable} = ${valuePath};`);
}
fileBlockStart(file, `if (typeof ${intermediateVariable} !== "string")`);
fileWrite(
file,
`${errorKey} = {
key: "validator.string",
};`,
);
fileBlockEnd(file);
fileBlockStart(file, `else`);
if (type.validator.trim) {
fileWrite(
file,
`${intermediateVariable} = ${intermediateVariable}.trim();`,
);
}
if (type.isOptional) {
fileBlockStart(file, `if (${intermediateVariable}.length === 0)`);
if (type.validator.min === 0) {
// This is a valid input, so it gets priority over the defaultValue.
fileWrite(file, `${resultPath} = "";`);
} else if (!isNil(type.defaultValue)) {
fileWrite(file, `${resultPath} = ${type.defaultValue};`);
} else {
fileWrite(
file,
`${resultPath} = ${type.validator.allowNull ? "null" : "undefined"};`,
);
}
fileBlockEnd(file);
fileBlockStart(file, "else");
}
if (type.validator.upperCase) {
fileWrite(
file,
`${intermediateVariable} = ${intermediateVariable}.toUpperCase();`,
);
}
if (type.validator.lowerCase) {
fileWrite(
file,
`${intermediateVariable} = ${intermediateVariable}.toLowerCase();`,
);
}
if (!isNil(type.validator.min) && type.validator.min > 0) {
fileBlockStart(
file,
`if (${intermediateVariable}.length < ${type.validator.min})`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.length",
minLength: ${type.validator.min}
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
if (!isNil(type.validator.max)) {
fileWrite(
file,
`if (${intermediateVariable}.length > ${type.validator.max}) {`,
);
fileContextSetIndent(file, 1);
fileWrite(
file,
`${errorKey} = {
key: "validator.length",
maxLength: ${type.validator.max}
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
if (type.oneOf) {
const condition = type.oneOf
.map((it) => `${intermediateVariable} !== "${it}"`)
.join(" && ");
fileWrite(file, `if (${condition}) {`);
fileContextSetIndent(file, 1);
fileWrite(
file,
`${errorKey} = {
key: "validator.oneOf",
allowedValues: ${JSON.stringify(type.oneOf)},
foundValue: ${intermediateVariable},
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
if (type.validator.pattern) {
fileWrite(
file,
`if (!${type.validator.pattern}.test(${intermediateVariable})) {`,
);
fileContextSetIndent(file, 1);
fileWrite(
file,
`${errorKey} = {
key: "validator.pattern",
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
if (type.validator.disallowedCharacters) {
const conditional = type.validator.disallowedCharacters
.map((it) => `${intermediateVariable}.includes(${JSON.stringify(it)})`)
.join(" || ");
fileWrite(file, `if (${conditional}) {`);
fileContextSetIndent(file, 1);
fileWrite(
file,
`${errorKey} = {
key: "validator.disallowedCharacters",
disallowedCharacters: ${JSON.stringify(type.validator.disallowedCharacters)},
};`,
);
fileContextSetIndent(file, -1);
fileWriteInline(file, `} else `);
}
fileBlockStart(file, ``);
fileWrite(file, `${resultPath} = ${intermediateVariable};`);
fileBlockEnd(file);
if (type.isOptional) {
fileBlockEnd(file);
}
fileBlockEnd(file);
validatorState.reusedVariableIndex--;
}
/**
*
* @param {import("../file/context").GenerateFile} file
* @param {import("../generated/common/types").ExperimentalUuidDefinition} type
* @param {import("./generator").ValidatorState} validatorState
*/
export function validatorJavascriptUuid(file, type, validatorState) {
const valuePath = formatValuePath(validatorState);
const resultPath = formatResultPath(validatorState);
const errorKey = formatErrorKey(validatorState);
const regex =
"/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/gi";
const regexWithoutDash = "/^[a-f0-9]{32}$/gi";
fileBlockStart(
file,
`if (typeof ${valuePath} !== "string" || (!${regex}.test(${valuePath}) && !${regexWithoutDash}.test(${valuePath})))`,
);
fileWrite(
file,
`${errorKey} = {
key: "validator.pattern",
patternExplanation: "UUID",
};`,
);
fileBlockEnd(file);
fileBlockStart(file, `else if (${valuePath}.length === 32)`);
fileWrite(
file,
`${resultPath} = ${valuePath}.slice(0,8) + "-" + ${valuePath}.slice(8, 12) + "-" + ${valuePath}.slice(12, 16) + "-" + ${valuePath}.slice(16, 20) + "-" + ${valuePath}.slice(20);`,
);
fileBlockEnd(file);
fileBlockStart(file, `else`);
fileWrite(file, `${resultPath} = ${valuePath};`);
fileBlockEnd(file);
}
/**
* Finish the else block for nil checks.
*
* @param {import("../file/context").GenerateFile} file
*/
export function validatorJavascriptFinishElseBlock(file) {
fileBlockEnd(file);
}