From bbec5f9241685fdc3827decfeb01428910e98597 Mon Sep 17 00:00:00 2001 From: Carson Full Date: Mon, 4 Mar 2024 16:58:59 -0600 Subject: [PATCH] Refactor walkCodec to accept custom generators for codecs --- .../driver/src/reflection/analyzeQuery.ts | 269 +++++++++++------- 1 file changed, 169 insertions(+), 100 deletions(-) diff --git a/packages/driver/src/reflection/analyzeQuery.ts b/packages/driver/src/reflection/analyzeQuery.ts index 8b24ff8e2..19ec5407c 100644 --- a/packages/driver/src/reflection/analyzeQuery.ts +++ b/packages/driver/src/reflection/analyzeQuery.ts @@ -26,134 +26,203 @@ export async function analyzeQuery( ): Promise { const { cardinality, in: inCodec, out: outCodec } = await client.parse(query); - const imports = new Set(); - const args = walkCodec(inCodec, { - indent: "", + const args = generateTSTypeFromCodec(inCodec, Cardinality.One, { optionalNulls: true, readonly: true, - imports, }); - - const result = applyCardinalityToTsType( - walkCodec(outCodec, { - indent: "", - optionalNulls: false, - readonly: false, - imports, - }), - cardinality - ); + const result = generateTSTypeFromCodec(outCodec, cardinality); return { - result, - args, + result: result.type, + args: args.type, cardinality, query, - imports, + imports: new Set([...args.imports, ...result.imports]), }; } -export function applyCardinalityToTsType( - type: string, - cardinality: Cardinality -): string { - switch (cardinality) { - case Cardinality.Many: - return `${type}[]`; - case Cardinality.One: - return type; - case Cardinality.AtMostOne: - return `${type} | null`; - case Cardinality.AtLeastOne: - return `[(${type}), ...(${type})[]]`; - } - throw Error(`unexpected cardinality: ${cardinality}`); -} +type AbstractClass = (abstract new (...arguments_: any[]) => T) & { + prototype: T; +}; + +type CodecLike = ICodec | ScalarCodec; + +export type CodecGenerator = ( + codec: Codec, + context: CodecGeneratorContext +) => string; -// type AtLeastOne = [T, ...T[]]; +type CodecGeneratorMap = ReadonlyMap, CodecGenerator>; + +export type CodecGeneratorContext = { + indent: string; + optionalNulls: boolean; + readonly: boolean; + imports: Set; + walk: (codec: CodecLike, context?: CodecGeneratorContext) => string; + generators: CodecGeneratorMap; + applyCardinality: (type: string, cardinality: Cardinality) => string; +}; -export { walkCodec as walkCodecToTsType }; -function walkCodec( +export type CodecGenerationOptions = Partial< + Pick< + CodecGeneratorContext, + "optionalNulls" | "readonly" | "generators" | "applyCardinality" + > +>; + +export const generateTSTypeFromCodec = ( codec: ICodec, - ctx: { - indent: string; - optionalNulls: boolean; - readonly: boolean; - imports: Set; - } -): string { - if (codec instanceof NullCodec) { - return "null"; - } - if (codec instanceof ScalarCodec) { - if (codec instanceof EnumCodec) { - return `(${codec.values.map((val) => JSON.stringify(val)).join(" | ")})`; - } + cardinality: Cardinality = Cardinality.One, + options: CodecGenerationOptions = {} +) => { + const optionsWithDefaults = { + indent: "", + optionalNulls: false, + readonly: false, + ...options, + }; + const context: CodecGeneratorContext = { + ...optionsWithDefaults, + generators: defaultCodecGenerators, + applyCardinality: defaultApplyCardinalityToTsType(optionsWithDefaults), + ...options, + imports: new Set(), + walk: (codec, innerContext) => { + innerContext ??= context; + for (const [type, generator] of innerContext.generators) { + if (codec instanceof type) { + return generator(codec, innerContext); + } + } + throw new Error(`Unexpected codec kind: ${codec.getKind()}`); + }, + }; + const type = context.applyCardinality( + context.walk(codec, context), + cardinality + ); + return { + type, + imports: context.imports, + }; +}; + +/** A helper function to define a codec generator tuple. */ +const genDef = ( + codecType: AbstractClass, + generator: CodecGenerator +) => + [codecType as AbstractClass, generator as CodecGenerator] as const; +export { genDef as defineCodecGeneratorTuple }; + +export const defaultCodecGenerators: CodecGeneratorMap = new Map([ + genDef(NullCodec, () => "null"), + genDef(EnumCodec, (codec: EnumCodec) => { + return `(${codec.values.map((val) => JSON.stringify(val)).join(" | ")})`; + }), + genDef(ScalarCodec, (codec, ctx) => { if (codec.importedType) { ctx.imports.add(codec.tsType); } return codec.tsType; - } - if (codec instanceof ObjectCodec || codec instanceof NamedTupleCodec) { - const fields = - codec instanceof ObjectCodec - ? codec.getFields() - : codec.getNames().map((name) => ({ name, cardinality: undefined })); + }), + genDef(ObjectCodec, (codec, ctx) => { const subCodecs = codec.getSubcodecs(); - const objectShape = `{\n${fields - .map((field, i) => { - const cardinality = field.cardinality - ? util.parseCardinality(field.cardinality) - : Cardinality.One; - let subCodec = subCodecs[i]; - if (subCodec instanceof SetCodec) { - if ( - !( - cardinality === Cardinality.Many || - cardinality === Cardinality.AtLeastOne - ) - ) { - throw Error("subcodec is SetCodec, but upper cardinality is one"); - } - subCodec = subCodec.getSubcodecs()[0]; - } - return `${ctx.indent} ${JSON.stringify(field.name)}${ - ctx.optionalNulls && cardinality === Cardinality.AtMostOne ? "?" : "" - }: ${applyCardinalityToTsType( - walkCodec(subCodec, { ...ctx, indent: ctx.indent + " " }), - cardinality - )};`; - }) - .join("\n")}\n${ctx.indent}}`; - return ctx.readonly ? `Readonly<${objectShape}>` : objectShape; - } - if (codec instanceof ArrayCodec) { - return `${ctx.readonly ? "readonly " : ""}${walkCodec( - codec.getSubcodecs()[0], - ctx - )}[]`; - } - if (codec instanceof TupleCodec) { - return `${ctx.readonly ? "readonly " : ""}[${codec + const fields = codec.getFields().map((field, i) => ({ + name: field.name, + codec: subCodecs[i], + cardinality: util.parseCardinality(field.cardinality), + })); + return generateTsObject(fields, ctx); + }), + genDef(NamedTupleCodec, (codec, ctx) => { + const subCodecs = codec.getSubcodecs(); + const fields = codec.getNames().map((name, i) => ({ + name, + codec: subCodecs[i], + cardinality: Cardinality.One, + })); + return generateTsObject(fields, ctx); + }), + genDef(TupleCodec, (codec, ctx) => { + const subCodecs = codec .getSubcodecs() - .map((subCodec) => walkCodec(subCodec, ctx)) - .join(", ")}]`; - } - if (codec instanceof RangeCodec) { + .map((subCodec: CodecLike) => ctx.walk(subCodec)); + return `${ctx.readonly ? "readonly " : ""}[${subCodecs.join(", ")}]`; + }), + genDef(ArrayCodec, (codec, ctx) => + ctx.applyCardinality(ctx.walk(codec.getSubcodecs()[0]), Cardinality.Many) + ), + genDef(RangeCodec, (codec, ctx) => { const subCodec = codec.getSubcodecs()[0]; if (!(subCodec instanceof ScalarCodec)) { throw Error("expected range subtype to be scalar type"); } ctx.imports.add("Range"); - return `Range<${walkCodec(subCodec, ctx)}>`; - } - if (codec instanceof MultiRangeCodec) { + return `Range<${ctx.walk(subCodec)}>`; + }), + genDef(MultiRangeCodec, (codec, ctx) => { const subCodec = codec.getSubcodecs()[0]; if (!(subCodec instanceof ScalarCodec)) { throw Error("expected multirange subtype to be scalar type"); } ctx.imports.add("MultiRange"); - return `MultiRange<${walkCodec(subCodec, ctx)}>`; + return `MultiRange<${ctx.walk(subCodec)}>`; + }), +]); + +export const generateTsObject = ( + fields: Array[0]>, + ctx: CodecGeneratorContext +) => { + const properties = fields.map((field) => generateTsObjectField(field, ctx)); + return `{\n${properties.join("\n")}\n${ctx.indent}}`; +}; + +export const generateTsObjectField = ( + field: { name: string; cardinality: Cardinality; codec: ICodec }, + ctx: CodecGeneratorContext +) => { + const codec = unwrapSetCodec(field.codec, field.cardinality); + + const name = JSON.stringify(field.name); + const value = ctx.applyCardinality( + ctx.walk(codec, { ...ctx, indent: ctx.indent + " " }), + field.cardinality + ); + const optional = + ctx.optionalNulls && field.cardinality === Cardinality.AtMostOne; + const questionMark = optional ? "?" : ""; + const isReadonly = ctx.readonly ? "readonly " : ""; + return `${ctx.indent} ${isReadonly}${name}${questionMark}: ${value};`; +}; + +function unwrapSetCodec(codec: ICodec, cardinality: Cardinality) { + if (!(codec instanceof SetCodec)) { + return codec; + } + if ( + cardinality === Cardinality.Many || + cardinality === Cardinality.AtLeastOne + ) { + return codec.getSubcodecs()[0]; } - throw Error(`Unexpected codec kind: ${codec.getKind()}`); + throw new Error("Sub-codec is SetCodec, but upper cardinality is one"); } + +export const defaultApplyCardinalityToTsType = + (ctx: Pick) => + (type: string, cardinality: Cardinality): string => { + switch (cardinality) { + case Cardinality.Many: + return `${ctx.readonly ? "readonly " : ""}${type}[]`; + case Cardinality.One: + return type; + case Cardinality.AtMostOne: + return `${type} | null`; + case Cardinality.AtLeastOne: + return `${ctx.readonly ? "readonly " : ""}[(${type}), ...(${type})[]]`; + } + throw new Error(`Unexpected cardinality: ${cardinality}`); + };