Skip to content

Commit 164f3bb

Browse files
authored
feat: Command classBuilder (#1118)
* feat: add Command ClassBuilder * fix: add dependency check script * add changeset * rename script file * fix yarn lock
1 parent f0ff3b7 commit 164f3bb

File tree

9 files changed

+340
-4
lines changed

9 files changed

+340
-4
lines changed

.changeset/cool-vans-tan.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@smithy/smithy-client": minor
3+
---
4+
5+
add Command classBuilder

.changeset/violet-drinks-judge.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/experimental-identity-and-auth": patch
3+
"@smithy/smithy-client": patch
4+
---
5+
6+
add missing dependency declarations

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"test:integration": "yarn build-test-packages && turbo run test:integration",
1212
"lint": "turbo run lint",
1313
"lint-fix": "turbo run lint -- --fix",
14-
"lint:pkgJson": "node scripts/check-dev-dependencies.js",
14+
"lint:pkgJson": "node scripts/check-dependencies.js",
1515
"format": "turbo run format --parallel",
1616
"stage-release": "turbo run stage-release",
1717
"extract:docs": "mkdir -p api-extractor-packages && turbo run extract:docs",

packages/experimental-identity-and-auth/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"@smithy/protocol-http": "workspace:^",
3131
"@smithy/signature-v4": "workspace:^",
3232
"@smithy/types": "workspace:^",
33+
"@smithy/util-middleware": "workspace:^",
3334
"tslib": "^2.5.0"
3435
},
3536
"engines": {

packages/smithy-client/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@
2323
},
2424
"license": "Apache-2.0",
2525
"dependencies": {
26+
"@smithy/middleware-endpoint": "workspace:^",
2627
"@smithy/middleware-stack": "workspace:^",
28+
"@smithy/protocol-http": "workspace:^",
2729
"@smithy/types": "workspace:^",
2830
"@smithy/util-stream": "workspace:^",
2931
"tslib": "^2.5.0"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Command } from "./command";
2+
3+
describe(Command.name, () => {
4+
it("implements a classBuilder", async () => {
5+
class MyCommand extends Command.classBuilder<any, any, any, any, any>()
6+
.ep({
7+
Endpoint: { type: "builtInParams", name: "Endpoint" },
8+
})
9+
.m(function () {
10+
return [];
11+
})
12+
.s("SmithyMyClient", "SmithyMyOperation", {})
13+
.n("MyClient", "MyCommand")
14+
.f()
15+
.ser(async (_) => _)
16+
.de(async (_) => _)
17+
.build() {}
18+
19+
const myCommand = new MyCommand({
20+
Prop: "prop1",
21+
});
22+
23+
expect(myCommand).toBeInstanceOf(Command);
24+
expect(myCommand).toBeInstanceOf(MyCommand);
25+
expect(MyCommand.getEndpointParameterInstructions()).toEqual({
26+
Endpoint: { type: "builtInParams", name: "Endpoint" },
27+
});
28+
expect(myCommand.input).toEqual({
29+
Prop: "prop1",
30+
});
31+
32+
// private method exists for compatibility
33+
expect((myCommand as any).serialize).toBeDefined();
34+
35+
// private method exists for compatibility
36+
expect((myCommand as any).deserialize).toBeDefined();
37+
});
38+
});

packages/smithy-client/src/command.ts

Lines changed: 269 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,21 @@
1+
import type { EndpointParameterInstructions } from "@smithy/middleware-endpoint";
12
import { constructStack } from "@smithy/middleware-stack";
2-
import { Command as ICommand, Handler, MetadataBearer, MiddlewareStack as IMiddlewareStack } from "@smithy/types";
3+
import type { HttpRequest } from "@smithy/protocol-http";
4+
import type {
5+
Command as ICommand,
6+
FinalizeHandlerArguments,
7+
Handler,
8+
HandlerExecutionContext,
9+
HttpRequest as IHttpRequest,
10+
HttpResponse as IHttpResponse,
11+
Logger,
12+
MetadataBearer,
13+
MiddlewareStack as IMiddlewareStack,
14+
Pluggable,
15+
RequestHandler,
16+
SerdeContext,
17+
} from "@smithy/types";
18+
import { SMITHY_CONTEXT_KEY } from "@smithy/types";
319

