diff --git a/.changeset/six-doors-speak.md b/.changeset/six-doors-speak.md new file mode 100644 index 00000000000..1bcf208f688 --- /dev/null +++ b/.changeset/six-doors-speak.md @@ -0,0 +1,6 @@ +--- +"@smithy/smithy-client": patch +"@smithy/types": patch +--- + +allow command constructor argument to be omitted if no required members diff --git a/packages/smithy-client/src/command.spec.ts b/packages/smithy-client/src/command.spec.ts index 59d6773a877..651a608229a 100644 --- a/packages/smithy-client/src/command.spec.ts +++ b/packages/smithy-client/src/command.spec.ts @@ -1,6 +1,25 @@ import { Command } from "./command"; describe(Command.name, () => { + it("has optional argument if the input type has no required members", async () => { + type OptionalInput = { + key?: string; + optional?: string; + }; + + type RequiredInput = { + key: string | undefined; + optional?: string; + }; + + class WithRequiredInputCommand extends Command.classBuilder().build() {} + + class WithOptionalInputCommand extends Command.classBuilder().build() {} + + new WithRequiredInputCommand({ key: "1" }); + + new WithOptionalInputCommand(); // expect no type error. + }); it("implements a classBuilder", async () => { class MyCommand extends Command.classBuilder() .ep({ diff --git a/packages/smithy-client/src/command.ts b/packages/smithy-client/src/command.ts index 7f265f064ce..93c9a3b1acc 100644 --- a/packages/smithy-client/src/command.ts +++ b/packages/smithy-client/src/command.ts @@ -11,6 +11,7 @@ import type { Logger, MetadataBearer, MiddlewareStack as IMiddlewareStack, + OptionalParameter, Pluggable, RequestHandler, SerdeContext, @@ -218,6 +219,7 @@ class ClassBuilder< */ public build(): { new (input: I): CommandImpl; + new (...[input]: OptionalParameter): CommandImpl; getEndpointParameterInstructions(): EndpointParameterInstructions; } { // eslint-disable-next-line @typescript-eslint/no-this-alias @@ -225,6 +227,8 @@ class ClassBuilder< let CommandRef: any; return (CommandRef = class extends Command { + public readonly input: I; + /** * @public */ @@ -235,8 +239,9 @@ class ClassBuilder< /** * @public */ - public constructor(readonly input: I) { + public constructor(...[input]: OptionalParameter) { super(); + this.input = input ?? (({} as unknown) as I); closure._init(this); } diff --git a/packages/types/src/client.ts b/packages/types/src/client.ts index e8d7fcbad01..576b2220c8d 100644 --- a/packages/types/src/client.ts +++ b/packages/types/src/client.ts @@ -1,7 +1,7 @@ import { Command } from "./command"; import { MiddlewareStack } from "./middleware"; import { MetadataBearer } from "./response"; -import { Exact } from "./util"; +import { OptionalParameter } from "./util"; /** * @public @@ -9,7 +9,7 @@ import { Exact } from "./util"; * A type which checks if the client configuration is optional. * If all entries of the client configuration are optional, it allows client creation without passing any config. */ -export type CheckOptionalClientConfig = Exact, T> extends true ? [] | [T] : [T]; +export type CheckOptionalClientConfig = OptionalParameter; /** * @public diff --git a/packages/types/src/util.spec.ts b/packages/types/src/util.spec.ts new file mode 100644 index 00000000000..1b98b80a7ea --- /dev/null +++ b/packages/types/src/util.spec.ts @@ -0,0 +1,40 @@ +import type { Exact, OptionalParameter } from "./util"; + +type Assignable = [RHS] extends [LHS] ? true : false; + +type OptionalInput = { + key?: string; + optional?: string; +}; + +type RequiredInput = { + key: string | undefined; + optional?: string; +}; + +{ + // optional parameter transform of an optional input is not equivalent to exactly 1 parameter. + type A = [...OptionalParameter]; + type B = [OptionalInput]; + type C = [OptionalInput] | []; + + const assert1: Exact = false as const; + const assert2: Exact = true as const; + + const assert3: Assignable = true as const; + const assert4: A = []; + + const assert5: Assignable = true as const; + const assert6: A = [{ key: "" }]; +} + +{ + // optional parameter transform of a required input is equivalent to exactly 1 parameter. + type A = [...OptionalParameter]; + type B = [RequiredInput]; + + const assert1: Exact = true as const; + const assert2: Assignable = false as const; + const assert3: Assignable = true as const; + const assert4: A = [{ key: "" }]; +} diff --git a/packages/types/src/util.ts b/packages/types/src/util.ts index bebb7775669..101df49de03 100644 --- a/packages/types/src/util.ts +++ b/packages/types/src/util.ts @@ -181,3 +181,11 @@ export interface RetryStrategy { args: FinalizeHandlerArguments ) => Promise>; } + +/** + * @public + * + * Indicates the parameter may be omitted if the parameter object T + * is equivalent to a Partial, i.e. all properties optional. + */ +export type OptionalParameter = Exact, T> extends true ? [] | [T] : [T]; diff --git a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServiceAggregatedClientGenerator.java b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServiceAggregatedClientGenerator.java index ff01e97116f..cde823dc291 100644 --- a/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServiceAggregatedClientGenerator.java +++ b/smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServiceAggregatedClientGenerator.java @@ -22,6 +22,7 @@ import software.amazon.smithy.codegen.core.SymbolProvider; import software.amazon.smithy.model.Model; import software.amazon.smithy.model.knowledge.TopDownIndex; +import software.amazon.smithy.model.shapes.MemberShape; import software.amazon.smithy.model.shapes.OperationShape; import software.amazon.smithy.model.shapes.ServiceShape; import software.amazon.smithy.utils.SmithyInternalApi; @@ -94,19 +95,28 @@ public void run() { writer.writeDocs( "@see {@link " + operationSymbol.getName() + "}" ); - writer.write("$L(\n" - + " args: $T,\n" - + " options?: $T,\n" - + "): Promise<$T>;", methodName, input, applicationProtocol.getOptionsType(), output); - writer.write("$L(\n" - + " args: $T,\n" - + " cb: (err: any, data?: $T) => void\n" - + "): void;", methodName, input, output); - writer.write("$L(\n" - + " args: $T,\n" - + " options: $T,\n" - + " cb: (err: any, data?: $T) => void\n" - + "): void;", methodName, input, applicationProtocol.getOptionsType(), output); + boolean inputOptional = model.getShape(operation.getInputShape()).map( + shape -> shape.getAllMembers().values().stream().noneMatch(MemberShape::isRequired) + ).orElse(true); + if (inputOptional) { + writer.write("$L(): Promise<$T>;", methodName, output); + } + writer.write(""" + $1L( + args: $2T, + options?: $3T, + ): Promise<$4T>; + $1L( + args: $2T, + cb: (err: any, data?: $4T) => void + ): void; + $1L( + args: $2T, + options: $3T, + cb: (err: any, data?: $4T) => void + ): void;""", + methodName, input, applicationProtocol.getOptionsType(), output + ); writer.write(""); } });