Skip to content

Commit 4f2764e

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

2 files changed

Lines changed: 273 additions & 3 deletions

File tree

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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+
case BOOLEAN, BYTE, SHORT, INTEGER, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL,
59+
STRING, TIMESTAMP, DOCUMENT, LIST, MAP, ENUM, INT_ENUM, STRUCTURE, UNION ->
60+
true;
61+
// Per Smithy spec § 13.3.1, missing required streaming blobs are already handled
62+
// by the deserializer (an empty HTTP body becomes a zero-length AsyncBytesReader),
63+
// so client error correction is unnecessary here.
64+
case BLOB -> !target.hasTrait(StreamingTrait.class);
65+
default -> false;
66+
};
67+
}
68+
69+
@Override
70+
public Boolean booleanShape(BooleanShape shape) {
71+
writer.writeInline("False");
72+
return true;
73+
}
74+
75+
@Override
76+
public Boolean byteShape(ByteShape shape) {
77+
writer.writeInline("0");
78+
return true;
79+
}
80+
81+
@Override
82+
public Boolean shortShape(ShortShape shape) {
83+
writer.writeInline("0");
84+
return true;
85+
}
86+
87+
@Override
88+
public Boolean integerShape(IntegerShape shape) {
89+
writer.writeInline("0");
90+
return true;
91+
}
92+
93+
@Override
94+
public Boolean longShape(LongShape shape) {
95+
writer.writeInline("0");
96+
return true;
97+
}
98+
99+
@Override
100+
public Boolean bigIntegerShape(BigIntegerShape shape) {
101+
writer.writeInline("0");
102+
return true;
103+
}
104+
105+
@Override
106+
public Boolean floatShape(FloatShape shape) {
107+
writer.writeInline("0.0");
108+
return true;
109+
}
110+
111+
@Override
112+
public Boolean doubleShape(DoubleShape shape) {
113+
writer.writeInline("0.0");
114+
return true;
115+
}
116+
117+
@Override
118+
public Boolean bigDecimalShape(BigDecimalShape shape) {
119+
writer.addStdlibImport("decimal", "Decimal");
120+
writer.writeInline("Decimal(0)");
121+
return true;
122+
}
123+
124+
@Override
125+
public Boolean stringShape(StringShape shape) {
126+
writer.writeInline("\"\"");
127+
return true;
128+
}
129+
130+
@Override
131+
public Boolean blobShape(BlobShape shape) {
132+
writer.writeInline("b\"\"");
133+
return true;
134+
}
135+
136+
@Override
137+
public Boolean timestampShape(TimestampShape shape) {
138+
writer.addStdlibImport("datetime", "datetime");
139+
writer.addStdlibImport("datetime", "timezone");
140+
writer.writeInline("datetime.fromtimestamp(0, tz=timezone.utc)");
141+
return true;
142+
}
143+
144+
@Override
145+
public Boolean documentShape(DocumentShape shape) {
146+
writer.addImport("smithy_core.documents", "Document");
147+
writer.writeInline("Document(None)");
148+
return true;
149+
}
150+
151+
@Override
152+
public Boolean listShape(ListShape shape) {
153+
writer.writeInline("[]");
154+
return true;
155+
}
156+
157+
@Override
158+
public Boolean mapShape(MapShape shape) {
159+
writer.writeInline("{}");
160+
return true;
161+
}
162+
163+
@Override
164+
public Boolean enumShape(EnumShape shape) {
165+
// TODO: the Smithy spec recommends enum types define an unknown variant. If a
166+
// future change adds an unknown variant to the generated enum class (e.g.
167+
// MyEnum.unknown(value)), revisit this to emit it instead of the bare "".
168+
writer.writeInline("\"\"");
169+
return true;
170+
}
171+
172+
@Override
173+
public Boolean intEnumShape(IntEnumShape shape) {
174+
// TODO: the Smithy spec recommends intEnum types define an unknown variant. If a
175+
// future change adds an unknown variant to the generated intEnum class (e.g.
176+
// MyIntEnum.unknown(value)), revisit this to emit it instead of the bare 0.
177+
writer.writeInline("0");
178+
return true;
179+
}
180+
181+
@Override
182+
public Boolean unionShape(UnionShape shape) {
183+
var unknownSymbol = context.symbolProvider()
184+
.toSymbol(shape)
185+
.expectProperty(SymbolProperties.UNION_UNKNOWN);
186+
writer.addImport(unknownSymbol, unknownSymbol.getName());
187+
writer.writeInline("$L(tag=\"\")", unknownSymbol.getName());
188+
return true;
189+
}
190+
191+
@Override
192+
public Boolean structureShape(StructureShape shape) {
193+
// Delegate to the target struct's _smithy_default() so nested required fields are
194+
// also filled in. Recursion terminates because Smithy forbids recursive paths whose
195+
// members are all @required:
196+
// https://smithy.io/2.0/spec/aggregate-types.html#recursive-shape-definitions
197+
var symbol = context.symbolProvider().toSymbol(shape);
198+
writer.addImport(symbol, symbol.getName());
199+
writer.writeInline("$L._smithy_default()", symbol.getName());
200+
return true;
201+
}
202+
203+
@Override
204+
public Boolean memberShape(MemberShape shape) {
205+
return context.model().expectShape(shape.getTarget()).accept(this);
206+
}
207+
}

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

Lines changed: 66 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,71 @@ 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.
422+
*/
423+
private void generateSmithyDefaultMethod() {
424+
writer.write("""
425+
@classmethod
426+
def _smithy_default(cls) -> Self:
427+
return cls(${C|})
428+
""",
429+
writer.consumer(w -> writeSmithyDefaultArguments()));
430+
}
431+
432+
private void writeSmithyDefaultArguments() {
433+
var visitor = new MemberErrorCorrectionGenerator(context, writer);
434+
var first = true;
435+
for (MemberShape member : requiredMembers) {
436+
var target = model.expectShape(member.getTarget());
437+
if (!MemberErrorCorrectionGenerator.hasDefault(target)) {
438+
continue;
439+
}
440+
if (!first) {
441+
writer.writeInline(", ");
442+
}
443+
first = false;
444+
writer.writeInline("$L=", symbolProvider.toMemberName(member));
445+
target.accept(visitor);
446+
}
447+
}
448+
386449
private void deserializeMembers(Collection<MemberShape> members) {
387450
int index = -1;
388451
for (MemberShape member : members) {

0 commit comments

Comments
 (0)