From 2ac0c96848273b2e52985b4660c3989720a923d9 Mon Sep 17 00:00:00 2001 From: airtoxin Date: Sun, 7 Mar 2021 04:45:56 +0900 Subject: [PATCH 1/5] Add withSchema --- package.json | 1 + src/Codec/utils.ts | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d2dd6cb..a8a5379 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "prepare": "tsdx build" }, "devDependencies": { + "@types/json-schema": "^7.0.7", "eslint-plugin-prettier": "^3.1.3", "husky": "^4.2.5", "prettier": "^2.0.5", diff --git a/src/Codec/utils.ts b/src/Codec/utils.ts index 96c58e3..a037096 100644 --- a/src/Codec/utils.ts +++ b/src/Codec/utils.ts @@ -1,16 +1,27 @@ import { Codec, Either } from "purify-ts"; +import { JSONSchema6 } from "json-schema"; + +export const withSchema = ( + codec: Codec, + schemaExtension: (baseSchema: JSONSchema6) => JSONSchema6 +): Codec => { + return Codec.custom({ + ...codec, + schema: () => schemaExtension(codec.schema()), + }); +}; export const extendCodec = ( base: Codec, ...decoders: Array<(value: T) => Either> ): Codec => { return Codec.custom({ + ...base, decode: (value) => (decoders ?? []).reduce( (decoded, decoder) => decoded.chain(decoder), base.decode(value) ), - encode: base.encode, }); }; From 8717d63b8c13c042df71c1543896f397508ce1c2 Mon Sep 17 00:00:00 2001 From: airtoxin Date: Sun, 7 Mar 2021 05:29:19 +0900 Subject: [PATCH 2/5] Add schema to number codecs --- package.json | 1 + src/Codec/Number.test.ts | 117 +++++++++++++++++++++++++++++++++++++++ src/Codec/Number.ts | 60 ++++++++++++++------ yarn.lock | 22 +++++++- 4 files changed, 181 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index a8a5379..8f3a53a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@types/json-schema": "^7.0.7", + "ajv": "^7.1.1", "eslint-plugin-prettier": "^3.1.3", "husky": "^4.2.5", "prettier": "^2.0.5", diff --git a/src/Codec/Number.test.ts b/src/Codec/Number.test.ts index edb31b6..23fd1af 100644 --- a/src/Codec/Number.test.ts +++ b/src/Codec/Number.test.ts @@ -5,7 +5,9 @@ import { NumberFromString, NumberRangedIn, } from "./Number"; +import Ajv from "ajv"; +const ajv = new Ajv(); const left = Left(expect.any(String)); describe("NumberRangedIn", () => { @@ -48,6 +50,70 @@ describe("NumberRangedIn", () => { expect(NumberRangedIn({ gt: 1 }).encode(10)).toBe(10); }); }); + + describe("schema", () => { + it("gt test", () => { + const schema = NumberRangedIn({ gt: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "number", + exclusiveMinimum: 3, + }); + expect(validate(4)).toBeTruthy(); + expect(validate(3.1)).toBeTruthy(); + expect(validate(3)).toBeFalsy(); + }); + + it("gte test", () => { + const schema = NumberRangedIn({ gte: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "number", + minimum: 3, + }); + expect(validate(4)).toBeTruthy(); + expect(validate(3)).toBeTruthy(); + expect(validate(2.9)).toBeFalsy(); + }); + + it("lt test", () => { + const schema = NumberRangedIn({ lt: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "number", + exclusiveMaximum: 3, + }); + expect(validate(2)).toBeTruthy(); + expect(validate(2.9)).toBeTruthy(); + expect(validate(3)).toBeFalsy(); + }); + + it("lte convert to maximum", () => { + const schema = NumberRangedIn({ lte: 3 }).schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "number", + maximum: 3, + }); + expect(validate(2)).toBeTruthy(); + expect(validate(3)).toBeTruthy(); + expect(validate(3.1)).toBeFalsy(); + }); + + it("multiple", () => { + const schema = NumberRangedIn({ gt: 3, lte: 10 }).schema(); + const validate = ajv.compile(schema); + expect(NumberRangedIn({ gt: 3, lte: 10 }).schema()).toEqual({ + type: "number", + maximum: 10, + exclusiveMinimum: 3, + }); + expect(validate(3)).toBeFalsy(); + expect(validate(3.1)).toBeTruthy(); + expect(validate(10)).toBeTruthy(); + expect(validate(10.1)).toBeFalsy(); + }); + }); }); describe("NumberFromString", () => { @@ -76,12 +142,29 @@ describe("NumberFromString", () => { expect(NumberFromString.encode(12.34)).toBe("12.34"); }); }); + + describe("schema", () => { + it("test", () => { + const schema = NumberFromString.schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + pattern: "^-?\\d+(?:\\.\\d*)?(?:e\\d+)?$", + }); + expect(validate("-12.34")).toBeTruthy(); + expect(validate("3.2e10")).toBeTruthy(); + expect(validate("Infinity")).toBeFalsy(); + expect(validate("")).toBeFalsy(); + expect(validate(10)).toBeFalsy(); + }); + }); }); describe("Integer", () => { describe("decode", () => { it("should return Right when value is integer", () => { expect(Integer.decode(1234)).toEqual(Right(1234)); + expect(Integer.decode(-1234)).toEqual(Right(-1234)); expect(Integer.decode(0.9999999999999999999999999)).toEqual(Right(1)); }); @@ -103,6 +186,23 @@ describe("Integer", () => { expect(Integer.encode(1234)).toBe(1234); }); }); + + describe("schema", () => { + it("test", () => { + const schema = Integer.schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "integer", + }); + expect(validate(1234)).toBeTruthy(); + expect(validate(-1234)).toBeTruthy(); + expect(validate(0.9999999999999999999999999)).toBeTruthy(); + expect(validate(12.34)).toBeFalsy(); + expect(validate(-12.34)).toBeFalsy(); + expect(validate(NaN)).toBeFalsy(); + expect(validate(Infinity)).toBeFalsy(); + }); + }); }); describe("IntegerFromString", () => { @@ -133,4 +233,21 @@ describe("IntegerFromString", () => { expect(IntegerFromString.encode(1234)).toBe("1234"); }); }); + + describe("schema", () => { + it("test", () => { + const schema = IntegerFromString.schema(); + const validate = ajv.compile(schema); + expect(schema).toEqual({ + type: "string", + pattern: "^-?\\d+(?:e\\d+)?$", + }); + expect(validate("1234")).toBeTruthy(); + expect(validate("-1234")).toBeTruthy(); + expect(validate("12.34")).toBeFalsy(); + expect(validate("-12.34")).toBeFalsy(); + expect(validate("NaN")).toBeFalsy(); + expect(validate("Infinity")).toBeFalsy(); + }); + }); }); diff --git a/src/Codec/Number.ts b/src/Codec/Number.ts index 23bb13f..6f99b24 100644 --- a/src/Codec/Number.ts +++ b/src/Codec/Number.ts @@ -1,4 +1,4 @@ -import { chainCodec, extendCodec } from "./utils"; +import { chainCodec, extendCodec, withSchema } from "./utils"; import { Codec, Left, number, Right, string } from "purify-ts"; export type NumberRangeOption = { @@ -9,17 +9,26 @@ export type NumberRangeOption = { }; export const NumberRangedIn = ({ gt, gte, lt, lte }: NumberRangeOption) => - extendCodec(number, (value) => { - if (gt != null && !(gt < value)) - return Left(`${value} must be greater than ${gt}`); - if (gte != null && !(gte <= value)) - return Left(`${value} must be greater than equal ${gte}`); - if (lt != null && !(lt > value)) - return Left(`${value} must be less than ${lt}`); - if (lte != null && !(lte >= value)) - return Left(`${value} must be less than equal ${lte}`); - return Right(value); - }); + withSchema( + extendCodec(number, (value) => { + if (gt != null && !(gt < value)) + return Left(`${value} must be greater than ${gt}`); + if (gte != null && !(gte <= value)) + return Left(`${value} must be greater than equal ${gte}`); + if (lt != null && !(lt > value)) + return Left(`${value} must be less than ${lt}`); + if (lte != null && !(lte >= value)) + return Left(`${value} must be less than equal ${lte}`); + return Right(value); + }), + (numberSchema) => ({ + ...numberSchema, + ...(gte ? { minimum: gte } : {}), + ...(gt ? { exclusiveMinimum: gt } : {}), + ...(lte ? { maximum: lte } : {}), + ...(lt ? { exclusiveMaximum: lt } : {}), + }) + ); export const NumberFromString = Codec.custom({ decode: (value) => @@ -31,12 +40,27 @@ export const NumberFromString = Codec.custom({ return Right(num); }), encode: (value) => `${value}`, + schema: () => ({ + type: "string", + pattern: "^-?\\d+(?:\\.\\d*)?(?:e\\d+)?$", + }), }); -export const Integer = extendCodec(number, (value) => { - if (value !== Math.floor(value)) return Left(`${value} is not integer`); - if (!Number.isFinite(value)) return Left(`${value} is not finite number`); - return Right(value); -}); +export const Integer = withSchema( + extendCodec(number, (value) => { + if (value !== Math.floor(value)) return Left(`${value} is not integer`); + if (!Number.isFinite(value)) return Left(`${value} is not finite number`); + return Right(value); + }), + () => ({ + type: "integer", + }) +); -export const IntegerFromString = chainCodec(NumberFromString, Integer); +export const IntegerFromString = withSchema( + chainCodec(NumberFromString, Integer), + () => ({ + type: "string", + pattern: "^-?\\d+(?:e\\d+)?$", + }) +); diff --git a/yarn.lock b/yarn.lock index 5d1e4cf..983522e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1131,7 +1131,7 @@ dependencies: jest-diff "^24.3.0" -"@types/json-schema@^7.0.3": +"@types/json-schema@^7.0.3", "@types/json-schema@^7.0.7": version "7.0.7" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.7.tgz#98a993516c859eb0d5c4c8f098317a9ea68db9ad" integrity sha512-cxWFQVseBm6O9Gbw1IWb8r6OS4OhSt3hPZLkFApLjM8TEXROBuQGLAH2i2gZpcXdLBIrpXuTDhH7Vbm1iXmNGA== @@ -1271,6 +1271,16 @@ ajv@^6.10.0, ajv@^6.10.2, ajv@^6.12.3: json-schema-traverse "^0.4.1" uri-js "^4.2.2" +ajv@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-7.1.1.tgz#1e6b37a454021fa9941713f38b952fc1c8d32a84" + integrity sha512-ga/aqDYnUy/o7vbsRTFhhTsNeXiYb5JWDIcRIeZfwRNCefwjNTVYCGdGSUrEmiu3yDK3vFvNbgJxvrQW4JXrYQ== + dependencies: + fast-deep-equal "^3.1.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + uri-js "^4.2.2" + ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" @@ -3865,6 +3875,11 @@ json-schema-traverse@^0.4.1: resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + json-schema@0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.2.3.tgz#b480c892e59a2f05954ce727bd3f2a4e882f9e13" @@ -5032,6 +5047,11 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" From 666b7cae1cb6008570a8b3a64d6c3c63aac25888 Mon Sep 17 00:00:00 2001 From: airtoxin Date: Sun, 7 Mar 2021 05:57:41 +0900 Subject: [PATCH 3/5] 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" From 25fd2113b683d224ea7048c33e700da734c3a2d8 Mon Sep 17 00:00:00 2001 From: airtoxin Date: Sun, 7 Mar 2021 06:11:38 +0900 Subject: [PATCH 4/5] Add schema test for Interface --- src/Codec/Interface.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Codec/Interface.test.ts b/src/Codec/Interface.test.ts index e193458..f7d1128 100644 --- a/src/Codec/Interface.test.ts +++ b/src/Codec/Interface.test.ts @@ -1,8 +1,9 @@ import { Interface } from "./Interface"; -import { GetType, optional, Right, string } from "purify-ts"; +import { Codec, GetType, optional, Right, string } from "purify-ts"; describe("Interface", () => { - const c = Interface({ str: string, ops: optional(string) }); + const def = { str: string, ops: optional(string) }; + const c = Interface(def); it("should return interface Codec", () => { expect(c.decode({ str: "foo" })).toEqual(Right({ str: "foo" })); @@ -17,4 +18,8 @@ describe("Interface", () => { const v: GetType = { str: "foo" }; expect(v).toEqual({ str: "foo" }); }); + + it("should return same schema to original interface Codec", () => { + expect(c.schema()).toEqual(Codec.interface(def).schema()); + }); }); From 69b95ba330d9def49d3f1df4f2f1d2d481f35610 Mon Sep 17 00:00:00 2001 From: airtoxin Date: Sun, 7 Mar 2021 06:29:53 +0900 Subject: [PATCH 5/5] Add documentation --- README.md | 45 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4bd22e3..fd3e30c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # purify-ts-extra-codec -Extra utility codecs for [purify-ts](https://gigobyte.github.io/purify/). +🛠 Extra utility codecs for [purify-ts](https://gigobyte.github.io/purify/). ## Install @@ -32,6 +32,8 @@ ObjCodec.decode({ str: "foo" }) // Right({ str: "foo" }); const obj: GetInterface = { str: "foo" }; ``` +❤️ Support `schema`. + ### String Codec Module #### NonEmptyString @@ -44,6 +46,8 @@ NonEmptyString.decode(""); // Left("[error message]") NonEmptyString.decode(1234); // Left("[error message]") ``` +❤️ Support `schema`. + #### StringLengthRangedIn Ensure length of input string is in range. @@ -54,16 +58,25 @@ StringLengthRangedIn({ gte: 5, lte: 5 }).decode("asdf"); // Left("[error message StringLengthRangedIn({ gte: 5, lte: 5 }).decode(1234); // Left("[error message]") ``` +❤️ Support `schema`. + #### RegExpMatchedString Ensure input string matched to RegExp. ```typescript +RegExpMatchedString("\\w+").decode("asdf"); // Right("asdf") +RegExpMatchedString("\\w+").decode("1234"); // Left("[error message]") +RegExpMatchedString("\\w+").decode(1234); // Left("[error message]") + +// those are deprecated interface (Non schema support) RegExpMatchedString(/\w+/).decode("asdf"); // Right("asdf") RegExpMatchedString(/\w+/).decode("1234"); // Left("[error message]") RegExpMatchedString(/\w+/).decode(1234); // Left("[error message]") ``` +❤️ Support `schema`. + #### FormattedStringFromDate Convert Date instance into formatted string. @@ -74,6 +87,8 @@ FormattedStringFromDate("yyyy_MM_dd").decode(new Date("InvalidDate")); // Left(" FormattedStringFromDate("yyyy_MM_dd").decode("asdf"); // Left("[error message]") ``` +⚠️ No `schema` support. + ### Number Codec Module #### NumberRangedIn @@ -86,6 +101,8 @@ NumberRangedIn({ gte: 2, lt: 5 }).decode(0); // Left("[error message]") NumberRangedIn({ gt: 2, lte: 5 }).decode("a"); // Left("[error message]") ``` +❤️ Support `schema`. + #### NumberFromString Convert string into number (if parsable). @@ -96,6 +113,8 @@ NumberFromString.decode("Infinity"); // Left("[error message]") NumberFromString.decode(1234); // Left("[error message]") ``` +❤️ Support `schema`. + #### Integer Ensure input number is integer. @@ -106,6 +125,8 @@ Integer.decode(12.34); // Left("[error message]") Integer.decode("1234"); // Left("[error message]") ``` +❤️ Support `schema`. + #### IntegerFromString Convert string into integer number (if possible). @@ -116,6 +137,8 @@ IntegerFromString.decode("12.34"); // Left("[error message]") IntegerFromString.decode(1234); // Left("[error message]") ``` +❤️ Support `schema`. + ### Date Codec Module #### DateFromAny @@ -128,6 +151,8 @@ DateFromAny.decode(1577804400000); // Right(Wed Jan 01 2020 00:00:00) DateFromAny.decode("today"); // Left("[error message]") ``` +⚠️ No `schema` support. + #### DateFromStringFormatOf Convert formatted date string into Date. @@ -138,6 +163,8 @@ DateFromStringFormatOf("yyyy_MM_dd").decode("2020"); // Left("[error message]") DateFromStringFormatOf("yyyy_MM_dd").decode(new Date()); // Left("[error message]") ``` +⚠️ No `schema` support. + ### Json Codec Module #### JsonFromString @@ -150,8 +177,24 @@ JsonFromString(Codec.interface({ type: string })).decode(`{}`); // Left("[error JsonFromString(Codec.interface({ type: string })).decode(1234); // Left("[error message]") ``` +⚠️ No `schema` support. + ### Codec Utility Module +#### withSchema + +Utility for adding a schema after defined. + +```typescript +withSchema( + MyCodec, + (myCodecSchema) => ({ + ...myCodecSchema, + pattern: "^[a-fA-F\\d]{8}$" + }) +); +``` + #### extendCodec Utility for type narrowing.