From 8717d63b8c13c042df71c1543896f397508ce1c2 Mon Sep 17 00:00:00 2001 From: airtoxin Date: Sun, 7 Mar 2021 05:29:19 +0900 Subject: [PATCH] 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"