420
/**
521
* @public
@@ -11,11 +27,261 @@ export abstract class Command<
1127
ClientInput extends object = any,
1228
ClientOutput extends MetadataBearer = any
1329
> implements ICommand<ClientInput, Input, ClientOutput, Output, ResolvedClientConfiguration> {
14-
abstract input: Input;
15-
readonly middlewareStack: IMiddlewareStack<Input, Output> = constructStack<Input, Output>();
30+
public abstract input: Input;
31+
public readonly middlewareStack: IMiddlewareStack<Input, Output> = constructStack<Input, Output>();
32+
33+
/**
34+
* Factory for Command ClassBuilder.
35+
* @internal
36+
*/
37+
public static classBuilder<
38+
I extends SI,
39+
O extends SO,
40+
C extends { logger: Logger; requestHandler: RequestHandler<any, any, any> },
41+
SI extends object = any,
42+
SO extends MetadataBearer = any
43+
>() {
44+
return new ClassBuilder<I, O, C, SI, SO>();
45+
}
46+
1647
abstract resolveMiddleware(
1748
stack: IMiddlewareStack<ClientInput, ClientOutput>,
1849
configuration: ResolvedClientConfiguration,
1950
options: any
2051
): Handler<Input, Output>;
52+
53+
/**
54+
* @internal
55+
*/
56+
public resolveMiddlewareWithContext(
57+
clientStack: IMiddlewareStack<any, any>,
58+
configuration: { logger: Logger; requestHandler: RequestHandler<any, any, any> },
59+
options: any,
60+
{
61+
middlewareFn,
62+
clientName,
63+
commandName,
64+
inputFilterSensitiveLog,
65+
outputFilterSensitiveLog,
66+
smithyContext,
67+
additionalContext,
68+
CommandCtor,
69+
}: ResolveMiddlewareContextArgs
70+
) {
71+
for (const mw of middlewareFn.bind(this)(CommandCtor, clientStack, configuration, options)) {
72+
this.middlewareStack.use(mw);
73+
}
74+
const stack = clientStack.concat(this.middlewareStack);
75+
const { logger } = configuration;
76+
const handlerExecutionContext: HandlerExecutionContext = {
77+
logger,
78+
clientName,
79+
commandName,
80+
inputFilterSensitiveLog,
81+
outputFilterSensitiveLog,
82+
[SMITHY_CONTEXT_KEY]: {
83+
...smithyContext,
84+
},
85+
...additionalContext,
86+
};
87+
const { requestHandler } = configuration;
88+
return stack.resolve(
89+
(request: FinalizeHandlerArguments<any>) => requestHandler.handle(request.request as HttpRequest, options || {}),
90+
handlerExecutionContext
91+
);
92+
}
93+
}
94+
95+
/**
96+
* @internal
97+
*/
98+
type ResolveMiddlewareContextArgs = {
99+
middlewareFn: (CommandCtor: any, clientStack: any, config: any, options: any) => Pluggable<any, any>[];
100+
clientName: string;
101+
commandName: string;
102+
smithyContext: Record<string, unknown>;
103+
additionalContext: HandlerExecutionContext;
104+
inputFilterSensitiveLog: (_: any) => any;
105+
outputFilterSensitiveLog: (_: any) => any;
106+
CommandCtor: any /* Command constructor */;
107+
};
108+
109+
/**
110+
* @internal
111+
*/
112+
class ClassBuilder<
113+
I extends SI,
114+
O extends SO,
115+
C extends { logger: Logger; requestHandler: RequestHandler<any, any, any> },
116+
SI extends object = any,
117+
SO extends MetadataBearer = any
118+
> {
119+
private _init: (_: Command<I, O, C, SI, SO>) => void = () => {};
120+
private _ep: EndpointParameterInstructions = {};
121+
private _middlewareFn: (
122+
CommandCtor: any,
123+
clientStack: any,
124+
config: any,
125+
options: any
126+
) => Pluggable<any, any>[] = () => [];
127+
private _commandName = "";
128+
private _clientName = "";
129+
private _additionalContext = {} as HandlerExecutionContext;
130+
private _smithyContext = {} as Record<string, unknown>;
131+
private _inputFilterSensitiveLog = (_: any) => _;
132+
private _outputFilterSensitiveLog = (_: any) => _;
133+
private _serializer: (input: I, context: SerdeContext | any) => Promise<IHttpRequest> = null as any;
134+
private _deserializer: (output: IHttpResponse, context: SerdeContext | any) => Promise<O> = null as any;
135+
/**
136+
* Optional init callback.
137+
*/
138+
public init(cb: (_: Command<I, O, C, SI, SO>) => void) {
139+
this._init = cb;
140+
}
141+
/**
142+
* Set the endpoint parameter instructions.
143+
*/
144+
public ep(endpointParameterInstructions: EndpointParameterInstructions): ClassBuilder<I, O, C, SI, SO> {
145+
this._ep = endpointParameterInstructions;
146+
return this;
147+
}
148+
/**
149+
* Add any number of middleware.
150+
*/
151+
public m(
152+
middlewareSupplier: (CommandCtor: any, clientStack: any, config: any, options: any) => Pluggable<any, any>[]
153+
): ClassBuilder<I, O, C, SI, SO> {
154+
this._middlewareFn = middlewareSupplier;
155+
return this;
156+
}
157+
/**
158+
* Set the initial handler execution context Smithy field.
159+
*/
160+
public s(
161+
service: string,
162+
operation: string,
163+
smithyContext: Record<string, unknown> = {}
164+
): ClassBuilder<I, O, C, SI, SO> {
165+
this._smithyContext = {
166+
service,
167+
operation,
168+
...smithyContext,
169+
};
170+
return this;
171+
}
172+
/**
173+
* Set the initial handler execution context.
174+
*/
175+
public c(additionalContext: HandlerExecutionContext = {}): ClassBuilder<I, O, C, SI, SO> {
176+
this._additionalContext = additionalContext;
177+
return this;
178+
}
179+
/**
180+
* Set constant string identifiers for the operation.
181+
*/
182+
public n(clientName: string, commandName: string): ClassBuilder<I, O, C, SI, SO> {
183+
this._clientName = clientName;
184+
this._commandName = commandName;
185+
return this;
186+
}
187+
/**
188+
* Set the input and output sensistive log filters.
189+
*/
190+
public f(
191+
inputFilter: (_: any) => any = (_) => _,
192+
outputFilter: (_: any) => any = (_) => _
193+
): ClassBuilder<I, O, C, SI, SO> {
194+
this._inputFilterSensitiveLog = inputFilter;
195+
this._outputFilterSensitiveLog = outputFilter;
196+
return this;
197+
}
198+
/**
199+
* Sets the serializer.
200+
*/
201+
public ser(
202+
serializer: (input: I, context?: SerdeContext | any) => Promise<IHttpRequest>
203+
): ClassBuilder<I, O, C, SI, SO> {
204+
this._serializer = serializer;
205+
return this;
206+
}
207+
/**
208+
* Sets the deserializer.
209+
*/
210+
public de(
211+
deserializer: (output: IHttpResponse, context?: SerdeContext | any) => Promise<O>
212+
): ClassBuilder<I, O, C, SI, SO> {
213+
this._deserializer = deserializer;
214+
return this;
215+
}
216+
/**
217+
* @returns a Command class with the classBuilder properties.
218+
*/
219+
public build(): {
220+
new (input: I): CommandImpl<I, O, C, SI, SO>;
221+
getEndpointParameterInstructions(): EndpointParameterInstructions;
222+
} {
223+
// eslint-disable-next-line @typescript-eslint/no-this-alias
224+
const closure = this;
225+
let CommandRef: any;
226+
227+
return (CommandRef = class extends Command<I, O, C, SI, SO> {
228+
/**
229+
* @public
230+
*/
231+
public static getEndpointParameterInstructions(): EndpointParameterInstructions {
232+
return closure._ep;
233+
}
234+
235+
/**
236+
* @public
237+
*/
238+
public constructor(readonly input: I) {
239+
super();
240+
closure._init(this);
241+
}
242+
243+
/**
244+
* @internal
245+
*/
246+
public resolveMiddleware(stack: IMiddlewareStack<any, any>, configuration: C, options: any): Handler<any, any> {
247+
return this.resolveMiddlewareWithContext(stack, configuration, options, {
248+
CommandCtor: CommandRef,
249+
middlewareFn: closure._middlewareFn,
250+
clientName: closure._clientName,
251+
commandName: closure._commandName,
252+
inputFilterSensitiveLog: closure._inputFilterSensitiveLog,
253+
outputFilterSensitiveLog: closure._outputFilterSensitiveLog,
254+
smithyContext: closure._smithyContext,
255+
additionalContext: closure._additionalContext,
256+
});
257+
}
258+
259+
/**
260+
* @internal
261+
*/
262+
// @ts-ignore used in middlewareFn closure.
263+
public serialize = closure._serializer;
264+
265+
/**
266+
* @internal
267+
*/
268+
// @ts-ignore used in middlewareFn closure.
269+
public deserialize = closure._deserializer;
270+
});
271+
}
272+
}
273+
274+
/**
275+
* A concrete implementation of ICommand with no abstract members.
276+
* @public
277+
*/
278+
export interface CommandImpl<
279+
I extends SI,
280+
O extends SO,
281+
C extends { logger: Logger; requestHandler: RequestHandler<any, any, any> },
282+
SI extends object = any,
283+
SO extends MetadataBearer = any
284+
> extends Command<I, O, C, SI, SO> {
285+
readonly input: I;
286+
resolveMiddleware(stack: IMiddlewareStack<SI, SO>, configuration: C, options: any): Handler<I, O>;
21287
}

scripts/check-dev-dependencies.js renamed to scripts/check-dependencies.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,21 @@ const pkgJsonEnforcement = require("./package-json-enforcement");
3030
continue;
3131
}
3232

33+
const importedDependencies = [];
34+
importedDependencies.push(
35+
...new Set(
36+
[...(contents.toString().match(/from "(@(aws-sdk|smithy)\/.*?)"/g) || [])]
37+
.slice(1)
38+
.map((_) => _.replace(/from "/g, "").replace(/"$/, ""))
39+
)
40+
);
41+
42+
for (const dependency of importedDependencies) {
43+
if (!(dependency in pkgJson.dependencies) && dependency !== pkgJson.name) {
44+
errors.push(`${dependency} undeclared but imported in ${pkgJson.name} ${file}}`);
45+
}
46+
}
47+
3348
for (const [dep, version] of Object.entries(pkgJson.devDependencies ?? {})) {
3449
if (dep.startsWith("@smithy/") && contents.includes(`from "${dep}";`)) {
3550
console.warn(`${dep} incorrectly declared in devDependencies of ${folder}`);

0 commit comments

Comments
 (0)