From 666b7cae1cb6008570a8b3a64d6c3c63aac25888 Mon Sep 17 00:00:00 2001 From: airtoxin Date: Sun, 7 Mar 2021 05:57:41 +0900 Subject: [PATCH] Add schema to string codecs --- package.json | 4 +- src/Codec/Number.test.ts | 2 +- src/Codec/String.test.ts | 99 ++++++++++++++++++++++++++++++++++++++-- src/Codec/String.ts | 76 ++++++++++++++++++++---------- yarn.lock | 12 +++++ 5 files changed, 163 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 8f3a53a..e9419f4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@types/json-schema": "^7.0.7", + "@types/warning": "^3.0.0", "ajv": "^7.1.1", "eslint-plugin-prettier": "^3.1.3", "husky": "^4.2.5", @@ -34,7 +35,8 @@ "typescript": "^3.9.3" }, "dependencies": { - "date-fns": "^2.14.0" + "date-fns": "^2.14.0", + "warning": "^4.0.3" }, "peerDependencies": { "purify-ts": ">=0.16.0" diff --git a/src/Codec/Number.test.ts b/src/Codec/Number.test.ts index 23fd1af..d724fdc 100644 --- a/src/Codec/Number.test.ts +++ b/src/Codec/Number.test.ts @@ -100,7 +100,7 @@ describe("NumberRangedIn", () => { expect(validate(3.1)).toBeFalsy(); }); - it("multiple", () => { + it("complex pattern", () => { const schema = NumberRangedIn({ gt: 3, lte: 10 }).schema(); const validate = ajv.compile(schema); expect(NumberRangedIn({ gt: 3, lte: 10 }).schema()).toEqual({ diff --git a/src/Codec/String.test.ts b/src/Codec/String.test.ts index f7ea0e0..4730a4b 100644 --- a/src/Codec/String.test.ts +++ b/src/Codec/String.test.ts @@ -6,7 +6,9 @@ import { StringLengthRangedIn, } from "./String"; import { Left, Right } from "purify-ts"; +import Ajv from "ajv"; +const ajv = new Ajv(); const left = Left(expect.any(String)); describe("NonEmptyString", () => { @@ -25,6 +27,20 @@ describe("NonEmptyString", () => { expect(NonEmptyString.encode("asdf")).toBe("asdf"); }); }); + + describe("schema", () => { + it("test", () => { + const schema = NonEmptyString.schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + pattern: ".+", + }); + expect(validate("asdf")).toBeTruthy(); + expect(validate("")).toBeFalsy(); + expect(validate(10)).toBeFalsy(); + }); + }); }); describe("StringLengthRangedIn", () => { @@ -91,24 +107,97 @@ describe("StringLengthRangedIn", () => { expect(StringLengthRangedIn({ lt: 1 }).encode("asdf")).toBe("asdf"); }); }); + + describe("schema", () => { + it("gt test", () => { + const schema = StringLengthRangedIn({ gt: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + minLength: 4, + }); + expect(validate("asd")).toBeFalsy(); + expect(validate("asdf")).toBeTruthy(); + }); + + it("gte test", () => { + const schema = StringLengthRangedIn({ gte: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + minLength: 3, + }); + expect(validate("as")).toBeFalsy(); + expect(validate("asd")).toBeTruthy(); + }); + + it("lt test", () => { + const schema = StringLengthRangedIn({ lt: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + maxLength: 2, + }); + expect(validate("as")).toBeTruthy(); + expect(validate("asd")).toBeFalsy(); + }); + + it("lte test", () => { + const schema = StringLengthRangedIn({ lte: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + maxLength: 3, + }); + expect(validate("asd")).toBeTruthy(); + expect(validate("asdf")).toBeFalsy(); + }); + + it("complex pattern", () => { + const schema = StringLengthRangedIn({ gt: 2, lte: 4 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + minLength: 3, + maxLength: 4, + }); + expect(validate("as")).toBeFalsy(); + expect(validate("asd")).toBeTruthy(); + expect(validate("asdf")).toBeTruthy(); + expect(validate("asdfg")).toBeFalsy(); + }); + }); }); describe("RegExpMatchedString", () => { describe("decode", () => { it("should return Right when value is matched to regexp", () => { - expect(RegExpMatchedString(/^\d{4}\s\w{2}$/).decode("1234 ab")).toEqual( - Right("1234 ab") - ); + expect( + RegExpMatchedString("^\\d{4}\\s\\w{2}$").decode("1234 ab") + ).toEqual(Right("1234 ab")); }); it("should return Left when value is not matched to regexp", () => { - expect(RegExpMatchedString(/^\w+$/).decode("ab cd")).toEqual(left); + expect(RegExpMatchedString("^\\w+$").decode("ab cd")).toEqual(left); }); }); describe("encode", () => { it("should return string", () => { - expect(RegExpMatchedString(/^\w$/).encode("a")).toBe("a"); + expect(RegExpMatchedString("^\\w$").encode("a")).toBe("a"); + }); + }); + + describe("schema", () => { + it("test", () => { + const schema = RegExpMatchedString("^\\d{4}\\s\\w{2}$").schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + pattern: "^\\d{4}\\s\\w{2}$", + }); + expect(validate("1234 ab")).toBeTruthy(); + expect(validate("1234")).toBeFalsy(); }); }); }); diff --git a/src/Codec/String.ts b/src/Codec/String.ts index 681e36b..4ce1a33 100644 --- a/src/Codec/String.ts +++ b/src/Codec/String.ts @@ -1,13 +1,20 @@ -import { extendCodec } from "./utils"; +import { extendCodec, withSchema } from "./utils"; import { Codec, Left, Right, string } from "purify-ts"; import formatDate from "date-fns/format"; import parseDate from "date-fns/parse"; import { isValid } from "date-fns"; +import warning from "warning"; -export const NonEmptyString = extendCodec(string, (value) => { - if (value === "") return Left("value must not be empty"); - return Right(value); -}); +export const NonEmptyString = withSchema( + extendCodec(string, (value) => { + if (value === "") return Left("value must not be empty"); + return Right(value); + }), + (stringSchema) => ({ + ...stringSchema, + pattern: ".+", + }) +); export type StringLengthRangeOption = { gt?: number; @@ -22,25 +29,48 @@ export const StringLengthRangedIn = ({ lt, lte, }: StringLengthRangeOption) => - extendCodec(string, (value) => { - const length = value.length; - if (gt != null && !(gt < length)) - return Left(`length of ${value} must be greater than ${gt}`); - if (gte != null && !(gte <= length)) - return Left(`length of ${value} must be greater than equal ${gte}`); - if (lt != null && !(lt > length)) - return Left(`length of ${value} must be less than ${lt}`); - if (lte != null && !(lte >= length)) - return Left(`length of ${value} must be less than equal ${lte}`); - return Right(value); - }); + withSchema( + extendCodec(string, (value) => { + const length = value.length; + if (gt != null && !(gt < length)) + return Left(`length of ${value} must be greater than ${gt}`); + if (gte != null && !(gte <= length)) + return Left(`length of ${value} must be greater than equal ${gte}`); + if (lt != null && !(lt > length)) + return Left(`length of ${value} must be less than ${lt}`); + if (lte != null && !(lte >= length)) + return Left(`length of ${value} must be less than equal ${lte}`); + return Right(value); + }), + (stringSchema) => ({ + ...stringSchema, + ...(gte ? { minLength: gte } : {}), + ...(gt ? { minLength: gt + 1 } : {}), + ...(lte ? { maxLength: lte } : {}), + ...(lt ? { maxLength: lt - 1 } : {}), + }) + ); -export const RegExpMatchedString = (regexp: RegExp) => - extendCodec(string, (value) => { - if (!regexp.test(value)) - return Left(`${value} is not matched to ${regexp}`); - return Right(value); - }); +export const RegExpMatchedString = ( + regexp: RegExp | string, + flags?: string +) => { + warning( + typeof regexp === "string", + "RegExp object argument of RegExpMatchedString Codec was deprecated, use string argument and flag instead." + ); + return withSchema( + extendCodec(string, (value) => { + const re = new RegExp(regexp, flags); + if (!re.test(value)) return Left(`${value} is not matched to ${re}`); + return Right(value); + }), + (stringSchema) => ({ + ...stringSchema, + ...(typeof regexp === "string" ? { pattern: regexp } : {}), + }) + ); +}; export const FormattedStringFromDate = (format: string) => Codec.custom({ diff --git a/yarn.lock b/yarn.lock index 983522e..066b45d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1168,6 +1168,11 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" integrity sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw== +"@types/warning@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@types/warning/-/warning-3.0.0.tgz#0d2501268ad8f9962b740d387c4654f5f8e23e52" + integrity sha1-DSUBJorY+ZYrdA04fEZU9fjiPlI= + "@types/yargs-parser@*": version "20.2.0" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" @@ -6066,6 +6071,13 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" +warning@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + wcwidth@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8"