Skip to content

feat: Command classBuilder #1118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cool-vans-tan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@smithy/smithy-client": minor
---

add Command classBuilder
6 changes: 6 additions & 0 deletions .changeset/violet-drinks-judge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@smithy/experimental-identity-and-auth": patch
"@smithy/smithy-client": patch
---

add missing dependency declarations
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"test:integration": "yarn build-test-packages && turbo run test:integration",
"lint": "turbo run lint",
"lint-fix": "turbo run lint -- --fix",
"lint:pkgJson": "node scripts/check-dev-dependencies.js",
"lint:pkgJson": "node scripts/check-dependencies.js",
"format": "turbo run format --parallel",
"stage-release": "turbo run stage-release",
"extract:docs": "mkdir -p api-extractor-packages && turbo run extract:docs",
Expand Down
1 change: 1 addition & 0 deletions packages/experimental-identity-and-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"@smithy/protocol-http": "workspace:^",
"@smithy/signature-v4": "workspace:^",
"@smithy/types": "workspace:^",
"@smithy/util-middleware": "workspace:^",
"tslib": "^2.5.0"
},
"engines": {
Expand Down
2 changes: 2 additions & 0 deletions packages/smithy-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
},
"license": "Apache-2.0",
"dependencies": {
"@smithy/middleware-endpoint": "workspace:^",
"@smithy/middleware-stack": "workspace:^",
"@smithy/protocol-http": "workspace:^",
"@smithy/types": "workspace:^",
"@smithy/util-stream": "workspace:^",
"tslib": "^2.5.0"
Expand Down
38 changes: 38 additions & 0 deletions packages/smithy-client/src/command.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Command } from "./command";

describe(Command.name, () => {
it("implements a classBuilder", async () => {
class MyCommand extends Command.classBuilder<any, any, any, any, any>()
.ep({
Endpoint: { type: "builtInParams", name: "Endpoint" },
})
.m(function () {
return [];
})
.s("SmithyMyClient", "SmithyMyOperation", {})
.n("MyClient", "MyCommand")
.f()
.ser(async (_) => _)
.de(async (_) => _)
.build() {}

const myCommand = new MyCommand({
Prop: "prop1",
});

expect(myCommand).toBeInstanceOf(Command);
expect(myCommand).toBeInstanceOf(MyCommand);
expect(MyCommand.getEndpointParameterInstructions()).toEqual({
Endpoint: { type: "builtInParams", name: "Endpoint" },
});
expect(myCommand.input).toEqual({
Prop: "prop1",
});

// private method exists for compatibility
expect((myCommand as any).serialize).toBeDefined();

// private method exists for compatibility
expect((myCommand as any).deserialize).toBeDefined();
});
});
272 changes: 269 additions & 3 deletions packages/smithy-client/src/command.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
import type { EndpointParameterInstructions } from "@smithy/middleware-endpoint";
import { constructStack } from "@smithy/middleware-stack";
import { Command as ICommand, Handler, MetadataBearer, MiddlewareStack as IMiddlewareStack } from "@smithy/types";
import type { HttpRequest } from "@smithy/protocol-http";
import type {
Command as ICommand,
FinalizeHandlerArguments,
Handler,
HandlerExecutionContext,
HttpRequest as IHttpRequest,
HttpResponse as IHttpResponse,
Logger,
MetadataBearer,
MiddlewareStack as IMiddlewareStack,
Pluggable,
RequestHandler,
SerdeContext,
} from "@smithy/types";
import { SMITHY_CONTEXT_KEY } from "@smithy/types";

/**
* @public
Expand All @@ -11,11 +27,261 @@ export abstract class Command<
ClientInput extends object = any,
ClientOutput extends MetadataBearer = any
> implements ICommand<ClientInput, Input, ClientOutput, Output, ResolvedClientConfiguration> {
abstract input: Input;
readonly middlewareStack: IMiddlewareStack<Input, Output> = constructStack<Input, Output>();
public abstract input: Input;
public readonly middlewareStack: IMiddlewareStack<Input, Output> = constructStack<Input, Output>();

/**
* Factory for Command ClassBuilder.
* @internal
*/
public static classBuilder<
I extends SI,
O extends SO,
C extends { logger: Logger; requestHandler: RequestHandler<any, any, any> },
SI extends object = any,
SO extends MetadataBearer = any
>() {
return new ClassBuilder<I, O, C, SI, SO>();
}

