Skip to content

Commit

Permalink
Add validation for intEnum shapes
Browse files Browse the repository at this point in the history
This PR adds an IntegerEnumValidator, IntegerEnumValidationFailure and
the relevant tests to the ssdk libs. StructuredMemberWriter was updated
to use IntegerEnumValidator when generating shape validators for intEnum
shapes.

Local testing:
- Update js sdk to use Smithy 1.26.4, which has the protocol tests for
intEnum
- Update js sdk to use local version of smithy-typescript-ssdk-libs,
specificaly the server protocol test packages
- Run `yarn generate-clients --server-artifacts` and
`yarn test:server-protocols`
  • Loading branch information
milesziemer committed Dec 8, 2022
1 parent f139025 commit 7acd0e1
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import software.amazon.smithy.codegen.core.SymbolProvider;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.shapes.CollectionShape;
import software.amazon.smithy.model.shapes.IntEnumShape;
import software.amazon.smithy.model.shapes.MapShape;
import software.amazon.smithy.model.shapes.MemberShape;
import software.amazon.smithy.model.shapes.Shape;
Expand Down Expand Up @@ -473,7 +474,9 @@ private void writeShapeValidator(TypeScriptWriter writer,
Shape shape,
Collection<Trait> constraints,
String trailer) {
if (constraints.isEmpty()) {
boolean shouldWriteIntEnumValidator = shape.isIntEnumShape();

if (constraints.isEmpty() && !shouldWriteIntEnumValidator) {
writer.addImport("NoOpValidator", "__NoOpValidator", "@aws-smithy/server-common");
writer.write("new __NoOpValidator()" + trailer);
return;
Expand All @@ -482,6 +485,15 @@ private void writeShapeValidator(TypeScriptWriter writer,
writer.addImport("CompositeValidator", "__CompositeValidator", "@aws-smithy/server-common");
writer.openBlock("new __CompositeValidator<$T>([", "])" + trailer, getSymbolForValidatedType(shape),
() -> {
if (shouldWriteIntEnumValidator) {
writer.addImport("IntegerEnumValidator", "__IntegerEnumValidator", "@aws-smithy/server-common");
writer.openBlock("new __IntegerEnumValidator([", "]),", () -> {
for (int i : ((IntEnumShape) shape).getEnumValues().values()) {
writer.write("$L,", i);
}
});
}

for (Trait t : constraints) {
writeSingleConstraintValidator(writer, t);
}
Expand Down Expand Up @@ -575,6 +587,8 @@ private Symbol getValidatorValueType(Shape shape) {
private Symbol getSymbolForValidatedType(Shape shape) {
if (shape instanceof StringShape) {
return symbolProvider.toSymbol(model.expectShape(ShapeId.from("smithy.api#String")));
} else if (shape instanceof IntEnumShape) {
return symbolProvider.toSymbol(model.expectShape(ShapeId.from("smithy.api#Integer")));
}

// Streaming blob inputs can also take string, Uint8Array and Buffer, so we widen the symbol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import {
EnumValidationFailure,
generateValidationMessage,
IntegerEnumValidationFailure,
LengthValidationFailure,
PatternValidationFailure,
RangeValidationFailure,
Expand Down Expand Up @@ -63,6 +64,17 @@ describe("message formatting", () => {
"Value pear at '/test' failed to satisfy constraint: Member must satisfy enum value set: [apple, banana]"
);
});
it("formats integer enum failures", () => {
const failure: IntegerEnumValidationFailure = {
constraintType: "integerEnum",
constraintValues: [1, 2],
failureValue: 3,
path: "/test",
};
expect(generateValidationMessage(failure)).toEqual(
"Value 3 at '/test' failed to satisfy constraint: Member must satisfy enum value set: [1, 2]"
);
});
describe("formats length failures", () => {
it("with only min values", () => {
const failure: LengthValidationFailure = {
Expand Down
12 changes: 12 additions & 0 deletions smithy-typescript-ssdk-libs/server-common/src/validation/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export interface EnumValidationFailure extends StandardValidationFailure<string,
constraintValues: string[];
}

export interface IntegerEnumValidationFailure extends StandardValidationFailure<number, number> {
constraintType: "integerEnum";
constraintValues: number[];
}

export interface LengthValidationFailure extends StandardValidationFailure<number | undefined, number> {
constraintType: "length";
constraintValues: [number, number] | [undefined, number] | [number, undefined];
Expand Down Expand Up @@ -63,6 +68,7 @@ export interface UniqueItemsValidationFailure {

export type ValidationFailure =
| EnumValidationFailure
| IntegerEnumValidationFailure
| LengthValidationFailure
| PatternValidationFailure
| RangeValidationFailure
Expand Down Expand Up @@ -124,6 +130,12 @@ export const generateValidationMessage = (failure: ValidationFailure): string =>
.join(", ")}]`;
break;
}
case "integerEnum": {
suffix = `must satisfy enum value set: [${failure.constraintValues
.sort((a, b) => a - b)
.join(", ")}]`;
break;
}
case "length": {
if (failure.failureValue !== null) {
prefix = prefix + " with length";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import {
CompositeValidator,
EnumValidator,
IntegerEnumValidator,
LengthValidator,
PatternValidator,
RangeValidator,
Expand All @@ -36,6 +37,9 @@ describe("sensitive validation", () => {
it("with enums", () => {
expect(sensitize(new EnumValidator(["apple", "banana", "orange"]), "pear").failureValue).toBeNull();
});
it("with integer enums", () => {
expect(sensitize(new IntegerEnumValidator([1, 2, 3]), 0).failureValue).toBeNull();
});
it("with length", () => {
expect(sensitize(new LengthValidator(2, 4), "pears").failureValue).toBeNull();
});
Expand Down Expand Up @@ -65,6 +69,23 @@ describe("enum validation", () => {
});
});

describe("integer enum validation", () => {
const integerEnumValidator = new IntegerEnumValidator([1, 2, 3]);

it("does not fail when the enum value is found", () => {
expect(integerEnumValidator.validate(1, "test")).toBeNull();
});

it("fails when the enum value is not found", () => {
expect(integerEnumValidator.validate(0, "test")).toEqual({
constraintType: "integerEnum",
constraintValues: [1, 2, 3],
path: "test",
failureValue: 0,
});
});
});

describe("length validation", () => {
const threeLengthThings = ["foo", { a: 1, b: 2, c: 3 }, ["a", "b", "c"], new Uint8Array([13, 37, 42])];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { RE2 } from "re2-wasm";
import { findDuplicates } from "../unique";
import {
EnumValidationFailure,
IntegerEnumValidationFailure,
LengthValidationFailure,
PatternValidationFailure,
RangeValidationFailure,
Expand Down Expand Up @@ -175,6 +176,31 @@ export class EnumValidator implements SingleConstraintValidator<string, EnumVali
}
}

export class IntegerEnumValidator implements SingleConstraintValidator<number, IntegerEnumValidationFailure> {
private readonly allowedValues: number[];

constructor(allowedValues: readonly number[]) {
this.allowedValues = allowedValues.slice();
}

validate(input: number | undefined | null, path: string): IntegerEnumValidationFailure | null {
if (input === null || input === undefined) {
return null;
}

if (this.allowedValues.indexOf(input) < 0) {
return {
constraintType: "integerEnum",
constraintValues: this.allowedValues.slice(),
path: path,
failureValue: input,
};
}

return null;
}
}

type LengthCheckable = string | { length: number } | Record<string, any>;

export class LengthValidator implements SingleConstraintValidator<LengthCheckable, LengthValidationFailure> {
Expand Down

0 comments on commit 7acd0e1

Please sign in to comment.