UNPKG

@compas/code-gen

Version:

Generate various boring parts of your server

683 lines (631 loc) 18.9 kB
import { AppError } from "@compas/stdlib"; import { referenceUtilsGetProperty } from "../processors/reference-utils.js"; import { structureNamedTypes, structureResolveReference, } from "../processors/structure.js"; import { stringFormatNameForError } from "../string-format.js"; import { targetLanguageSwitch } from "../target/switcher.js"; import { typesCacheGet } from "../types/cache.js"; import { typesGeneratorGenerateNamedType } from "../types/generator.js"; import { typesOptionalityIsOptional } from "../types/optionality.js"; import { validatorJavascriptAny, validatorJavascriptAnyOf, validatorJavascriptArray, validatorJavascriptBoolean, validatorJavascriptDate, validatorJavascriptFile, validatorJavascriptFinishElseBlock, validatorJavascriptGeneric, validatorJavascriptGetFile, validatorJavascriptGetNameAndImport, validatorJavascriptNilCheck, validatorJavascriptNumber, validatorJavascriptObject, validatorJavascriptReference, validatorJavascriptStartValidator, validatorJavascriptStopValidator, validatorJavascriptString, validatorJavascriptUuid, } from "./javascript.js"; import { validatorTypescriptGetFile, validatorTypescriptGetNameAndImport, validatorTypescriptStartValidator, } from "./typescript.js"; /** * Cache for which type names we have written a validator in a file. * * File references are unique per generate context, so we use that as the cache key. * * @type {WeakMap<object, Set<string>>} */ const validatorCache = new WeakMap(); /** * @typedef {{ type: "root"} * |{ type: "stringKey", key: string} * |{ type: "dynamicKey", key: string } * } ValidatorPath */ /** * @typedef {object} ValidatorState * @property {import("../generate").GenerateContext} generateContext * @property {string} inputVariableName * @property {string} outputVariableName * @property {string} errorMapVariableName * @property {string} inputTypeName * @property {string} outputTypeName * @property {import("../types/generator").GenerateTypeOptions} outputTypeOptions * @property {number} reusedVariableIndex * @property {ValidatorPath[]} validatedValuePath * @property {import("../generated/common/types") * .ExperimentalReferenceDefinition[] * } dependingValidators * @property {boolean} [jsHasInlineTypes] * @property {boolean} [skipFirstNilCheck] */ /** * Generate all the 'validated' types in the provided structure. This means that, for * example, `defaults` are resolved, and things like `T.date()` are always in the * language native `Date` type. * * We skip `route` ad `crud` since these are not directly usable as types. * * @param {import("../generate").GenerateContext} generateContext */ export function validatorGeneratorGenerateBaseTypes(generateContext) { if (!generateContext.options.generators.validators?.includeBaseTypes) { return; } for (const type of structureNamedTypes(generateContext.structure)) { if (type.type === "route" || type.type === "crud") { continue; } validatorGeneratorGenerateValidator(generateContext, type, { validatorState: "output", nameSuffixes: { input: "Input", output: "Validated", }, targets: [generateContext.options.targetLanguage], }); } } /** * Provides the validator function name and adds the import to the provided file for the * type. * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType< * import("../generated/common/types").ExperimentalTypeSystemDefinition * >} type * @param {string} outputTypeName * @returns {string} */ export function validatorGetNameAndImport( generateContext, file, type, outputTypeName, ) { // @ts-expect-error return targetLanguageSwitch( generateContext, { js: validatorJavascriptGetNameAndImport, ts: validatorTypescriptGetNameAndImport, }, [file, type, outputTypeName], ); } /** * Generate a named type for the target language. Skips if the cache already has a name * registered for the provided type and options. * * TODO: Expand docs * * - How to use the types * - Duplication * - Resolved & unique names * * @param {import("../generate").GenerateContext} generateContext * @param {import("../types").NamedType< * import("../generated/common/types").ExperimentalTypeSystemDefinition * >} type * @param {import("../types/generator").GenerateTypeOptions & { * preferInputBaseName?: boolean; * }} outputTypeOptions */ export function validatorGeneratorGenerateValidator( generateContext, type, outputTypeOptions, ) { /** @type {import("../types/generator").GenerateTypeOptions} */ const inputTypeOptions = { ...outputTypeOptions, validatorState: "input", }; // Prepare the types that we are using, this way we can fetch the name from the type // cache. if (outputTypeOptions.preferInputBaseName) { // Generate the input type first, this way it can get the 'normal' name without a suffix, improving usability typesGeneratorGenerateNamedType(generateContext, type, inputTypeOptions); typesGeneratorGenerateNamedType(generateContext, type, outputTypeOptions); } else { typesGeneratorGenerateNamedType(generateContext, type, outputTypeOptions); typesGeneratorGenerateNamedType(generateContext, type, inputTypeOptions); } const outputTypeName = typesCacheGet( generateContext, type, outputTypeOptions, ); const inputTypeName = typesCacheGet(generateContext, type, inputTypeOptions); if (!outputTypeName || !inputTypeName) { throw AppError.serverError({ message: "Could not resolve type name", outputTypeOptions, inputTypeName, outputTypeName, type, }); } const file = targetLanguageSwitch( generateContext, { js: validatorJavascriptGetFile, ts: validatorTypescriptGetFile, }, [generateContext, type], ); if (!file) { throw AppError.serverError({ message: `Could not resolve validator file for ${stringFormatNameForError( type, )}.`, }); } if (validatorCache.get(file)?.has(outputTypeName)) { return; } if (!validatorCache.has(file)) { validatorCache.set(file, new Set([outputTypeName])); } else { validatorCache.get(file)?.add(outputTypeName); } /** * @type {ValidatorState} */ const validatorState = { generateContext, reusedVariableIndex: 0, inputVariableName: "value", outputVariableName: "result", errorMapVariableName: "errorMap", validatedValuePath: [{ type: "root" }], inputTypeName, outputTypeName, outputTypeOptions, dependingValidators: [], jsHasInlineTypes: generateContext.options.targetLanguage === "ts", }; targetLanguageSwitch( generateContext, { js: validatorJavascriptStartValidator, ts: validatorTypescriptStartValidator, }, [generateContext, file, type, validatorState], ); validatorGeneratorGenerateBody(generateContext, file, type, validatorState); targetLanguageSwitch( generateContext, { js: validatorJavascriptStopValidator, ts: validatorJavascriptStopValidator, }, [generateContext, file, validatorState], ); for (const dependingValidator of validatorState.dependingValidators) { const ref = structureResolveReference( generateContext.structure, dependingValidator, ); validatorGeneratorGenerateValidator( generateContext, // @ts-ignore-error // // Ref is always a system type here ref, outputTypeOptions, ); } } /** * Generate the body of a validator. This function should be called and work for * recursive types as well. * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ export function validatorGeneratorGenerateBody( generateContext, file, type, validatorState, ) { const skipNilCheck = validatorState.skipFirstNilCheck === true; validatorState.skipFirstNilCheck = undefined; if (!skipNilCheck) { targetLanguageSwitch( generateContext, { js: validatorJavascriptNilCheck, ts: validatorJavascriptNilCheck, }, [ file, validatorState, { isOptional: typesOptionalityIsOptional(generateContext, type, { validatorState: "input", }), defaultValue: referenceUtilsGetProperty(generateContext, type, [ "defaultValue", ]), allowNull: referenceUtilsGetProperty( generateContext, type, ["validator", "allowNull"], false, ), }, ], ); } switch (type.type) { case "any": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorAny(generateContext, file, type, validatorState); break; case "anyOf": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorAnyOf(generateContext, file, type, validatorState); break; case "array": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorArray(generateContext, file, type, validatorState); break; case "boolean": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorBoolean(generateContext, file, type, validatorState); break; case "date": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorDate(generateContext, file, type, validatorState); break; case "file": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorFile(generateContext, file, type, validatorState); break; case "generic": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorGeneric(generateContext, file, type, validatorState); break; case "number": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorNumber(generateContext, file, type, validatorState); break; case "object": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorObject(generateContext, file, type, validatorState); break; case "reference": validatorState.dependingValidators.push(type); // @ts-ignore-error // // Ref is always a system type here validatorGeneratorReference(generateContext, file, type, validatorState); break; case "string": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorString(generateContext, file, type, validatorState); break; case "uuid": // @ts-ignore-error // // Ref is always a system type here validatorGeneratorUuid(generateContext, file, type, validatorState); break; } if (!skipNilCheck) { targetLanguageSwitch( generateContext, { js: validatorJavascriptFinishElseBlock, ts: validatorJavascriptFinishElseBlock, }, [file], ); } } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorAny(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptAny, ts: validatorJavascriptAny, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorAnyOf(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptAnyOf, ts: validatorJavascriptAnyOf, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorArray(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptArray, ts: validatorJavascriptArray, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorBoolean( generateContext, file, type, validatorState, ) { targetLanguageSwitch( generateContext, { js: validatorJavascriptBoolean, ts: validatorJavascriptBoolean, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorDate(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptDate, ts: validatorJavascriptDate, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorFile(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptFile, ts: validatorJavascriptFile, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorGeneric( generateContext, file, type, validatorState, ) { targetLanguageSwitch( generateContext, { js: validatorJavascriptGeneric, ts: validatorJavascriptGeneric, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorNumber(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptNumber, ts: validatorJavascriptNumber, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorObject(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptObject, ts: validatorJavascriptObject, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorReference( generateContext, file, type, validatorState, ) { targetLanguageSwitch( generateContext, { js: validatorJavascriptReference, ts: validatorJavascriptReference, }, // @ts-ignore-error // // Ref is always a system type here [generateContext, file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorString(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptString, ts: validatorJavascriptString, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); } /** * * @param {import("../generate").GenerateContext} generateContext * @param {import("../file/context").GenerateFile} file * @param {import("../types").NamedType<import("../generated/common/types").ExperimentalTypeSystemDefinition>} type * @param {ValidatorState} validatorState */ function validatorGeneratorUuid(generateContext, file, type, validatorState) { targetLanguageSwitch( generateContext, { js: validatorJavascriptUuid, ts: validatorJavascriptUuid, }, // @ts-ignore-error // // Ref is always a system type here [file, type, validatorState], ); }