Skip to content

Commit

Permalink
feat: Command classBuilder (#1118)
Browse files Browse the repository at this point in the history
* feat: add Command ClassBuilder

* fix: add dependency check script

* add changeset

* rename script file

* fix yarn lock
  • Loading branch information
kuhe authored Dec 22, 2023
1 parent f0ff3b7 commit 164f3bb
Show file tree
Hide file tree
Showing 9 changed files with 340 additions and 4 deletions.
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

0 comments on commit 164f3bb

Please sign in to comment.