abstract resolveMiddleware(
stack: IMiddlewareStack<ClientInput, ClientOutput>,
configuration: ResolvedClientConfiguration,
options: any
): Handler<Input, Output>;

/**
* @internal
*/
public resolveMiddlewareWithContext(
clientStack: IMiddlewareStack<any, any>,
configuration: { logger: Logger; requestHandler: RequestHandler<any, any, any> },
options: any,
{
middlewareFn,
clientName,
commandName,
inputFilterSensitiveLog,
outputFilterSensitiveLog,
smithyContext,
additionalContext,
CommandCtor,
}: ResolveMiddlewareContextArgs
) {
for (const mw of middlewareFn.bind(this)(CommandCtor, clientStack, configuration, options)) {
this.middlewareStack.use(mw);
}
const stack = clientStack.concat(this.middlewareStack);
const { logger } = configuration;
const handlerExecutionContext: HandlerExecutionContext = {
logger,
clientName,
commandName,
inputFilterSensitiveLog,
outputFilterSensitiveLog,
[SMITHY_CONTEXT_KEY]: {
...smithyContext,
},
...additionalContext,
};
const { requestHandler } = configuration;
return stack.resolve(
(request: FinalizeHandlerArguments<any>) => requestHandler.handle(request.request as HttpRequest, options || {}),
handlerExecutionContext
);
}
}

/**
* @internal
*/
type ResolveMiddlewareContextArgs = {
middlewareFn: (CommandCtor: any, clientStack: any, config: any, options: any) => Pluggable<any, any>[];
clientName: string;
commandName: string;
smithyContext: Record<string, unknown>;
additionalContext: HandlerExecutionContext;
inputFilterSensitiveLog: (_: any) => any;
outputFilterSensitiveLog: (_: any) => any;
CommandCtor: any /* Command constructor */;
};

