Skip to content

Commit

Permalink
Refactor walkCodec to accept custom generators for codecs
Browse files Browse the repository at this point in the history
  • Loading branch information
CarsonF committed Mar 4, 2024
1 parent 618659f commit bbec5f9
Showing 1 changed file with 169 additions and 100 deletions.
269 changes: 169 additions & 100 deletions packages/driver/src/reflection/analyzeQuery.ts
Expand Up @@ -26,134 +26,203 @@ export async function analyzeQuery(
): Promise<QueryType> {
const { cardinality, in: inCodec, out: outCodec } = await client.parse(query);

const imports = new Set<string>();
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<T> = (abstract new (...arguments_: any[]) => T) & {
prototype: T;
};

type CodecLike = ICodec | ScalarCodec;

export type CodecGenerator<Codec extends CodecLike = CodecLike> = (
codec: Codec,
context: CodecGeneratorContext
) => string;

// type AtLeastOne<T> = [T, ...T[]];
type CodecGeneratorMap = ReadonlyMap<AbstractClass<CodecLike>, CodecGenerator>;

export type CodecGeneratorContext = {
indent: string;
optionalNulls: boolean;
readonly: boolean;
imports: Set<string>;
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>;
}
): 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 = <Codec extends CodecLike>(
codecType: AbstractClass<Codec>,
generator: CodecGenerator<Codec>
) =>
[codecType as AbstractClass<CodecLike>, 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<Parameters<typeof generateTsObjectField>[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<CodecGeneratorContext, "readonly">) =>
(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}`);
};

0 comments on commit bbec5f9

Please sign in to comment.