Skip to content

Commit e57b0b7

Browse files
committed
Implement client error correction
1 parent 87763fd commit e57b0b7

2 files changed

Lines changed: 285 additions & 3 deletions

File tree

Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
package software.amazon.smithy.python.codegen.generators;
6+
7+
import software.amazon.smithy.model.shapes.BigDecimalShape;
8+
import software.amazon.smithy.model.shapes.BigIntegerShape;
9+
import software.amazon.smithy.model.shapes.BlobShape;
10+
import software.amazon.smithy.model.shapes.BooleanShape;
11+
import software.amazon.smithy.model.shapes.ByteShape;
12+
import software.amazon.smithy.model.shapes.DocumentShape;
13+
import software.amazon.smithy.model.shapes.DoubleShape;
14+
import software.amazon.smithy.model.shapes.EnumShape;
15+
import software.amazon.smithy.model.shapes.FloatShape;
16+
import software.amazon.smithy.model.shapes.IntEnumShape;
17+
import software.amazon.smithy.model.shapes.IntegerShape;
18+
import software.amazon.smithy.model.shapes.ListShape;
19+
import software.amazon.smithy.model.shapes.LongShape;
20+
import software.amazon.smithy.model.shapes.MapShape;
21+
import software.amazon.smithy.model.shapes.MemberShape;
22+
import software.amazon.smithy.model.shapes.Shape;
23+
import software.amazon.smithy.model.shapes.ShapeVisitor;
24+
import software.amazon.smithy.model.shapes.ShortShape;
25+
import software.amazon.smithy.model.shapes.StringShape;
26+
import software.amazon.smithy.model.shapes.StructureShape;
27+
import software.amazon.smithy.model.shapes.TimestampShape;
28+
import software.amazon.smithy.model.shapes.UnionShape;
29+
import software.amazon.smithy.model.traits.StreamingTrait;
30+
import software.amazon.smithy.python.codegen.GenerationContext;
31+
import software.amazon.smithy.python.codegen.SymbolProperties;
32+
import software.amazon.smithy.python.codegen.writer.PythonWriter;
33+
import software.amazon.smithy.utils.SmithyInternalApi;
34+
35+
/**
36+
* Emits the Python expression used to fill a missing required member during client error
37+
* correction.
38+
*
39+
* @see <a href="https://smithy.io/2.0/spec/aggregate-types.html#client-error-correction">Smithy
40+
* spec: Client error correction</a>
41+
*/
42+
@SmithyInternalApi
43+
public final class MemberErrorCorrectionGenerator extends ShapeVisitor.DataShapeVisitor<Boolean> {
44+
45+
private final GenerationContext context;
46+
private final PythonWriter writer;
47+
48+
public MemberErrorCorrectionGenerator(GenerationContext context, PythonWriter writer) {
49+
this.context = context;
50+
this.writer = writer;
51+
}
52+
53+
/**
54+
* @return {@code true} if the visitor will emit a default expression for this shape.
55+
*/
56+
public static boolean hasDefault(Shape target) {
57+
return switch (target.getType()) {
58+
// Note on streaming shapes:
59+
// - Streaming unions (event streams) are filtered out earlier by
60+
// StructureGenerator#filterEventStreamMember and never reach this visitor,
61+
// so UNION can unconditionally return true here.
62+
// - Streaming blobs are NOT filtered earlier, so we explicitly exclude them
63+
// below. Per Smithy spec § 13.3.1, a missing streaming blob is already
64+
// handled by the deserializer (an empty HTTP body becomes a zero-length
65+
// AsyncBytesReader), so client error correction is unnecessary.
66+
case BOOLEAN, BYTE, SHORT, INTEGER, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL,
67+
STRING, TIMESTAMP, DOCUMENT, LIST, MAP, ENUM, INT_ENUM, STRUCTURE, UNION ->
68+
true;
69+
case BLOB -> !target.hasTrait(StreamingTrait.class);
70+
default -> false;
71+
};
72+
}
73+
74+
@Override
75+
public Boolean booleanShape(BooleanShape shape) {
76+
writer.writeInline("False");
77+
return true;
78+
}
79+
80+
@Override
81+
public Boolean byteShape(ByteShape shape) {
82+
writer.writeInline("0");
83+
return true;
84+
}
85+
86+
@Override
87+
public Boolean shortShape(ShortShape shape) {
88+
writer.writeInline("0");
89+
return true;
90+
}
91+
92+
@Override
93+
public Boolean integerShape(IntegerShape shape) {
94+
writer.writeInline("0");
95+
return true;
96+
}
97+
98+
@Override
99+
public Boolean longShape(LongShape shape) {
100+
writer.writeInline("0");
101+
return true;
102+
}
103+
104+
@Override
105+
public Boolean bigIntegerShape(BigIntegerShape shape) {
106+
writer.writeInline("0");
107+
return true;
108+
}
109+
110+
@Override
111+
public Boolean floatShape(FloatShape shape) {
112+
writer.writeInline("0.0");
113+
return true;
114+
}
115+
116+
@Override
117+
public Boolean doubleShape(DoubleShape shape) {
118+
writer.writeInline("0.0");
119+
return true;
120+
}
121+
122+
@Override
123+
public Boolean bigDecimalShape(BigDecimalShape shape) {
124+
writer.addStdlibImport("decimal", "Decimal");
125+
writer.writeInline("Decimal(0)");
126+
return true;
127+
}
128+
129+
@Override
130+
public Boolean stringShape(StringShape shape) {
131+
writer.writeInline("\"\"");
132+
return true;
133+
}
134+
135+
@Override
136+
public Boolean blobShape(BlobShape shape) {
137+
writer.writeInline("b\"\"");
138+
return true;
139+
}
140+
141+
@Override
142+
public Boolean timestampShape(TimestampShape shape) {
143+
writer.addStdlibImport("datetime", "datetime");
144+
writer.addStdlibImport("datetime", "timezone");
145+
writer.writeInline("datetime.fromtimestamp(0, tz=timezone.utc)");
146+
return true;
147+
}
148+
149+
@Override
150+
public Boolean documentShape(DocumentShape shape) {
151+
writer.addImport("smithy_core.documents", "Document");
152+
writer.writeInline("Document(None)");
153+
return true;
154+
}
155+
156+
@Override
157+
public Boolean listShape(ListShape shape) {
158+
writer.writeInline("[]");
159+
return true;
160+
}
161+
162+
@Override
163+
public Boolean mapShape(MapShape shape) {
164+
writer.writeInline("{}");
165+
return true;
166+
}
167+
168+
@Override
169+
public Boolean enumShape(EnumShape shape) {
170+
// TODO: the Smithy spec recommends enum types define an unknown variant. If a
171+
// future change adds an unknown variant to the generated enum class (e.g.
172+
// MyEnum.unknown(value)), revisit this to emit it instead of the bare "".
173+
writer.writeInline("\"\"");
174+
return true;
175+
}
176+
177+
@Override
178+
public Boolean intEnumShape(IntEnumShape shape) {
179+
// TODO: the Smithy spec recommends intEnum types define an unknown variant. If a
180+
// future change adds an unknown variant to the generated intEnum class (e.g.
181+
// MyIntEnum.unknown(value)), revisit this to emit it instead of the bare 0.
182+
writer.writeInline("0");
183+
return true;
184+
}
185+
186+
@Override
187+
public Boolean unionShape(UnionShape shape) {
188+
var unknownSymbol = context.symbolProvider()
189+
.toSymbol(shape)
190+
.expectProperty(SymbolProperties.UNION_UNKNOWN);
191+
writer.addImport(unknownSymbol, unknownSymbol.getName());
192+
writer.writeInline("$L(tag=\"\")", unknownSymbol.getName());
193+
return true;
194+
}
195+
196+
@Override
197+
public Boolean structureShape(StructureShape shape) {
198+
// Delegate to the target struct's _smithy_default() so nested required fields are
199+
// also filled in. Recursion terminates because Smithy forbids recursive paths whose
200+
// members are all @required:
201+
// https://smithy.io/2.0/spec/aggregate-types.html#recursive-shape-definitions
202+
var symbol = context.symbolProvider().toSymbol(shape);
203+
writer.addImport(symbol, symbol.getName());
204+
writer.writeInline("$L._smithy_default()", symbol.getName());
205+
return true;
206+
}
207+
208+
@Override
209+
public Boolean memberShape(MemberShape shape) {
210+
return context.model().expectShape(shape.getTarget()).accept(this);
211+
}
212+
}

codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java

Lines changed: 73 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -104,12 +104,15 @@ class $L:
104104
105105
${C|}
106106
107+
${C|}
108+
107109
""",
108110
symbol.getName(),
109111
writer.consumer(w -> writeClassDocs()),
110112
writer.consumer(w -> writeProperties()),
111113
writer.consumer(w -> generateSerializeMethod()),
112-
writer.consumer(w -> generateDeserializeMethod()));
114+
writer.consumer(w -> generateDeserializeMethod()),
115+
writer.consumer(w -> generateSmithyDefaultMethod()));
113116
}
114117

115118
private void renderError() {
@@ -147,14 +150,17 @@ class $1L($2T):
147150
148151
${7C|}
149152
153+
${8C|}
154+
150155
""",
151156
symbol.getName(),
152157
baseError,
153158
fault,
154159
writer.consumer(w -> writeClassDocs()),
155160
writer.consumer(w -> writeProperties()),
156161
writer.consumer(w -> generateSerializeMethod()),
157-
writer.consumer(w -> generateDeserializeMethod()));
162+
writer.consumer(w -> generateDeserializeMethod()),
163+
writer.consumer(w -> generateSmithyDefaultMethod()));
158164
}
159165

160166
private void writeClassDocs() {
@@ -375,14 +381,78 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
375381
logger.debug("Unexpected member schema: %s", schema)
376382
377383
deserializer.read_struct($T, consumer=_consumer)
384+
${C|}
378385
return kwargs
379386
380387
""",
381388
writer.consumer(w -> deserializeMembers(shape.members())),
382-
schemaSymbol);
389+
schemaSymbol,
390+
writer.consumer(w -> writeErrorCorrection()));
383391
writer.popState();
384392
}
385393

394+
/**
395+
* Emits client error correction for required members the server failed to serialize.
396+
*
397+
* @see <a href="https://smithy.io/2.0/spec/aggregate-types.html#client-error-correction">Smithy
398+
* spec: Client error correction</a>
399+
*/
400+
private void writeErrorCorrection() {
401+
var visitor = new MemberErrorCorrectionGenerator(context, writer);
402+
for (MemberShape member : requiredMembers) {
403+
var target = model.expectShape(member.getTarget());
404+
if (!MemberErrorCorrectionGenerator.hasDefault(target)) {
405+
// Streaming shapes have no synthesizable default; let the dataclass raise.
406+
continue;
407+
}
408+
writer.pushState();
409+
writer.putContext("memberName", symbolProvider.toMemberName(member));
410+
writer.write("""
411+
if ${memberName:S} not in kwargs:
412+
kwargs[${memberName:S}] = ${C|}""",
413+
writer.consumer(w -> target.accept(visitor)));
414+
writer.popState();
415+
}
416+
}
417+
418+
/**
419+
* Emits a {@code _smithy_default()} classmethod that constructs an instance with all
420+
* required members filled in via client error correction. Used to fill nested structure
421+
* members per the Smithy spec. If the structure has any required member whose target has
422+
* no synthesizable default (i.e. a streaming blob), {@code _smithy_default()} is omitted:
423+
* a generated {@code cls()} call would be missing a required argument. But such structures
424+
* can only appear as a top-level operation input or output (per spec § 13.3), never as a
425+
* nested-struct member, so {@code _smithy_default()} would never be invoked on them anyway.
426+
*/
427+
private void generateSmithyDefaultMethod() {
428+
for (MemberShape member : requiredMembers) {
429+
var target = model.expectShape(member.getTarget());
430+
if (!MemberErrorCorrectionGenerator.hasDefault(target)) {
431+
return;
432+
}
433+
}
434+
writer.write("""
435+
@classmethod
436+
def _smithy_default(cls) -> Self:
437+
return cls(${C|})
438+
""",
439+
writer.consumer(w -> writeSmithyDefaultArguments()));
440+
}
441+
442+
private void writeSmithyDefaultArguments() {
443+
var visitor = new MemberErrorCorrectionGenerator(context, writer);
444+
var first = true;
445+
for (MemberShape member : requiredMembers) {
446+
var target = model.expectShape(member.getTarget());
447+
if (!first) {
448+
writer.writeInline(", ");
449+
}
450+
first = false;
451+
writer.writeInline("$L=", symbolProvider.toMemberName(member));
452+
target.accept(visitor);
453+
}
454+
}
455+
386456
private void deserializeMembers(Collection<MemberShape> members) {
387457
int index = -1;
388458
for (MemberShape member : members) {

0 commit comments

Comments
 (0)