Skip to content

Commit 661f1d6

Browse files
authored
feat: allow commands to be constructed without arg if all arg fields optional (#1206)
1 parent 9462d00 commit 661f1d6

File tree

7 files changed

+104
-16
lines changed

7 files changed

+104
-16
lines changed

.changeset/six-doors-speak.md

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@smithy/smithy-client": patch
3+
"@smithy/types": patch
4+
---
5+
6+
allow command constructor argument to be omitted if no required members

packages/smithy-client/src/command.spec.ts

+19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
import { Command } from "./command";
22

33
describe(Command.name, () => {
4+
it("has optional argument if the input type has no required members", async () => {
5+
type OptionalInput = {
6+
key?: string;
7+
optional?: string;
8+
};
9+
10+
type RequiredInput = {
11+
key: string | undefined;
12+
optional?: string;
13+
};
14+
15+
class WithRequiredInputCommand extends Command.classBuilder<RequiredInput, any, any, any, any>().build() {}
16+
17+
class WithOptionalInputCommand extends Command.classBuilder<OptionalInput, any, any, any, any>().build() {}
18+
19+
new WithRequiredInputCommand({ key: "1" });
20+
21+
new WithOptionalInputCommand(); // expect no type error.
22+
});
423
it("implements a classBuilder", async () => {
524
class MyCommand extends Command.classBuilder<any, any, any, any, any>()
625
.ep({

packages/smithy-client/src/command.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import type {
1111
Logger,
1212
MetadataBearer,
1313
MiddlewareStack as IMiddlewareStack,
14+
OptionalParameter,
1415
Pluggable,
1516
RequestHandler,
1617
SerdeContext,
@@ -218,13 +219,16 @@ class ClassBuilder<
218219
*/
219220
public build(): {
220221
new (input: I): CommandImpl<I, O, C, SI, SO>;
222+
new (...[input]: OptionalParameter<I>): CommandImpl<I, O, C, SI, SO>;
221223
getEndpointParameterInstructions(): EndpointParameterInstructions;
222224
} {
223225
// eslint-disable-next-line @typescript-eslint/no-this-alias
224226
const closure = this;
225227
let CommandRef: any;
226228

227229
return (CommandRef = class extends Command<I, O, C, SI, SO> {
230+
public readonly input: I;
231+
228232
/**
229233
* @public
230234
*/
@@ -235,8 +239,9 @@ class ClassBuilder<
235239
/**
236240
* @public
237241
*/
238-
public constructor(readonly input: I) {
242+
public constructor(...[input]: OptionalParameter<I>) {
239243
super();
244+
this.input = input ?? (({} as unknown) as I);
240245
closure._init(this);
241246
}
242247

packages/types/src/client.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
import { Command } from "./command";
22
import { MiddlewareStack } from "./middleware";
33
import { MetadataBearer } from "./response";
4-
import { Exact } from "./util";
4+
import { OptionalParameter } from "./util";
55

66
/**
77
* @public
88
*
99
* A type which checks if the client configuration is optional.
1010
* If all entries of the client configuration are optional, it allows client creation without passing any config.
1111
*/
12-
export type CheckOptionalClientConfig<T> = Exact<Partial<T>, T> extends true ? [] | [T] : [T];
12+
export type CheckOptionalClientConfig<T> = OptionalParameter<T>;
1313

1414
/**
1515
* @public

packages/types/src/util.spec.ts

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import type { Exact, OptionalParameter } from "./util";
2+
3+
type Assignable<LHS, RHS> = [RHS] extends [LHS] ? true : false;
4+
5+
type OptionalInput = {
6+
key?: string;
7+
optional?: string;
8+
};
9+
10+
type RequiredInput = {
11+
key: string | undefined;
12+
optional?: string;
13+
};
14+
15+
{
16+
// optional parameter transform of an optional input is not equivalent to exactly 1 parameter.
17+
type A = [...OptionalParameter<OptionalInput>];
18+
type B = [OptionalInput];
19+
type C = [OptionalInput] | [];
20+
21+
const assert1: Exact<A, B> = false as const;
22+
const assert2: Exact<A, C> = true as const;
23+
24+
const assert3: Assignable<A, []> = true as const;
25+
const assert4: A = [];
26+
27+
const assert5: Assignable<A, [{ key: "" }]> = true as const;
28+
const assert6: A = [{ key: "" }];
29+
}
30+
31+
{
32+
// optional parameter transform of a required input is equivalent to exactly 1 parameter.
33+
type A = [...OptionalParameter<RequiredInput>];
34+
type B = [RequiredInput];
35+
36+
const assert1: Exact<A, B> = true as const;
37+
const assert2: Assignable<A, []> = false as const;
38+
const assert3: Assignable<A, [{ key: "" }]> = true as const;
39+
const assert4: A = [{ key: "" }];
40+
}

packages/types/src/util.ts

+8
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,11 @@ export interface RetryStrategy {
181181
args: FinalizeHandlerArguments<Input>
182182
) => Promise<FinalizeHandlerOutput<Output>>;
183183
}
184+
185+
/**
186+
* @public
187+
*
188+
* Indicates the parameter may be omitted if the parameter object T
189+
* is equivalent to a Partial<T>, i.e. all properties optional.
190+
*/
191+
export type OptionalParameter<T> = Exact<Partial<T>, T> extends true ? [] | [T] : [T];

smithy-typescript-codegen/src/main/java/software/amazon/smithy/typescript/codegen/ServiceAggregatedClientGenerator.java

+23-13
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import software.amazon.smithy.codegen.core.SymbolProvider;
2323
import software.amazon.smithy.model.Model;
2424
import software.amazon.smithy.model.knowledge.TopDownIndex;
25+
import software.amazon.smithy.model.shapes.MemberShape;
2526
import software.amazon.smithy.model.shapes.OperationShape;
2627
import software.amazon.smithy.model.shapes.ServiceShape;
2728
import software.amazon.smithy.utils.SmithyInternalApi;
@@ -94,19 +95,28 @@ public void run() {
9495
writer.writeDocs(
9596
"@see {@link " + operationSymbol.getName() + "}"
9697
);
97-
writer.write("$L(\n"
98-
+ " args: $T,\n"
99-
+ " options?: $T,\n"
100-
+ "): Promise<$T>;", methodName, input, applicationProtocol.getOptionsType(), output);
101-
writer.write("$L(\n"
102-
+ " args: $T,\n"
103-
+ " cb: (err: any, data?: $T) => void\n"
104-
+ "): void;", methodName, input, output);
105-
writer.write("$L(\n"
106-
+ " args: $T,\n"
107-
+ " options: $T,\n"
108-
+ " cb: (err: any, data?: $T) => void\n"
109-
+ "): void;", methodName, input, applicationProtocol.getOptionsType(), output);
98+
boolean inputOptional = model.getShape(operation.getInputShape()).map(
99+
shape -> shape.getAllMembers().values().stream().noneMatch(MemberShape::isRequired)
100+
).orElse(true);
101+
if (inputOptional) {
102+
writer.write("$L(): Promise<$T>;", methodName, output);
103+
}
104+
writer.write("""
105+
$1L(
106+
args: $2T,
107+
options?: $3T,
108+
): Promise<$4T>;
109+
$1L(
110+
args: $2T,
111+
cb: (err: any, data?: $4T) => void
112+
): void;
113+
$1L(
114+
args: $2T,
115+
options: $3T,
116+
cb: (err: any, data?: $4T) => void
117+
): void;""",
118+
methodName, input, applicationProtocol.getOptionsType(), output
119+
);
110120
writer.write("");
111121
}
112122
});

0 commit comments

Comments
 (0)