Skip to content

Commit

Permalink
Union support for string validator (#624)
Browse files Browse the repository at this point in the history
  • Loading branch information
lfportal authored Dec 24, 2019
1 parent 90c10a9 commit 05e5b39
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 70 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ OPTIONS
--all see all commands in CLI
```

_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.1/src/commands/help.ts)_
_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v2.2.3/src/commands/help.ts)_

## `spot init`

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ import {
int64Type,
objectType,
referenceType,
stringLiteralType,
stringType,
TypeTable
TypeTable,
unionType
} from "../../types";
import { StringValidator } from "./string-validator";

Expand Down Expand Up @@ -66,6 +68,18 @@ describe("validators", () => {
expect(result).toBe(true);
expect(validator.messages.length).toEqual(0);
});

test("should return true when value matches a union type", () => {
const result = validator.run(
{ name: "param", value: "unionElementA" },
unionType([
stringLiteralType("unionElementA"),
stringLiteralType("unionElementB")
])
);
expect(result).toBe(true);
expect(validator.messages.length).toEqual(0);
});
});

describe("invalid inputs", () => {
Expand Down Expand Up @@ -107,5 +121,19 @@ describe("validators", () => {
expect(result).toBe(false);
expect(validator.messages[0]).toEqual('".param.id" should be int64');
});

test("should return an error value doesn't match a union type", () => {
const result = validator.run(
{ name: "param", value: "unknownElement" },
unionType([
stringLiteralType("unionElementA"),
stringLiteralType("unionElementB")
])
);
expect(result).toBe(false);
expect(validator.messages[0]).toEqual(
'"param" should be a member of a union'
);
});
});
});
169 changes: 101 additions & 68 deletions lib/src/neu/validation-server/verifications/string-validator.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import assertNever from "assert-never";
import validator from "validator";
import {
ArrayType,
Expand All @@ -6,52 +7,41 @@ import {
ReferenceType,
Type,
TypeKind,
TypeTable
TypeTable,
UnionType
} from "../../types";

const {
NULL,
BOOLEAN,
DATE,
DATE_TIME,
STRING,
FLOAT,
DOUBLE,
FLOAT_LITERAL,
INT32,
INT64,
INT_LITERAL,
OBJECT,
ARRAY,
REFERENCE
} = TypeKind;

export interface StringInput {
name: string;
value: string | { [key: string]: unknown } | unknown[];
}

type ValidatorMap = {
[key in TypeKind]?: (str: string, options?: {}) => boolean | never;
};

export class StringValidator {
static validatorMap: ValidatorMap = {
[NULL]: validator.isEmpty,
[BOOLEAN]: validator.isBoolean,
[DATE]: validator.isISO8601,
[DATE_TIME]: validator.isISO8601,
[FLOAT]: validator.isFloat,
[INT32]: validator.isInt,
[INT64]: validator.isInt,
[DOUBLE]: validator.isFloat,
[FLOAT_LITERAL]: validator.isFloat,
[INT_LITERAL]: validator.isInt,
[STRING]: (str: string) => typeof str === "string"
};

static getErrorMessage(input: string, type: string): string {
return `"${input}" should be ${type}`;
static getErrorMessage(
input: string,
type: Exclude<Type, ObjectType | ArrayType | ReferenceType>
): string {
switch (type.kind) {
case TypeKind.NULL:
case TypeKind.BOOLEAN:
case TypeKind.STRING:
case TypeKind.FLOAT:
case TypeKind.DOUBLE:
case TypeKind.INT32:
case TypeKind.INT64:
case TypeKind.DATE:
case TypeKind.DATE_TIME:
return `"${input}" should be ${type.kind}`;
case TypeKind.BOOLEAN_LITERAL:
case TypeKind.STRING_LITERAL:
case TypeKind.FLOAT_LITERAL:
case TypeKind.INT_LITERAL:
return `"${input}" should be ${type.value}`;
case TypeKind.UNION:
return `"${input}" should be a member of a union`;
default:
assertNever(type);
}
}

messages: string[] = [];
Expand All @@ -66,36 +56,83 @@ export class StringValidator {
type: Type,
isMandatory: boolean = true
): boolean | never {
if (type.kind === OBJECT) {
return this.validateObject(input, type);
}

if (type.kind === ARRAY) {
return this.validateArray(input, type);
}

if (type.kind === REFERENCE) {
return this.validateReference(input, type);
}

const validator = StringValidator.validatorMap[type.kind];

if (typeof validator !== "function") {
throw new Error(
`StringValidator Err - no validator found for type ${type.kind}`
);
if (!input.value && !isMandatory) return true;

switch (type.kind) {
case TypeKind.NULL:
return this.validateWithValidator(input, type, validator.isEmpty);
case TypeKind.BOOLEAN:
return this.validateWithValidator(input, type, (str: string) =>
["true", "false"].includes(str.toLowerCase())
);
case TypeKind.BOOLEAN_LITERAL:
return this.validateWithValidator(
input,
type,
(str: string) => str.toLowerCase() === type.value.toString()
);
case TypeKind.STRING:
return this.validateWithValidator(
input,
type,
(str: string) => typeof str === "string"
);
case TypeKind.STRING_LITERAL:
return this.validateWithValidator(
input,
type,
(str: string) => typeof str === "string" && str === type.value
);
case TypeKind.FLOAT:
case TypeKind.DOUBLE:
return this.validateWithValidator(input, type, validator.isFloat);
case TypeKind.FLOAT_LITERAL:
return this.validateWithValidator(
input,
type,
(str: string) => validator.isFloat(str) && Number(str) === type.value
);
case TypeKind.INT32:
case TypeKind.INT64:
return this.validateWithValidator(input, type, validator.isInt);
case TypeKind.INT_LITERAL:
return this.validateWithValidator(
input,
type,
(str: string) => validator.isInt(str) && Number(str) === type.value
);
case TypeKind.DATE:
case TypeKind.DATE_TIME:
return this.validateWithValidator(input, type, validator.isISO8601);
case TypeKind.OBJECT:
return this.validateObject(input, type);
case TypeKind.ARRAY:
return this.validateArray(input, type);
case TypeKind.UNION:
const anyValid = type.types.some(t => {
const unionStringValidator = new StringValidator(this.typeTable);
return unionStringValidator.run(input, t, isMandatory);
});
if (!anyValid) {
this.messages.push(StringValidator.getErrorMessage(input.name, type));
}
return anyValid;
case TypeKind.REFERENCE:
return this.run(input, dereferenceType(type, this.typeTable));
default:
assertNever(type);
}
}

const isNotRequired = !input.value && !isMandatory;

const isValid = isNotRequired || validator(`${input.value}`);

private validateWithValidator(
input: StringInput,
type: Exclude<Type, ObjectType | ArrayType | ReferenceType | UnionType>,
validatorFn: (str: string, options?: {}) => boolean
): boolean {
const isValid = validatorFn(`${input.value}`);
if (!isValid) {
this.messages.push(
StringValidator.getErrorMessage(input.name, type.kind)
);
this.messages.push(StringValidator.getErrorMessage(input.name, type));
}

return isValid;
}

Expand Down Expand Up @@ -130,8 +167,4 @@ export class StringValidator {

return Array.isArray(input.value) && validateItems();
}

private validateReference(input: StringInput, type: ReferenceType) {
return this.run(input, dereferenceType(type, this.typeTable));
}
}

0 comments on commit 05e5b39

Please sign in to comment.