/**
* @internal
*/
class ClassBuilder<
I extends SI,
O extends SO,
C extends { logger: Logger; requestHandler: RequestHandler<any, any, any> },
SI extends object = any,
SO extends MetadataBearer = any
> {
private _init: (_: Command<I, O, C, SI, SO>) => void = () => {};
private _ep: EndpointParameterInstructions = {};
private _middlewareFn: (
CommandCtor: any,
clientStack: any,
config: any,
options: any
) => Pluggable<any, any>[] = () => [];
private _commandName = "";
private _clientName = "";
private _additionalContext = {} as HandlerExecutionContext;
private _smithyContext = {} as Record<string, unknown>;
private _inputFilterSensitiveLog = (_: any) => _;
private _outputFilterSensitiveLog = (_: any) => _;
private _serializer: (input: I, context: SerdeContext | any) => Promise<IHttpRequest> = null as any;
private _deserializer: (output: IHttpResponse, context: SerdeContext | any) => Promise<O> = null as any;
/**
* Optional init callback.
*/
public init(cb: (_: Command<I, O, C, SI, SO>) => void) {
this._init = cb;
}
/**
* Set the endpoint parameter instructions.
*/
public ep(endpointParameterInstructions: EndpointParameterInstructions): ClassBuilder<I, O, C, SI, SO> {
this._ep = endpointParameterInstructions;
return this;
}
/**
* Add any number of middleware.
*/
public m(
middlewareSupplier: (CommandCtor: any, clientStack: any, config: any, options: any) => Pluggable<any, any>[]
): ClassBuilder<I, O, C, SI, SO> {
this._middlewareFn = middlewareSupplier;
return this;
}
/**
* Set the initial handler execution context Smithy field.
*/
public s(
service: string,
operation: string,
smithyContext: Record<string, unknown> = {}
): ClassBuilder<I, O, C, SI, SO> {
this._smithyContext = {
service,
operation,
...smithyContext,
};
return this;
}
/**
* Set the initial handler execution context.
*/
public c(additionalContext: HandlerExecutionContext = {}): ClassBuilder<I, O, C, SI, SO> {
this._additionalContext = additionalContext;
return this;
}
/**
* Set constant string identifiers for the operation.
*/
public n(clientName: string, commandName: string): ClassBuilder<I, O, C, SI, SO> {
this._clientName = clientName;
this._commandName = commandName;
return this;
}
/**
* Set the input and output sensistive log filters.
*/
public f(
inputFilter: (_: any) => any = (_) => _,
outputFilter: (_: any) => any = (_) => _
): ClassBuilder<I, O, C, SI, SO> {
this._inputFilterSensitiveLog = inputFilter;
this._outputFilterSensitiveLog = outputFilter;
return this;
}
/**
* Sets the serializer.
*/
public ser(
serializer: (input: I, context?: SerdeContext | any) => Promise<IHttpRequest>
): ClassBuilder<I, O, C, SI, SO> {
this._serializer = serializer;
return this;
}
/**
* Sets the deserializer.
*/
public de(
deserializer: (output: IHttpResponse, context?: SerdeContext | any) => Promise<O>
): ClassBuilder<I, O, C, SI, SO> {
this._deserializer = deserializer;
return this;
}
/**
* @returns a Command class with the classBuilder properties.
*/
public build(): {
new (input: I): CommandImpl<I, O, C, SI, SO>;
getEndpointParameterInstructions(): EndpointParameterInstructions;
} {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const closure = this;
let CommandRef: any;

return (CommandRef = class extends Command<I, O, C, SI, SO> {
/**
* @public
*/
public static getEndpointParameterInstructions(): EndpointParameterInstructions {
return closure._ep;
}

/**
* @public
*/
public constructor(readonly input: I) {
super();
closure._init(this);
}

/**
* @internal
*/
public resolveMiddleware(stack: IMiddlewareStack<any, any>, configuration: C, options: any): Handler<any, any> {
return this.resolveMiddlewareWithContext(stack, configuration, options, {
CommandCtor: CommandRef,
middlewareFn: closure._middlewareFn,
clientName: closure._clientName,
commandName: closure._commandName,
inputFilterSensitiveLog: closure._inputFilterSensitiveLog,
outputFilterSensitiveLog: closure._outputFilterSensitiveLog,
smithyContext: closure._smithyContext,
additionalContext: closure._additionalContext,
});
}

/**
* @internal
*/
// @ts-ignore used in middlewareFn closure.
public serialize = closure._serializer;

/**
* @internal
*/
// @ts-ignore used in middlewareFn closure.
public deserialize = closure._deserializer;
});
}
}

/**
* A concrete implementation of ICommand with no abstract members.
* @public
*/
export interface CommandImpl<
I extends SI,
O extends SO,
C extends { logger: Logger; requestHandler: RequestHandler<any, any, any> },
SI extends object = any,
SO extends MetadataBearer = any
> extends Command<I, O, C, SI, SO> {
readonly input: I;
resolveMiddleware(stack: IMiddlewareStack<SI, SO>, configuration: C, options: any): Handler<I, O>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,21 @@ const pkgJsonEnforcement = require("./package-json-enforcement");
continue;
}

const importedDependencies = [];
importedDependencies.push(
...new Set(
[...(contents.toString().match(/from "(@(aws-sdk|smithy)\/.*?)"/g) || [])]
.slice(1)
.map((_) => _.replace(/from "/g, "").replace(/"$/, ""))
)
);

for (const dependency of importedDependencies) {
if (!(dependency in pkgJson.dependencies) && dependency !== pkgJson.name) {
errors.push(`${dependency} undeclared but imported in ${pkgJson.name} ${file}}`);
}
}

for (const [dep, version] of Object.entries(pkgJson.devDependencies ?? {})) {
if (dep.startsWith("@smithy/") && contents.includes(`from "${dep}";`)) {
console.warn(`${dep} incorrectly declared in devDependencies of ${folder}`);
Expand Down
Loading