Skip to content

Commit 183e0d0

Browse files
adamthom-amznsrchase
authored andcommitted
Generate validation methods for types
When server generation is enabled, type generation includes a validate method, aka the low-level validation API, which will enable input validation in the SSDK.
1 parent fdc83bc commit 183e0d0

File tree

4 files changed

+243
-7
lines changed

4 files changed

+243
-7
lines changed

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ protected Void getDefault(Shape shape) {
259259
*/
260260
@Override
261261
public Void structureShape(StructureShape shape) {
262-
writers.useShapeWriter(shape, writer -> new StructureGenerator(model, symbolProvider, writer, shape).run());
262+
writers.useShapeWriter(shape, writer ->
263+
new StructureGenerator(model, symbolProvider, writer, shape, settings.generateServerSdk()).run());
263264
return null;
264265
}
265266

@@ -271,7 +272,8 @@ public Void structureShape(StructureShape shape) {
271272
*/
272273
@Override
273274
public Void unionShape(UnionShape shape) {
274-
writers.useShapeWriter(shape, writer -> new UnionGenerator(model, symbolProvider, writer, shape).run());
275+
writers.useShapeWriter(shape, writer ->
276+
new UnionGenerator(model, symbolProvider, writer, shape, settings.generateServerSdk()).run());
275277
return null;
276278
}
277279

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

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,25 @@ final class StructureGenerator implements Runnable {
3636
private final SymbolProvider symbolProvider;
3737
private final TypeScriptWriter writer;
3838
private final StructureShape shape;
39+
private final boolean includeValidation;
3940

41+
/**
42+
* sets 'includeValidation' to 'false' for backwards compatibility.
43+
*/
4044
StructureGenerator(Model model, SymbolProvider symbolProvider, TypeScriptWriter writer, StructureShape shape) {
45+
this(model, symbolProvider, writer, shape, false);
46+
}
47+
48+
StructureGenerator(Model model,
49+
SymbolProvider symbolProvider,
50+
TypeScriptWriter writer,
51+
StructureShape shape,
52+
boolean includeValidation) {
4153
this.model = model;
4254
this.symbolProvider = symbolProvider;
4355
this.writer = writer;
4456
this.shape = shape;
57+
this.includeValidation = includeValidation;
4558
}
4659

4760
@Override
@@ -64,6 +77,7 @@ public void run() {
6477
* structure Person {
6578
* @required
6679
* name: String,
80+
* @range(min: 1)
6781
* age: Integer,
6882
* }
6983
* }</pre>
@@ -80,6 +94,22 @@ public void run() {
8094
* export const filterSensitiveLog = (obj: Person): any => ({...obj});
8195
* }
8296
* }</pre>
97+
*
98+
* <p>If validation is enabled, it generates the following:
99+
*
100+
* <pre>{@code
101+
* export interface Person {
102+
* name: string | undefined;
103+
* age?: number | null;
104+
* }
105+
*
106+
* export namespace Person {
107+
* export const filterSensitiveLog = (obj: Person): any => ({...obj});
108+
* export const validate = (obj: Person): ValidationFailure[] => {
109+
* // validation
110+
* }
111+
* }
112+
* }</pre>
83113
*/
84114
private void renderNonErrorStructure() {
85115
Symbol symbol = symbolProvider.toSymbol(shape);
@@ -102,7 +132,7 @@ private void renderNonErrorStructure() {
102132
config.writeMembers(writer, shape);
103133
writer.closeBlock("}");
104134
writer.write("");
105-
renderStructureNamespace();
135+
renderStructureNamespace(config, includeValidation);
106136
}
107137

108138
/**
@@ -162,21 +192,33 @@ private void renderErrorStructure() {
162192
structuredMemberWriter.writeMembers(writer, shape);
163193
writer.closeBlock("}"); // interface
164194
writer.write("");
165-
renderStructureNamespace();
195+
renderStructureNamespace(structuredMemberWriter, false);
166196
}
167197

168-
private void renderStructureNamespace() {
198+
private void renderStructureNamespace(StructuredMemberWriter structuredMemberWriter, boolean includeValidation) {
169199
Symbol symbol = symbolProvider.toSymbol(shape);
170200
writer.openBlock("export namespace $L {", "}", symbol.getName(), () -> {
171201
String objectParam = "obj";
172202
writer.openBlock("export const filterSensitiveLog = ($L: $L): any => ({", "})",
173203
objectParam, symbol.getName(),
174204
() -> {
175-
StructuredMemberWriter structuredMemberWriter = new StructuredMemberWriter(
176-
model, symbolProvider, shape.getAllMembers().values());
177205
structuredMemberWriter.writeFilterSensitiveLog(writer, objectParam);
178206
}
179207
);
208+
209+
if (!includeValidation) {
210+
return;
211+
}
212+
213+
structuredMemberWriter.writeMemberValidators(writer);
214+
215+
writer.addImport("ValidationFailure", "__ValidationFailure", "@aws-smithy/server-common");
216+
writer.openBlock("export const validate = ($L: $L): __ValidationFailure[] => {", "}",
217+
objectParam, symbol.getName(),
218+
() -> {
219+
structuredMemberWriter.writeValidate(writer, objectParam);
220+
}
221+
);
180222
});
181223
}
182224
}

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

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,22 +15,34 @@
1515

1616
package software.amazon.smithy.typescript.codegen;
1717

18+
import java.util.ArrayList;
1819
import java.util.Collection;
1920
import java.util.HashSet;
2021
import java.util.LinkedHashSet;
22+
import java.util.List;
2123
import java.util.Set;
2224
import software.amazon.smithy.codegen.core.CodegenException;
25+
import software.amazon.smithy.codegen.core.Symbol;
2326
import software.amazon.smithy.codegen.core.SymbolProvider;
2427
import software.amazon.smithy.model.Model;
2528
import software.amazon.smithy.model.shapes.CollectionShape;
2629
import software.amazon.smithy.model.shapes.MapShape;
2730
import software.amazon.smithy.model.shapes.MemberShape;
2831
import software.amazon.smithy.model.shapes.Shape;
32+
import software.amazon.smithy.model.shapes.ShapeId;
2933
import software.amazon.smithy.model.shapes.SimpleShape;
34+
import software.amazon.smithy.model.shapes.StringShape;
3035
import software.amazon.smithy.model.shapes.StructureShape;
36+
import software.amazon.smithy.model.traits.EnumTrait;
3137
import software.amazon.smithy.model.traits.IdempotencyTokenTrait;
38+
import software.amazon.smithy.model.traits.LengthTrait;
39+
import software.amazon.smithy.model.traits.PatternTrait;
40+
import software.amazon.smithy.model.traits.RangeTrait;
41+
import software.amazon.smithy.model.traits.RequiredTrait;
3242
import software.amazon.smithy.model.traits.SensitiveTrait;
3343
import software.amazon.smithy.model.traits.StreamingTrait;
44+
import software.amazon.smithy.model.traits.Trait;
45+
import software.amazon.smithy.model.traits.UniqueItemsTrait;
3446
import software.amazon.smithy.utils.SmithyInternalApi;
3547

3648
/**
@@ -266,4 +278,153 @@ private String getSanitizedMemberName(MemberShape member) {
266278
private boolean isRequiredMember(MemberShape member) {
267279
return member.isRequired() && !member.hasTrait(IdempotencyTokenTrait.class);
268280
}
281+
282+
/**
283+
* Writes a const map of member validators into the namespace for use by the validate method.
284+
*
285+
* @param writer the writer for the type, currently positioned in the type's exported namespace
286+
*/
287+
void writeMemberValidators(TypeScriptWriter writer) {
288+
writer.openBlock("const memberValidators = {", "};", () -> {
289+
for (MemberShape member : members) {
290+
final Shape targetShape = model.expectShape(member.getTarget());
291+
Collection<Trait> constraintTraits = getConstraintTraits(member);
292+
writer.writeInline("$L: ", getSanitizedMemberName(member));
293+
writeShapeValidator(writer, targetShape, constraintTraits);
294+
}
295+
});
296+
}
297+
298+
private void writeShapeValidator(TypeScriptWriter writer, Shape shape, Collection<Trait> constraintTraits) {
299+
if (shape.isStructureShape() || shape.isUnionShape()) {
300+
writer.addImport("CompositeStructureValidator",
301+
"__CompositeStructureValidator",
302+
"@aws-smithy/server-common");
303+
writer.openBlock("new __CompositeStructureValidator<$T>(", "),",
304+
symbolProvider.toSymbol(shape),
305+
() -> {
306+
writeCompositeValidator(writer, shape, constraintTraits);
307+
writer.write("$T.validate,", symbolProvider.toSymbol(shape));
308+
}
309+
);
310+
} else if (shape.isListShape() || shape.isSetShape()) {
311+
writer.addImport("CompositeCollectionValidator",
312+
"__CompositeCollectionValidator",
313+
"@aws-smithy/server-common");
314+
MemberShape collectionMemberShape = ((CollectionShape) shape).getMember();
315+
Shape collectionMemberTargetShape = model.expectShape(collectionMemberShape.getTarget());
316+
writer.openBlock("new __CompositeCollectionValidator<$T>(", "),",
317+
symbolProvider.toSymbol(collectionMemberTargetShape),
318+
() -> {
319+
writeCompositeValidator(writer, shape, constraintTraits);
320+
writeShapeValidator(writer,
321+
collectionMemberTargetShape,
322+
getConstraintTraits(collectionMemberShape));
323+
});
324+
} else if (shape.isMapShape()) {
325+
writer.addImport("CompositeMapValidator",
326+
"__CompositeMapValidator",
327+
"@aws-smithy/server-common");
328+
MapShape mapShape = (MapShape) shape;
329+
final MemberShape keyShape = mapShape.getKey();
330+
final MemberShape valueShape = mapShape.getValue();
331+
writer.openBlock("new __CompositeMapValidator<$T>(", "),",
332+
symbolProvider.toSymbol(model.expectShape(valueShape.getTarget())),
333+
() -> {
334+
writeCompositeValidator(writer, mapShape, constraintTraits);
335+
writeShapeValidator(writer,
336+
model.expectShape(keyShape.getTarget()),
337+
getConstraintTraits(keyShape));
338+
writeShapeValidator(writer,
339+
model.expectShape(valueShape.getTarget()),
340+
getConstraintTraits(valueShape));
341+
});
342+
} else if (shape instanceof SimpleShape) {
343+
writeCompositeValidator(writer, shape, constraintTraits);
344+
} else {
345+
throw new IllegalArgumentException(
346+
String.format("Unsupported shape found when generating validator: %s", shape));
347+
}
348+
}
349+
350+
private void writeCompositeValidator(TypeScriptWriter writer,
351+
Shape shape,
352+
Collection<Trait> constraints) {
353+
if (constraints.isEmpty()) {
354+
writer.addImport("NoOpValidator", "__NoOpValidator", "@aws-smithy/server-common");
355+
writer.write("new __NoOpValidator(),");
356+
return;
357+
}
358+
writer.addImport("CompositeValidator",
359+
"__CompositeValidator",
360+
"@aws-smithy/server-common");
361+
362+
Symbol symbol;
363+
if (shape instanceof StringShape) {
364+
// Don't let the TypeScript type for an enum narrow our validator's type too much
365+
symbol = symbolProvider.toSymbol(model.expectShape(ShapeId.from("smithy.api#String")));
366+
} else {
367+
symbol = symbolProvider.toSymbol(shape);
368+
}
369+
370+
writer.openBlock("new __CompositeValidator<$T>([", "]),", symbol,
371+
() -> {
372+
for (Trait t : constraints) {
373+
writeValidator(writer, t);
374+
}
375+
}
376+
);
377+
}
378+
379+
void writeValidate(TypeScriptWriter writer, String param) {
380+
writer.openBlock("return [", "];", () -> {
381+
for (MemberShape member : members) {
382+
writer.write("...memberValidators.$1L.validate($2L.$1L, $3S),",
383+
getSanitizedMemberName(member), param, member.getMemberName());
384+
}
385+
});
386+
}
387+
388+
private void writeValidator(TypeScriptWriter writer, Trait trait) {
389+
if (trait instanceof RequiredTrait) {
390+
writer.addImport("RequiredValidator", "__RequiredValidator", "@aws-smithy/server-common");
391+
writer.write("new __RequiredValidator(),");
392+
} else if (trait instanceof EnumTrait) {
393+
writer.addImport("EnumValidator", "__EnumValidator", "@aws-smithy/server-common");
394+
writer.openBlock("new __EnumValidator([", "]),", () -> {
395+
for (String e : ((EnumTrait) trait).getEnumDefinitionValues()) {
396+
writer.write("$S,", e);
397+
}
398+
});
399+
} else if (trait instanceof LengthTrait) {
400+
LengthTrait lengthTrait = (LengthTrait) trait;
401+
writer.addImport("LengthValidator", "__LengthValidator", "@aws-smithy/server-common");
402+
writer.write("new __LengthValidator($L, $L),",
403+
lengthTrait.getMin().map(Object::toString).orElse("undefined"),
404+
lengthTrait.getMax().map(Object::toString).orElse("undefined"));
405+
} else if (trait instanceof PatternTrait) {
406+
writer.addImport("PatternValidator", "__PatternValidator", "@aws-smithy/server-common");
407+
writer.write("new __PatternValidator($S),", ((PatternTrait) trait).getValue());
408+
} else if (trait instanceof RangeTrait) {
409+
RangeTrait rangeTrait = (RangeTrait) trait;
410+
writer.addImport("RangeValidator", "__RangeValidator", "@aws-smithy/server-common");
411+
writer.write("new __RangeValidator($L, $L),",
412+
rangeTrait.getMin().map(Object::toString).orElse("undefined"),
413+
rangeTrait.getMax().map(Object::toString).orElse("undefined"));
414+
} else if (trait instanceof UniqueItemsTrait) {
415+
writer.addImport("UniqueItemsValidator", "__UniqueItemsValidator", "@aws-smithy/server-common");
416+
writer.write("new __UniqueItemsValidator(),");
417+
}
418+
}
419+
420+
private Collection<Trait> getConstraintTraits(MemberShape member) {
421+
List<Trait> traits = new ArrayList<>();
422+
member.getTrait(RequiredTrait.class).ifPresent(traits::add);
423+
member.getMemberTrait(model, EnumTrait.class).ifPresent(traits::add);
424+
member.getMemberTrait(model, LengthTrait.class).ifPresent(traits::add);
425+
member.getMemberTrait(model, PatternTrait.class).ifPresent(traits::add);
426+
member.getMemberTrait(model, RangeTrait.class).ifPresent(traits::add);
427+
member.getMemberTrait(model, UniqueItemsTrait.class).ifPresent(traits::add);
428+
return traits;
429+
}
269430
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,13 +132,26 @@ final class UnionGenerator implements Runnable {
132132
private final Symbol symbol;
133133
private final UnionShape shape;
134134
private final Map<String, String> variantMap;
135+
private final boolean includeValidation;
135136

137+
/**
138+
* sets 'includeValidation' to 'false' for backwards compatibility.
139+
*/
136140
UnionGenerator(Model model, SymbolProvider symbolProvider, TypeScriptWriter writer, UnionShape shape) {
141+
this(model, symbolProvider, writer, shape, false);
142+
}
143+
144+
UnionGenerator(Model model,
145+
SymbolProvider symbolProvider,
146+
TypeScriptWriter writer,
147+
UnionShape shape,
148+
boolean includeValidation) {
137149
this.shape = shape;
138150
this.symbol = symbolProvider.toSymbol(shape);
139151
this.model = model;
140152
this.symbolProvider = symbolProvider;
141153
this.writer = writer;
154+
this.includeValidation = includeValidation;
142155

143156
variantMap = new TreeMap<>();
144157
for (MemberShape member : shape.getAllMembers().values()) {
@@ -165,6 +178,9 @@ public void run() {
165178
writeVisitorType();
166179
writeVisitorFunction();
167180
writeFilterSensitiveLog();
181+
if (includeValidation) {
182+
writeValidate();
183+
}
168184
});
169185
}
170186

@@ -245,4 +261,19 @@ private void writeFilterSensitiveLog() {
245261
}
246262
);
247263
}
264+
265+
private void writeValidate() {
266+
StructuredMemberWriter structuredMemberWriter = new StructuredMemberWriter(
267+
model, symbolProvider, shape.getAllMembers().values());
268+
269+
structuredMemberWriter.writeMemberValidators(writer);
270+
271+
writer.addImport("ValidationFailure", "__ValidationFailure", "@aws-smithy/server-common");
272+
writer.openBlock("export const validate = ($L: $L): __ValidationFailure[] => {", "}",
273+
"obj", symbol.getName(),
274+
() -> {
275+
structuredMemberWriter.writeValidate(writer, "obj");
276+
}
277+
);
278+
}
248279
}

0 commit comments

Comments
 (0)