Skip to content

Commit

Permalink
ref(v22): parseRequest() and substitute() functions (#2303)
Browse files Browse the repository at this point in the history
This should extract the refactoring and general improvement from #2280
  • Loading branch information
RobinTail authored Jan 13, 2025
1 parent 0bd6289 commit ebf8b47
Show file tree
Hide file tree
Showing 8 changed files with 253 additions and 279 deletions.
11 changes: 6 additions & 5 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,10 +85,6 @@ const tsFactoryConcerns = [
"[arguments.0.callee.property.name='createPropertyAccessExpression']",
message: "use makePropCall() helper",
},
{
selector: "Identifier[name='AmpersandAmpersandToken']",
message: "use makeAnd() helper",
},
{
selector: "Identifier[name='EqualsEqualsEqualsToken']",
message: "use makeEqual() helper",
Expand All @@ -109,6 +105,11 @@ const tsFactoryConcerns = [
selector: "Literal[value='Promise']",
message: "use makePromise() helper",
},
{
selector:
"CallExpression[callee.property.name='createTypeReferenceNode'][arguments.length=1]",
message: "use ensureTypeNode() helper",
},
];

export default tsPlugin.config(
Expand Down Expand Up @@ -155,7 +156,7 @@ export default tsPlugin.config(
},
{
name: "source/integration",
files: ["src/integration.ts", "src/zts.ts"],
files: ["src/integration.ts", "src/integration-base.ts", "src/zts.ts"],
rules: {
"no-restricted-syntax": [
"warn",
Expand Down
35 changes: 16 additions & 19 deletions example/example.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,20 @@ export const endpointTags = {
"get /v1/events/time": ["subscriptions"],
};

const parseRequest = (request: string) =>
request.split(/ (.+)/, 2) as [Method, Path];

const substitute = (path: string, params: Record<string, any>) => {
const rest = { ...params };
for (const key in params) {
path = path.replace(`:${key}`, () => {
delete rest[key];
return params[key];
});
}
return [path, rest] as const;
};

export type Implementation = (
method: Method,
path: string,
Expand All @@ -426,25 +440,8 @@ export class ExpressZodAPIClient {
request: K,
params: Input[K],
): Promise<Response[K]> {
const [method, path] = request.split(/ (.+)/, 2) as [Method, Path];
return this.implementation(
method,
Object.keys(params).reduce(
(acc, key) =>
acc.replace(`:${key}`, (params as Record<string, any>)[key]),
path,
),
Object.keys(params).reduce(
(acc, key) =>
Object.assign(
acc,
!path.includes(`:${key}`) && {
[key]: (params as Record<string, any>)[key],
},
),
{},
),
);
const [method, path] = parseRequest(request);
return this.implementation(method, ...substitute(path, params));
}
}

Expand Down
198 changes: 113 additions & 85 deletions src/integration-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { ResponseVariant } from "./api-response";
import { contentTypes } from "./content-type";
import { Method, methods } from "./method";
import {
accessModifiers,
ensureTypeNode,
f,
makeAnd,
makeArrowFn,
makeConst,
makeDeconstruction,
Expand All @@ -13,7 +14,6 @@ import {
makeInterfaceProp,
makeKeyOf,
makeNew,
makeObjectKeysReducer,
makeParam,
makeParams,
makePromise,
Expand All @@ -26,7 +26,6 @@ import {
makeTernary,
makeType,
propOf,
protectedReadonlyModifier,
recordStringAny,
} from "./typescript-api";

Expand Down Expand Up @@ -54,14 +53,16 @@ export abstract class IntegrationBase {
paramsArgument: f.createIdentifier("params"),
methodParameter: f.createIdentifier("method"),
requestParameter: f.createIdentifier("request"),
accumulator: f.createIdentifier("acc"),
parseRequestFn: f.createIdentifier("parseRequest"),
substituteFn: f.createIdentifier("substitute"),
provideMethod: f.createIdentifier("provide"),
implementationArgument: f.createIdentifier("implementation"),
headersProperty: f.createIdentifier("headers"),
hasBodyConst: f.createIdentifier("hasBody"),
undefinedValue: f.createIdentifier("undefined"),
bodyProperty: f.createIdentifier("body"),
responseConst: f.createIdentifier("response"),
restConst: f.createIdentifier("rest"),
searchParamsConst: f.createIdentifier("searchParams"),
exampleImplementationConst: f.createIdentifier("exampleImplementation"),
clientConst: f.createIdentifier("client"),
Expand All @@ -86,10 +87,7 @@ export abstract class IntegrationBase {
// type SomeOf<T> = T[keyof T];
protected someOfType = makeType(
"SomeOf",
f.createIndexedAccessTypeNode(
f.createTypeReferenceNode("T"),
makeKeyOf("T"),
),
f.createIndexedAccessTypeNode(ensureTypeNode("T"), makeKeyOf("T")),
{ params: { T: undefined } },
);

Expand All @@ -104,9 +102,7 @@ export abstract class IntegrationBase {

/** @example SomeOf<_> */
protected someOf = ({ name }: ts.TypeAliasDeclaration) =>
f.createTypeReferenceNode(this.someOfType.name, [
f.createTypeReferenceNode(name),
]);
f.createTypeReferenceNode(this.someOfType.name, [ensureTypeNode(name)]);

// export type Path = "/v1/user/retrieve" | ___;
protected makePathType = () =>
Expand Down Expand Up @@ -148,9 +144,7 @@ export abstract class IntegrationBase {
f.createFunctionTypeNode(
undefined,
makeParams({
[this.ids.methodParameter.text]: f.createTypeReferenceNode(
this.ids.methodType,
),
[this.ids.methodParameter.text]: ensureTypeNode(this.ids.methodType),
[this.ids.pathParameter.text]: f.createKeywordTypeNode(
ts.SyntaxKind.StringKeyword,
),
Expand All @@ -161,106 +155,143 @@ export abstract class IntegrationBase {
{ expose: true },
);

// public provide<K extends MethodPath>(request: K, params: Input[K]): Promise<Response[K]> {}
private makeProvider = () => {
// `:${key}`
const keyParamExpression = makeTemplate(":", [this.ids.keyParameter]);

// Object.keys(params).reduce((acc, key) => acc.replace(___, params[key]), path)
const pathArgument = makeObjectKeysReducer(
this.ids.paramsArgument,
makePropCall(this.ids.accumulator, propOf<string>("replace"), [
keyParamExpression,
f.createElementAccessExpression(
f.createAsExpression(this.ids.paramsArgument, recordStringAny),
this.ids.keyParameter,
// const parseRequest = (request: string) => request.split(/ (.+)/, 2) as [Method, Path];
protected makeParseRequestFn = () =>
makeConst(
this.ids.parseRequestFn,
makeArrowFn(
{
[this.ids.requestParameter.text]: f.createKeywordTypeNode(
ts.SyntaxKind.StringKeyword,
),
},
f.createAsExpression(
makePropCall(this.ids.requestParameter, propOf<string>("split"), [
f.createRegularExpressionLiteral("/ (.+)/"), // split once
f.createNumericLiteral(2), // excludes third empty element
]),
f.createTupleTypeNode([
ensureTypeNode(this.ids.methodType),
ensureTypeNode(this.ids.pathType),
]),
),
]),
this.ids.pathParameter,
),
);

// Object.keys(params).reduce((acc, key) =>
// Object.assign(acc, !path.includes(`:${key}`) && {[key]: params[key]} ), {})
const paramsArgument = makeObjectKeysReducer(
this.ids.paramsArgument,
makePropCall(
f.createIdentifier(Object.name),
propOf<typeof Object>("assign"),
[
this.ids.accumulator,
makeAnd(
f.createPrefixUnaryExpression(
ts.SyntaxKind.ExclamationToken,
makePropCall(this.ids.pathParameter, propOf<string>("includes"), [
keyParamExpression,
]),
// const substitute = (path: string, params: Record<string, any>) => { ___ return [path, rest] as const; }
protected makeSubstituteFn = () =>
makeConst(
this.ids.substituteFn,
makeArrowFn(
{
[this.ids.pathParameter.text]: f.createKeywordTypeNode(
ts.SyntaxKind.StringKeyword,
),
[this.ids.paramsArgument.text]: recordStringAny,
},
f.createBlock([
makeConst(
this.ids.restConst,
f.createObjectLiteralExpression([
f.createSpreadAssignment(this.ids.paramsArgument),
]),
),
f.createForInStatement(
f.createVariableDeclarationList(
[f.createVariableDeclaration(this.ids.keyParameter)],
ts.NodeFlags.Const,
),
f.createObjectLiteralExpression(
[
f.createPropertyAssignment(
f.createComputedPropertyName(this.ids.keyParameter),
f.createElementAccessExpression(
f.createAsExpression(
this.ids.paramsArgument,
recordStringAny,
),
this.ids.keyParameter,
this.ids.paramsArgument,
f.createBlock([
f.createExpressionStatement(
f.createBinaryExpression(
this.ids.pathParameter,
f.createToken(ts.SyntaxKind.EqualsToken),
makePropCall(
this.ids.pathParameter,
propOf<string>("replace"),
[
makeTemplate(":", [this.ids.keyParameter]), // `:${key}`
makeArrowFn(
[],
f.createBlock([
f.createExpressionStatement(
f.createDeleteExpression(
f.createElementAccessExpression(
f.createIdentifier("rest"),
this.ids.keyParameter,
),
),
),
f.createReturnStatement(
f.createElementAccessExpression(
this.ids.paramsArgument,
this.ids.keyParameter,
),
),
]),
),
],
),
),
],
false,
),
]),
),
f.createReturnStatement(
f.createAsExpression(
f.createArrayLiteralExpression([
this.ids.pathParameter,
this.ids.restConst,
]),
ensureTypeNode("const"),
),
),
],
]),
),
f.createObjectLiteralExpression(),
);

return makePublicMethod(
// public provide<K extends MethodPath>(request: K, params: Input[K]): Promise<Response[K]> {}
private makeProvider = () =>
makePublicMethod(
this.ids.provideMethod,
makeParams({
[this.ids.requestParameter.text]: f.createTypeReferenceNode("K"),
[this.ids.requestParameter.text]: ensureTypeNode("K"),
[this.ids.paramsArgument.text]: f.createIndexedAccessTypeNode(
f.createTypeReferenceNode(this.ids.inputInterface),
f.createTypeReferenceNode("K"),
ensureTypeNode(this.ids.inputInterface),
ensureTypeNode("K"),
),
}),
f.createBlock([
makeConst(
// const [method, path, params] =
// const [method, path] = this.parseRequest(request);
makeDeconstruction(this.ids.methodParameter, this.ids.pathParameter),
// request.split(/ (.+)/, 2) as [Method, Path];
f.createAsExpression(
makePropCall(this.ids.requestParameter, propOf<string>("split"), [
f.createRegularExpressionLiteral("/ (.+)/"), // split once
f.createNumericLiteral(2), // excludes third empty element
]),
f.createTupleTypeNode([
f.createTypeReferenceNode(this.ids.methodType),
f.createTypeReferenceNode(this.ids.pathType),
]),
),
f.createCallExpression(this.ids.parseRequestFn, undefined, [
this.ids.requestParameter,
]),
),
// return this.implementation(___)
f.createReturnStatement(
makePropCall(f.createThis(), this.ids.implementationArgument, [
this.ids.methodParameter,
pathArgument,
paramsArgument,
f.createSpreadElement(
f.createCallExpression(this.ids.substituteFn, undefined, [
this.ids.pathParameter,
this.ids.paramsArgument,
]),
),
]),
),
]),
{
typeParams: { K: this.ids.requestType },
returns: makePromise(
f.createIndexedAccessTypeNode(
f.createTypeReferenceNode(this.ids.responseInterface),
f.createTypeReferenceNode("K"),
ensureTypeNode(this.ids.responseInterface),
ensureTypeNode("K"),
),
),
},
);
};

// export class ExpressZodAPIClient { ___ }
protected makeClientClass = () =>
Expand All @@ -270,8 +301,8 @@ export abstract class IntegrationBase {
makeEmptyInitializingConstructor([
makeParam(
this.ids.implementationArgument,
f.createTypeReferenceNode(this.ids.implementationType),
protectedReadonlyModifier,
ensureTypeNode(this.ids.implementationType),
accessModifiers.protectedReadonly,
),
]),
[this.makeProvider()],
Expand Down Expand Up @@ -429,10 +460,7 @@ export abstract class IntegrationBase {
]),
{ isAsync: true },
),
{
expose: true,
type: f.createTypeReferenceNode(this.ids.implementationType),
},
{ expose: true, type: ensureTypeNode(this.ids.implementationType) },
);
};

Expand Down
Loading

0 comments on commit ebf8b47

Please sign in to comment.