├── .gitattributes ├── lib ├── tag.d.ts ├── tag.js ├── index.d.ts ├── taghiro.d.ts ├── validator │ ├── strings.d.ts │ ├── strings.js │ ├── validator.d.ts │ ├── results.d.ts │ ├── validator.js │ └── results.js ├── index.js ├── taghiro.js ├── collections.js ├── collections.d.ts ├── strings.d.ts ├── strings.js ├── numeric.js └── numeric.d.ts ├── src ├── tag.ts ├── index.ts ├── collections.ts ├── strings.ts └── numeric.ts ├── taghiro.code-workspace ├── jest.config.js ├── tsconfig.json ├── examples ├── positive.ts ├── bug.ts ├── gmail.ts └── coupon.ts ├── .github └── workflows │ └── nodejs.yml ├── tslint.json ├── TODO.md ├── .eslintrc.js ├── wallaby.js ├── package.json ├── LICENSE ├── .gitignore ├── test ├── collections.spec.ts ├── strings.spec.ts └── numeric.spec.ts └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | 3 | *.png binary 4 | *.jpg binary 5 | -------------------------------------------------------------------------------- /lib/tag.d.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | readonly __tag: S; 3 | } 4 | -------------------------------------------------------------------------------- /src/tag.ts: -------------------------------------------------------------------------------- 1 | export interface Tag { 2 | readonly __tag: S; 3 | } 4 | -------------------------------------------------------------------------------- /taghiro.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ] 7 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'node', 4 | }; -------------------------------------------------------------------------------- /lib/tag.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from "./numeric"; 2 | export * from "./strings"; 3 | export * from "./collections"; 4 | export * from "./tag"; 5 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./numeric"; 2 | export * from "./strings"; 3 | export * from "./collections"; 4 | export * from "./tag"; 5 | -------------------------------------------------------------------------------- /lib/taghiro.d.ts: -------------------------------------------------------------------------------- 1 | export * from './numeric'; 2 | export * from './strings'; 3 | export * from './collections'; 4 | export * from './tag'; 5 | -------------------------------------------------------------------------------- /lib/validator/strings.d.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "./results"; 2 | import { NonEmpty } from ".."; 3 | import { ErrorMessage } from "./validator"; 4 | export { vNotEmpty }; 5 | declare function vNotEmpty(value: T): Result; 8 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "strict": true 8 | }, 9 | "include": ["src"], 10 | "exclude": ["node_modules", "**/__tests__/*"] 11 | } 12 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | __export(require("./numeric")); 7 | __export(require("./strings")); 8 | __export(require("./collections")); 9 | -------------------------------------------------------------------------------- /lib/validator/strings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const __1 = require(".."); 4 | const validator_1 = require("./validator"); 5 | function vNotEmpty(value) { 6 | return validator_1.validate(value, __1.isNotEmpty); 7 | } 8 | exports.vNotEmpty = vNotEmpty; 9 | -------------------------------------------------------------------------------- /lib/taghiro.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | function __export(m) { 3 | for (var p in m) if (!exports.hasOwnProperty(p)) exports[p] = m[p]; 4 | } 5 | Object.defineProperty(exports, "__esModule", { value: true }); 6 | __export(require("./numeric")); 7 | __export(require("./strings")); 8 | __export(require("./collections")); 9 | -------------------------------------------------------------------------------- /examples/positive.ts: -------------------------------------------------------------------------------- 1 | import { 2 | negative, 3 | positive, 4 | Positive, 5 | GreaterEqual, 6 | greaterEqualThan, 7 | } from "../src"; 8 | 9 | // You can add logs by using console.log 10 | console.log("We ♡ JavaScript!"); 11 | 12 | function x(a: number & Positive & GreaterEqual<10>): number { 13 | return a + 1; 14 | } 15 | 16 | function y(a: number & GreaterEqual<14>): number { 17 | return a + 1; 18 | } 19 | 20 | const v = 11; 21 | console.log(v); 22 | 23 | if (positive(v) && greaterEqualThan(v, 10)) { 24 | const z = x(v); 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Node CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: [ 10.x, 12.x] 13 | 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Use Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v1 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | - name: npm install, build, and test 21 | run: | 22 | npm install 23 | npm run build --if-present 24 | npm run test:ci 25 | env: 26 | CI: true 27 | -------------------------------------------------------------------------------- /examples/bug.ts: -------------------------------------------------------------------------------- 1 | class Min { 2 | private __min: T; 3 | } 4 | 5 | function min( 6 | value: U, 7 | minSize: T, 8 | ): value is U & Min { 9 | return value.length >= minSize; 10 | } 11 | 12 | function f1(s: string & Min<10>) { 13 | return s; 14 | } 15 | 16 | function f2(s: string & Min<10> & Min<5>) { 17 | return s; 18 | } 19 | 20 | const s = "1234567890"; 21 | 22 | if (min(s, 10)) { 23 | f1(s); 24 | } 25 | 26 | if (min(s, 11)) { 27 | // Does not compile, correct 28 | // f1(s); 29 | } 30 | 31 | if (min(s, 5) && min(s, 10)) { 32 | f2(s); 33 | } 34 | -------------------------------------------------------------------------------- /lib/validator/validator.d.ts: -------------------------------------------------------------------------------- 1 | import { Result, Failure } from "./results"; 2 | export { ErrorMessage, ValidationFailure, validate, withGood }; 3 | declare type ErrorMessage = string; 4 | declare class ValidationFailure extends Failure { 5 | } 6 | declare function validate(v: T, fn: (v: T) => v is U): Result; 7 | declare function withGood(fn: (v1?: T1, v2?: T2, v3?: T3, v4?: T4, v5?: T5, v6?: T6, v7?: T7, v8?: T8, v9?: T9) => A, v1?: Result, v2?: Result, v3?: Result, v4?: Result, v5?: Result, v6?: Result, v7?: Result, v8?: Result, v9?: Result): Result; 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "extends": ["tslint:latest","tslint-sonarts"], 4 | "rules": { 5 | "variable-name": { 6 | "options": [ 7 | "ban-keywords" 8 | ] 9 | }, 10 | "max-classes-per-file": false, 11 | "no-duplicate-string": false, 12 | "parameters-max-number": false, 13 | "semicolon": [true, "always", "strict-bound-class-methods"], 14 | "no-submodule-imports": [true, "typeorm/util/StringUtils"], 15 | "arrow-parens": [true, "ban-single-arg-parens"], 16 | "object-literal-sort-keys": false, 17 | "member-access": [true, "no-public"], 18 | "ordered-imports": false, 19 | "interface-name": [true, "never-prefix"], 20 | "no-implicit-dependencies": [true, "dev"] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | ## Possible integrations 2 | 3 | https://github.com/arhs/iban.js 4 | BIC? 5 | 6 | ## Now 7 | 8 | - Add SizeBetween 9 | 10 | ## Others 11 | 12 | - Write about sanitizers (toEmail = trim + lowercase etc.) 13 | - Write about force asEmail(string):string & Email | undefined {..} 14 | 15 | ## Ideas 16 | 17 | AccountNumber 18 | PublicUserId 19 | PackageName 20 | VenueId 21 | ExtensionType 22 | PlainText 23 | SafeHtml 24 | Html 25 | Country 26 | IBAN 27 | BIC 28 | BankName 29 | AccountHolder 30 | CompanyName 31 | PersonName 32 | VatId 33 | Netto 34 | Brutto 35 | SalesTax 36 | InvoiceNumber 37 | ImageId 38 | CouponCode 39 | 40 | string & RelativeURL 41 | string & AbsoluteURL 42 | 43 | Date & InvoiceDate 44 | Date & SEPAPaymentDate 45 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', // Specifies the ESLint parser 3 | extends: [ 4 | 'plugin:@typescript-eslint/recommended', // Uses the recommended rules from the @typescript-eslint/eslint-plugin 5 | 'prettier/@typescript-eslint', // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 6 | 'plugin:prettier/recommended', // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 7 | ], 8 | parserOptions: { 9 | ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features 10 | sourceType: 'module', // Allows for the use of imports 11 | }, 12 | rules: { 13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /wallaby.js: -------------------------------------------------------------------------------- 1 | module.exports = function(wallaby) { 2 | return { 3 | files: ["src/**/*.ts"], 4 | 5 | tests: ["test/**/*.domain-spec.ts", "test/**/*.api-spec.ts"], 6 | 7 | env: { 8 | type: "node", 9 | }, 10 | 11 | testFramework: "jest", 12 | 13 | setup: wallaby => { 14 | console.log("Setup"); 15 | console.log("Current worker id: " + wallaby.workerId); 16 | console.log("Current session id: " + wallaby.sessionId); 17 | // wallaby.delayStart(); 18 | // yourAsyncCleanUp(() => { 19 | // console.log("Setup"); 20 | // console.log("Current worker id: " + wallaby.workerId); 21 | // console.log("Current session id: " + wallaby.sessionId); 22 | // wallaby.start(); 23 | // }); 24 | }, 25 | 26 | teardown: function(wallaby) { 27 | console.log("Teardown"); 28 | console.log("Current worker id: " + wallaby.workerId); 29 | console.log("Current session id: " + wallaby.sessionId); 30 | }, 31 | }; 32 | }; 33 | -------------------------------------------------------------------------------- /lib/validator/results.d.ts: -------------------------------------------------------------------------------- 1 | export { Success, Failure, Many, Many as Failures, Result, isOneFailure, isSuccess, handle, }; 2 | declare function isSuccess(arg: Result): arg is Success; 3 | declare function isOneFailure(arg: Result): arg is Failure; 4 | declare function handle(v: Promise, f: () => Failure): Promise>; 5 | export declare function success(value: T): Result; 6 | declare class Success { 7 | readonly value: T; 8 | constructor(value: T); 9 | isSuccess(): boolean; 10 | hasMany(): boolean; 11 | } 12 | declare class Failure { 13 | readonly error: E; 14 | constructor(error: E); 15 | hasMany(): boolean; 16 | isSuccess(): boolean; 17 | toMany(): Many>; 18 | } 19 | declare class Many { 20 | readonly failures: F[]; 21 | static of(f: F): Many; 22 | constructor(failures: F[]); 23 | isSuccess(): boolean; 24 | hasMany(): boolean; 25 | toMany(): Many; 26 | } 27 | declare type Result = Success | Many> | Failure; 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taghiro", 3 | "version": "0.3.5", 4 | "description": "Tag types for Typescript", 5 | "main": "lib/index.js", 6 | "types": "lib/index.d.ts", 7 | "devDependencies": { 8 | "@types/jest": "^24.0.18", 9 | "faker": "^4.1.0", 10 | "jest": "^24.9.0", 11 | "npm-run-all": "^4.1.5", 12 | "ts-jest": "^24.1.0", 13 | "tslint": "5.20.0", 14 | "tslint-sonarts": "^1.9.0", 15 | "typescript": "^3.8.3" 16 | }, 17 | "scripts": { 18 | "build": "tsc", 19 | "test": "jest", 20 | "test:cov": "jest --coverage", 21 | "test:ci": "jest", 22 | "lint": "tslint -p tsconfig.json -c tslint.json", 23 | "pre-push": "npm-run-all build lint test" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/sveseme/taghiro.git" 28 | }, 29 | "keywords": [ 30 | "tag", 31 | "types" 32 | ], 33 | "author": "Stephan J. Schmidt", 34 | "license": "MIT", 35 | "bugs": { 36 | "url": "https://github.com/sveseme/taghiro/issues" 37 | }, 38 | "homepage": "https://github.com/sveseme/taghiro#readme" 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Stephan Schmidt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/collections.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function isSorted(a) { 4 | for (let i = 0; i < a.length - 1; i++) { 5 | if (a[i] > a[i + 1]) { 6 | return false; 7 | } 8 | } 9 | return true; 10 | } 11 | exports.isSorted = isSorted; 12 | function isUnsorted(a) { 13 | for (let i = 0; i < a.length - 1; i++) { 14 | if (a[i] > a[i + 1]) { 15 | return true; 16 | } 17 | } 18 | return false; 19 | } 20 | exports.isUnsorted = isUnsorted; 21 | function hasSize(value, size) { 22 | return value.length === size; 23 | } 24 | exports.hasSize = hasSize; 25 | function hasMinSize(value, minSize) { 26 | if (minSize < 0) { 27 | return false; 28 | } 29 | return value.length >= minSize; 30 | } 31 | exports.hasMinSize = hasMinSize; 32 | function hasMaxSize(value, maxSize) { 33 | if (maxSize < 0) { 34 | return false; 35 | } 36 | return value.length <= maxSize; 37 | } 38 | exports.hasMaxSize = hasMaxSize; 39 | function isEmpty(value) { 40 | return hasSize(value, 0); 41 | } 42 | exports.isEmpty = isEmpty; 43 | function isNotEmpty(value) { 44 | return !isEmpty(value); 45 | } 46 | exports.isNotEmpty = isNotEmpty; 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | .DS_Store 64 | 65 | typings/ 66 | *.map 67 | -------------------------------------------------------------------------------- /lib/collections.d.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "./tag"; 2 | export declare type Sorted = Tag<"sorted">; 3 | export declare type Unsorted = Tag<"unsorted">; 4 | export declare function isSorted(a: T[]): a is T[] & Sorted; 5 | export declare function isUnsorted(a: T[]): a is T[] & Unsorted; 6 | export declare class Size { 7 | private __size; 8 | } 9 | export declare function hasSize(value: U, size: T): value is U & Size; 12 | export declare class MinSize { 13 | private __minSize; 14 | } 15 | export declare function hasMinSize(value: U, minSize: T): value is U & MinSize; 18 | export declare class MaxSize { 19 | private __minSize; 20 | } 21 | export declare function hasMaxSize(value: U, maxSize: T): value is U & Size; 24 | export declare type Empty = Tag<"empty">; 25 | export declare function isEmpty(value: T): value is T & Empty; 28 | export declare type NonEmpty = Tag<"non-empty">; 29 | export declare function isNotEmpty(value: T): value is T & NonEmpty; 32 | -------------------------------------------------------------------------------- /examples/gmail.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EndsWith, 3 | endingWith, 4 | Tag, 5 | Result, 6 | ErrorMessage, 7 | validate, 8 | withGood, 9 | hasMaxSize, 10 | isSuccess, 11 | } from "../src/"; 12 | import { MinSize, hasMinSize } from "../src/"; 13 | 14 | function gmailDelete(email: string & EndsWith<"@gmail.com"> & MinSize<15>) {} 15 | 16 | const email = " stephan.schmidt@Gmail.com "; 17 | // gmailDelete(email); // would not compile 18 | if ( 19 | endingWith(email, "@gmail.com") && 20 | hasMinSize(email, 5) && 21 | hasMinSize(email, 16) 22 | ) { 23 | gmailDelete(email); 24 | } else { 25 | // 26 | } 27 | 28 | export type Gmail = Tag<"gmail">; 29 | 30 | export function isGmail( 31 | value: string, 32 | ): value is string & Gmail { 33 | const s = email.trim().toLowerCase(); 34 | return endingWith(s, "@gmail.com") && hasMinSize(s, 5) && hasMaxSize(s, 200); 35 | } 36 | 37 | function vGmail(value: string): Result { 38 | return validate(value.toLowerCase().trim(), isGmail); 39 | } 40 | 41 | class Person { 42 | gmail: string & Gmail; 43 | constructor(gmail: string & Gmail) { 44 | this.gmail = gmail; 45 | } 46 | } 47 | 48 | const p = withGood((g: string & Gmail) => new Person(g), vGmail(email)); 49 | if (isSuccess(p)) { 50 | // Do something 51 | } 52 | 53 | console.log(p); 54 | -------------------------------------------------------------------------------- /lib/validator/validator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const results_1 = require("./results"); 4 | class ValidationFailure extends results_1.Failure { 5 | } 6 | exports.ValidationFailure = ValidationFailure; 7 | function validate(v, fn) { 8 | if (fn(v)) { 9 | return new results_1.Success(v); 10 | } 11 | else { 12 | return new ValidationFailure("Could not validated"); 13 | } 14 | } 15 | exports.validate = validate; 16 | function vDefined(value) { 17 | if (value !== undefined) { 18 | return new results_1.Success(value); 19 | } 20 | else { 21 | return new ValidationFailure("Could not validated"); 22 | } 23 | } 24 | // Replace with currying 25 | // https://medium.com/@hernanrajchert/creating-typings-for-curry-using-ts-3-x-956da2780bbf 26 | function withGood(fn, v1, v2, v3, v4, v5, v6, v7, v8, v9) { 27 | function value(r) { 28 | if (r === undefined) { 29 | return undefined; 30 | } 31 | else if (results_1.isSuccess(r)) { 32 | return r.value; 33 | } 34 | else { 35 | return undefined; 36 | } 37 | } 38 | const args = [v1, v2, v3, v4, v5, v6, v7, v8, v9].filter(x => { 39 | return x !== undefined; 40 | }); 41 | const allSuccess = (args.length = args.filter(x => x !== undefined && x.isSuccess).length); 42 | if (allSuccess) { 43 | return new results_1.Success(fn(value(v1), value(v2), value(v3), value(v4), value(v5), value(v6), value(v7), value(v8), value(v9))); 44 | } 45 | else { 46 | return new results_1.Many([v1, v2]); 47 | } 48 | } 49 | exports.withGood = withGood; 50 | -------------------------------------------------------------------------------- /test/collections.spec.ts: -------------------------------------------------------------------------------- 1 | import { isSorted, isUnsorted, hasMinSize, hasMaxSize } from "../src"; 2 | 3 | test("isSorted returns true for [1,2,3]", () => { 4 | expect(isSorted([1, 2, 3])).toBe(true); 5 | }); 6 | 7 | test("isSorted returns false for [3,1,2]", () => { 8 | expect(isSorted([3, 1, 2])).toBe(false); 9 | }); 10 | 11 | test("isUnsorted returns false for [1,2,3]", () => { 12 | expect(isUnsorted([1, 2, 3])).toBe(false); 13 | }); 14 | 15 | test("isUnsorted returns false for [3,1,2]", () => { 16 | expect(isUnsorted([3, 1, 2])).toBe(true); 17 | }); 18 | 19 | test("hasMinSize returns true for [3,1,2] and minimum size 2", () => { 20 | expect(hasMinSize([3, 1, 2], 2)).toBe(true); 21 | }); 22 | 23 | test("hasMinSize returns true for [3,1,2] and minimum size 4", () => { 24 | expect(hasMinSize([3, 1, 2], 4)).toBe(false); 25 | }); 26 | 27 | test("hasMinSize returns true for [] and minimum size 4", () => { 28 | expect(hasMinSize([], 2)).toBe(false); 29 | }); 30 | 31 | test("hasMinSize returns true for [] and minimum size 0", () => { 32 | expect(hasMinSize([], 0)).toBe(true); 33 | }); 34 | 35 | test("hasMinSize returns false for [1] and minimum size -2", () => { 36 | expect(hasMinSize([1], -2)).toBe(false); 37 | }); 38 | 39 | test("hasMaxSize returns false for [1,2] and maximum size -2", () => { 40 | expect(hasMaxSize([1, 2], -2)).toBe(false); 41 | }); 42 | 43 | test("hasMaxSize returns false for [7,1,2] and maximum size 2", () => { 44 | expect(hasMaxSize([7, 1, 2], 2)).toBe(false); 45 | }); 46 | 47 | test("hasMaxSize returns true for [7,1,2] and maximum size 3", () => { 48 | expect(hasMaxSize([7, 1, 2], 3)).toBe(true); 49 | }); 50 | 51 | test("hasMaxSize returns true for [7,1,2] and maximum size 2", () => { 52 | expect(hasMaxSize([7, 1, 2], 2)).toBe(false); 53 | }); 54 | 55 | test("hasMaxSize returns true for [7,1,2] and maximum siz4", () => { 56 | expect(hasMaxSize([7, 1, 2], 4)).toBe(true); 57 | }); 58 | -------------------------------------------------------------------------------- /lib/strings.d.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "./tag"; 2 | export declare class Regex { 3 | private __regex; 4 | } 5 | export declare function isRegex(value: string, regex: T): value is string & Regex; 6 | export declare type Accii = Tag<"ascii">; 7 | export declare function isAscii(value: string): value is string & Uuid; 8 | export declare type Digits = Tag<"digits">; 9 | export declare function isDigits(value: string): value is string & Digits; 10 | export declare type Letters = Tag<"letters">; 11 | export declare function isLetters(value: string): value is string & Letters; 12 | export declare type LettersOrDigits = Tag<"letters-or-digits">; 13 | export declare function isLetterOrDigits(value: string): value is string & LettersOrDigits; 14 | export declare type Trimmed = Tag<"trimmed">; 15 | export declare function isTrimmed(value: string): value is string & Trimmed; 16 | export declare type LowerCase = Tag<"lowercase">; 17 | export declare function isLowerCase(value: string): value is string & LowerCase; 18 | export declare type UpperCase = Tag<"uppercase">; 19 | export declare function isUpperCase(value: string): value is string & LowerCase; 20 | export declare class EndsWith { 21 | private __endsWith; 22 | } 23 | export declare function endingWith(value: string, endsWith: T): value is string & EndsWith; 24 | export declare class StartsWith { 25 | private __startsWith; 26 | } 27 | export declare function startingWith(value: string, startsWith: T): value is string & StartsWith; 28 | export declare type Url = Tag<"url">; 29 | export declare function isUrl(value: string): value is string & LowerCase; 30 | export declare type Uuid = Tag<"uuid">; 31 | export declare function isUuid(value: string): value is string & Uuid; 32 | export declare type Json = Tag<"json">; 33 | export declare function isJson(value: string): value is string & Json; 34 | export declare type Base64 = Tag<"base64">; 35 | export declare function isBase64(value: string): value is string & Base64; 36 | -------------------------------------------------------------------------------- /src/collections.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "./tag"; 2 | 3 | // Size 4 | // MinSize 5 | // MaxSize 6 | // Empty 7 | // NonEmpty 8 | // Sorted 9 | // Unsorted 10 | 11 | export type Sorted = Tag<"sorted">; 12 | 13 | export type Unsorted = Tag<"unsorted">; 14 | 15 | export function isSorted(a: T[]): a is T[] & Sorted { 16 | for (let i = 0; i < a.length - 1; i++) { 17 | if (a[i] > a[i + 1]) { 18 | return false; 19 | } 20 | } 21 | return true; 22 | } 23 | 24 | export function isUnsorted(a: T[]): a is T[] & Unsorted { 25 | for (let i = 0; i < a.length - 1; i++) { 26 | if (a[i] > a[i + 1]) { 27 | return true; 28 | } 29 | } 30 | return false; 31 | } 32 | 33 | export declare class Size { 34 | private __size: T; 35 | } 36 | 37 | export function hasSize( 38 | value: U, 39 | size: T, 40 | ): value is U & Size { 41 | return value.length === size; 42 | } 43 | 44 | export declare class MinSize { 45 | private __minSize: T; 46 | } 47 | 48 | export function hasMinSize( 49 | value: U, 50 | minSize: T, 51 | ): value is U & MinSize { 52 | if (minSize < 0) { 53 | return false; 54 | } 55 | return value.length >= minSize; 56 | } 57 | 58 | export declare class MaxSize { 59 | private __maxSize: T; 60 | } 61 | 62 | export function hasMaxSize( 63 | value: U, 64 | maxSize: T, 65 | ): value is U & MaxSize { 66 | if (maxSize < 0) { 67 | return false; 68 | } 69 | return value.length <= maxSize; 70 | } 71 | 72 | export type Empty = Tag<"empty">; 73 | 74 | export function isEmpty( 75 | value: T, 76 | ): value is T & Empty { 77 | return hasSize(value, 0); 78 | } 79 | 80 | export type NonEmpty = Tag<"non-empty">; 81 | 82 | export function isNotEmpty( 83 | value: T, 84 | ): value is T & NonEmpty { 85 | return !isEmpty(value); 86 | } 87 | -------------------------------------------------------------------------------- /lib/strings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | const numeric_1 = require("./numeric"); 4 | function isRegex(value, regex) { 5 | const regexp = new RegExp(regex); 6 | return regexp.test(value); 7 | } 8 | exports.isRegex = isRegex; 9 | function isAscii(value) { 10 | const regexp = new RegExp("^[\x00-\x7F]+$"); 11 | return regexp.test(value); 12 | } 13 | exports.isAscii = isAscii; 14 | function isDigits(value) { 15 | return isRegex(value, "^\\d*$"); 16 | } 17 | exports.isDigits = isDigits; 18 | function isLetters(value) { 19 | return isRegex(value, "^[a-zA-Z]+$"); 20 | } 21 | exports.isLetters = isLetters; 22 | function isLetterOrDigits(value) { 23 | return isRegex(value, "^[\\da-zA-Z]+$"); 24 | } 25 | exports.isLetterOrDigits = isLetterOrDigits; 26 | function isTrimmed(value) { 27 | return value.trim() === value; 28 | } 29 | exports.isTrimmed = isTrimmed; 30 | function isLowerCase(value) { 31 | return value.toLowerCase() === value; 32 | } 33 | exports.isLowerCase = isLowerCase; 34 | function isUpperCase(value) { 35 | return value.toUpperCase() === value; 36 | } 37 | exports.isUpperCase = isUpperCase; 38 | function endingWith(value, endsWith) { 39 | return value.endsWith(endsWith); 40 | } 41 | exports.endingWith = endingWith; 42 | function startingWith(value, startsWith) { 43 | return value.startsWith(startsWith); 44 | } 45 | exports.startingWith = startingWith; 46 | function isUrl(value) { 47 | try { 48 | const url = new URL(value); 49 | return true; 50 | } 51 | catch (_) { 52 | return false; 53 | } 54 | } 55 | exports.isUrl = isUrl; 56 | function isUuid(value) { 57 | const regexp = new RegExp("^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", "i"); 58 | return regexp.test(value); 59 | } 60 | exports.isUuid = isUuid; 61 | function isJson(value) { 62 | try { 63 | const result = JSON.parse(value); 64 | return !!result && typeof result === "object"; 65 | } 66 | catch (e) { 67 | return false; 68 | } 69 | } 70 | exports.isJson = isJson; 71 | function isBase64(value) { 72 | return (numeric_1.isDivisible(0, 4) && 73 | isRegex(value, "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$")); 74 | } 75 | exports.isBase64 = isBase64; 76 | -------------------------------------------------------------------------------- /lib/numeric.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | Object.defineProperty(exports, "__esModule", { value: true }); 3 | function isInOpenInterval(value, minValue, maxValue) { 4 | return value > minValue && value < maxValue; 5 | } 6 | exports.isInOpenInterval = isInOpenInterval; 7 | function isInClosedInterval(value, minValue, maxValue) { 8 | return value >= minValue && value <= maxValue; 9 | } 10 | exports.isInClosedInterval = isInClosedInterval; 11 | function isInRange(value, minValue, maxValue) { 12 | return isInClosedInterval(value, minValue, maxValue); 13 | } 14 | exports.isInRange = isInRange; 15 | function isInOpenClosedInterval(value, minValue, maxValue) { 16 | return value > minValue && value <= maxValue; 17 | } 18 | exports.isInOpenClosedInterval = isInOpenClosedInterval; 19 | function isInClosedOpenInterval(value, minValue, maxValue) { 20 | return value >= minValue && value < maxValue; 21 | } 22 | exports.isInClosedOpenInterval = isInClosedOpenInterval; 23 | function greaterEqualThan(value, minValue) { 24 | return value >= minValue; 25 | } 26 | exports.greaterEqualThan = greaterEqualThan; 27 | function greaterThan(value, minValue) { 28 | return value > minValue; 29 | } 30 | exports.greaterThan = greaterThan; 31 | function lessEqualThan(value, maxValue) { 32 | return value <= maxValue; 33 | } 34 | exports.lessEqualThan = lessEqualThan; 35 | function lessThan(value, maxValue) { 36 | return value < maxValue; 37 | } 38 | exports.lessThan = lessThan; 39 | function positive(value) { 40 | return value > 0; 41 | } 42 | exports.positive = positive; 43 | function negative(value) { 44 | return value < 0; 45 | } 46 | exports.negative = negative; 47 | function nonPositive(value) { 48 | return !positive(value); 49 | } 50 | exports.nonPositive = nonPositive; 51 | function nonNegative(value) { 52 | return !negative(value); 53 | } 54 | exports.nonNegative = nonNegative; 55 | function isModulo(value, m1, m2) { 56 | return value % m1 === m2; 57 | } 58 | exports.isModulo = isModulo; 59 | function isDivisible(value, d) { 60 | return isModulo(value, d, 0); 61 | } 62 | exports.isDivisible = isDivisible; 63 | function isNotDivisible(value, d) { 64 | return !isDivisible(value, d); 65 | } 66 | exports.isNotDivisible = isNotDivisible; 67 | function isOdd(value) { 68 | return isNotDivisible(value, 2); 69 | } 70 | exports.isOdd = isOdd; 71 | function isEven(value) { 72 | return isDivisible(value, 2); 73 | } 74 | exports.isEven = isEven; 75 | function isNotZero(value) { 76 | return value !== 0; 77 | } 78 | exports.isNotZero = isNotZero; 79 | -------------------------------------------------------------------------------- /lib/validator/results.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 3 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 4 | return new (P || (P = Promise))(function (resolve, reject) { 5 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 6 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 7 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 8 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 9 | }); 10 | }; 11 | Object.defineProperty(exports, "__esModule", { value: true }); 12 | function isSuccess(arg) { 13 | return arg.isSuccess(); 14 | } 15 | exports.isSuccess = isSuccess; 16 | function isOneFailure(arg) { 17 | return !arg.isSuccess() && arg.hasMany() === false; 18 | } 19 | exports.isOneFailure = isOneFailure; 20 | function handle(v, f) { 21 | return __awaiter(this, void 0, void 0, function* () { 22 | const value = yield v; 23 | if (value) { 24 | return new Success(value); 25 | } 26 | else { 27 | return f(); 28 | } 29 | }); 30 | } 31 | exports.handle = handle; 32 | function success(value) { 33 | return new Success(value); 34 | } 35 | exports.success = success; 36 | class Success { 37 | constructor(value) { 38 | this.value = value; 39 | } 40 | isSuccess() { 41 | return true; 42 | } 43 | hasMany() { 44 | return false; 45 | } 46 | } 47 | exports.Success = Success; 48 | class Failure { 49 | constructor(error) { 50 | this.error = error; 51 | } 52 | hasMany() { 53 | return false; 54 | } 55 | isSuccess() { 56 | return false; 57 | } 58 | toMany() { 59 | return new Many([this]); 60 | } 61 | } 62 | exports.Failure = Failure; 63 | class Many { 64 | constructor(failures) { 65 | this.failures = failures; 66 | } 67 | static of(f) { 68 | return new Many([f]); 69 | } 70 | isSuccess() { 71 | return false; 72 | } 73 | hasMany() { 74 | return true; 75 | } 76 | toMany() { 77 | return this; 78 | } 79 | } 80 | exports.Many = Many; 81 | exports.Failures = Many; 82 | // Mix fail fast and accumulate 83 | // a 84 | // .then((a) => R(b)) 85 | // .then((b) => { 86 | // c = x(b); 87 | // d = y(b); 88 | // withGood(c,d, (c,d) => Account(c,b)) 89 | // }); 90 | -------------------------------------------------------------------------------- /test/strings.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | isAscii, 3 | isDigits, 4 | isLetters, 5 | endingWith, 6 | startingWith, 7 | isLetterOrDigits, 8 | isTrimmed, 9 | isUpperCase, 10 | isLowerCase, 11 | isUrl, 12 | isJson, 13 | isBase64, 14 | } from "../src"; 15 | 16 | test("isDigits returns true for strings of digits", () => { 17 | const v = "123"; 18 | expect(isDigits(v)).toBe(true); 19 | }); 20 | 21 | test("isDigits returns false for strings with a letter", () => { 22 | const v = "123a"; 23 | expect(isDigits(v)).toBe(false); 24 | }); 25 | 26 | test("isLetters returns true for strings of letters", () => { 27 | const v = "abc"; 28 | expect(isLetters(v)).toBe(true); 29 | }); 30 | 31 | test("isLetters returns false for strings with a number", () => { 32 | const v = "abc1"; 33 | expect(isLetters(v)).toBe(false); 34 | }); 35 | 36 | test("isLetterOrDigits returns true for strings of letters and digits", () => { 37 | expect(isLetterOrDigits("abc123")).toBe(true); 38 | }); 39 | 40 | test("isLetterOrDigits returns false for strings with other character", () => { 41 | expect(isLetterOrDigits("[abc]")).toBe(false); 42 | }); 43 | 44 | test("isTrimmed returns true for trimmed strings", () => { 45 | expect(isTrimmed("abc 123")).toBe(true); 46 | }); 47 | 48 | test("isTrimmed returns false for untrimmed strings", () => { 49 | expect(isTrimmed(" abc 123 ")).toBe(false); 50 | expect(isTrimmed("abc 123 ")).toBe(false); 51 | expect(isTrimmed(" abc 123")).toBe(false); 52 | }); 53 | 54 | test("isLowerCase returns true for lowercase strings", () => { 55 | expect(isLowerCase("abc")).toBe(true); 56 | }); 57 | 58 | test("isLowerCase returns false for mixed case strings", () => { 59 | expect(isLowerCase("abC")).toBe(false); 60 | }); 61 | 62 | test("isUpperCase returns true for uppercase strings", () => { 63 | expect(isUpperCase("ABC")).toBe(true); 64 | }); 65 | 66 | test("isUpperCase returns false for mixed case strings", () => { 67 | expect(isUpperCase("ABc")).toBe(false); 68 | }); 69 | 70 | test('endingWith returns true for "abc" ending with "bc"', () => { 71 | expect(endingWith("abc", "bc")).toBe(true); 72 | }); 73 | 74 | test('endingWith returns false for "abc" ending with "bcd"', () => { 75 | expect(endingWith("abc", "bcd")).toBe(false); 76 | }); 77 | 78 | test('startingWith returns true for "abc" starting with "ab"', () => { 79 | expect(startingWith("abc", "ab")).toBe(true); 80 | }); 81 | 82 | test('startingWith returns false for "abc" starting with "abx"', () => { 83 | expect(startingWith("abc", "abx")).toBe(false); 84 | }); 85 | 86 | test("isAscii returns true for ascii", () => { 87 | expect(isAscii("abc123")).toBe(true); 88 | }); 89 | 90 | test("isAscii returns false for other characters", () => { 91 | expect(isAscii("äbc")).toBe(false); 92 | }); 93 | 94 | test("isUrl returns true for urls", () => { 95 | expect(isUrl("http://www.github.com")).toBe(true); 96 | }); 97 | 98 | test("isUrl returns false for incorrect urls", () => { 99 | expect(isUrl("abc")).toBe(false); 100 | }); 101 | 102 | test('isJson returns true for {"a":"b"}', () => { 103 | expect(isJson('{"a":"b"}')).toBe(true); 104 | }); 105 | 106 | test('isJson returns false for "abc" ', () => { 107 | expect(isJson("abc")).toBe(false); 108 | }); 109 | -------------------------------------------------------------------------------- /lib/numeric.d.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "./tag"; 2 | export declare class IntervalOpen { 3 | private __minValue; 4 | private __maxValue; 5 | } 6 | export declare function isInOpenInterval(value: number, minValue: T, maxValue: U): value is number & IntervalOpen; 7 | export declare class IntervalClosed { 8 | private __minValue; 9 | private __maxValue; 10 | } 11 | export declare function isInClosedInterval(value: number, minValue: T, maxValue: U): value is number & IntervalClosed; 12 | declare type Range = IntervalClosed; 13 | export declare function isInRange(value: number, minValue: T, maxValue: U): value is number & Range; 14 | export declare class IntervalOpenClosed { 15 | private __minValue; 16 | private __maxValue; 17 | } 18 | export declare function isInOpenClosedInterval(value: number, minValue: T, maxValue: U): value is number & IntervalOpenClosed; 19 | export declare class IntervalClosedOpen { 20 | private __minValue; 21 | private __maxValue; 22 | } 23 | export declare function isInClosedOpenInterval(value: number, minValue: T, maxValue: U): value is number & IntervalClosedOpen; 24 | export declare class GreaterEqual { 25 | private __minValue; 26 | } 27 | export declare function greaterEqualThan(value: number, minValue: T): value is number & GreaterEqual; 28 | export declare class Greater { 29 | private __minValue; 30 | } 31 | export declare function greaterThan(value: number, minValue: T): value is number & Greater; 32 | export declare class LessEqual { 33 | private __maxValue; 34 | } 35 | export declare function lessEqualThan(value: number, maxValue: T): value is number & LessEqual; 36 | export declare class Less { 37 | private __maxValue; 38 | } 39 | export declare function lessThan(value: number, maxValue: T): value is number & LessEqual; 40 | export declare type Positive = Tag<"positive">; 41 | export declare function positive(value: number): value is number & Greater<0> & Positive; 42 | declare type Negative = Tag<"negative">; 43 | export declare function negative(value: number): value is number & Less<0> & Negative; 44 | declare type NonPositive = Tag<"non-positive">; 45 | export declare function nonPositive(value: number): value is number & NonPositive & LessEqual<0>; 46 | declare type NonNegative = Tag<"non-negative">; 47 | export declare function nonNegative(value: number): value is number & NonNegative & GreaterEqual<0>; 48 | export declare class Modulo { 49 | private __m1; 50 | private __m2; 51 | } 52 | export declare function isModulo(value: number, m1: T, m2: U): value is number & Modulo; 53 | export declare class Divisible { 54 | private __divisor; 55 | } 56 | export declare function isDivisible(value: number, d: T): value is number & Divisible; 57 | export declare class NotDivisible { 58 | private __divisor; 59 | } 60 | export declare function isNotDivisible(value: number, d: T): value is number & NotDivisible; 61 | declare type Odd = Tag<"odd">; 62 | export declare function isOdd(value: number): value is number & Odd; 63 | export declare function isEven(value: number): value is number & Odd; 64 | declare type NotZero = Tag<"not-zero">; 65 | export declare function isNotZero(value: number): value is number & NotZero; 66 | export {}; 67 | -------------------------------------------------------------------------------- /src/strings.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "./tag"; 2 | import { isDivisible } from "./numeric"; 3 | 4 | // Regex 5 | // Digits 6 | // Letters 7 | // LettersOrDigits 8 | // LowerCase 9 | // UpperCase 10 | // EndsWith 11 | // StartsWith 12 | // Trimmed 13 | // Url 14 | // Uuid 15 | 16 | export declare class Regex { 17 | private __regex: T; 18 | } 19 | 20 | export function isRegex( 21 | value: string, 22 | regex: T, 23 | ): value is string & Regex { 24 | const regexp = new RegExp(regex); 25 | return regexp.test(value); 26 | } 27 | 28 | export type Accii = Tag<"ascii">; 29 | 30 | export function isAscii( 31 | value: string, 32 | ): value is string & Uuid { 33 | const regexp = new RegExp("^[\x00-\x7F]+$"); 34 | return regexp.test(value); 35 | } 36 | 37 | export type Digits = Tag<"digits">; 38 | 39 | export function isDigits(value: string): value is string & Digits { 40 | return isRegex(value, "^\\d*$"); 41 | } 42 | 43 | export type Letters = Tag<"letters">; 44 | 45 | export function isLetters(value: string): value is string & Letters { 46 | return isRegex(value, "^[a-zA-Z]+$"); 47 | } 48 | 49 | export type LettersOrDigits = Tag<"letters-or-digits">; 50 | 51 | export function isLetterOrDigits( 52 | value: string, 53 | ): value is string & LettersOrDigits { 54 | return isRegex(value, "^[\\da-zA-Z]+$"); 55 | } 56 | 57 | export type Trimmed = Tag<"trimmed">; 58 | 59 | export function isTrimmed( 60 | value: string, 61 | ): value is string & Trimmed { 62 | return value.trim() === value; 63 | } 64 | 65 | export type LowerCase = Tag<"lowercase">; 66 | 67 | export function isLowerCase( 68 | value: string, 69 | ): value is string & LowerCase { 70 | return value.toLowerCase() === value; 71 | } 72 | 73 | export type UpperCase = Tag<"uppercase">; 74 | 75 | export function isUpperCase( 76 | value: string, 77 | ): value is string & LowerCase { 78 | return value.toUpperCase() === value; 79 | } 80 | 81 | export declare class EndsWith { 82 | private __endsWith: T; 83 | } 84 | 85 | export function endingWith( 86 | value: string, 87 | endsWith: T, 88 | ): value is string & EndsWith { 89 | return value.endsWith(endsWith); 90 | } 91 | 92 | export declare class StartsWith { 93 | private __startsWith: T; 94 | } 95 | 96 | export function startingWith( 97 | value: string, 98 | startsWith: T, 99 | ): value is string & StartsWith { 100 | return value.startsWith(startsWith); 101 | } 102 | 103 | export type Url = Tag<"url">; 104 | 105 | export function isUrl(value: string): value is string & LowerCase { 106 | try { 107 | const url = new URL(value); 108 | return true; 109 | } catch (_) { 110 | return false; 111 | } 112 | } 113 | 114 | export type Uuid = Tag<"uuid">; 115 | 116 | export function isUuid(value: string): value is string & Uuid { 117 | const regexp = new RegExp( 118 | "^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", 119 | "i", 120 | ); 121 | return regexp.test(value); 122 | } 123 | 124 | export type Json = Tag<"json">; 125 | 126 | export function isJson(value: string): value is string & Json { 127 | try { 128 | const result = JSON.parse(value); 129 | return !!result && typeof result === "object"; 130 | } catch (e) { 131 | return false; 132 | } 133 | } 134 | 135 | export type Base64 = Tag<"base64">; 136 | 137 | export function isBase64(value: string): value is string & Base64 { 138 | return ( 139 | isDivisible(0, 4) && 140 | isRegex( 141 | value, 142 | "^([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}==)?$", 143 | ) 144 | ); 145 | } 146 | -------------------------------------------------------------------------------- /examples/coupon.ts: -------------------------------------------------------------------------------- 1 | import { Tag, LessEqual, lessEqualThan } from '../src'; 2 | 3 | // ------- 4 | // Example: Validation inside function 5 | 6 | // percentage 7 | // Integer not Float 8 | // 0 - 100 9 | // max 80 for coupon 10 | function handle1(param: string) { 11 | const n = Number.parseInt(param); 12 | if (Number.isNaN(n)) { 13 | // return error 14 | } else { 15 | var product: string; 16 | try { 17 | product = buy1(n); 18 | } catch (e) { 19 | // Mixed 20 | // Input validation error 21 | // Business error 22 | } 23 | if (product) { 24 | decreaseInventory1(product); 25 | } 26 | } 27 | } 28 | 29 | function buy1(percentage: number): string { 30 | // Validation handling 31 | if (!Number.isInteger(percentage) || percentage < 0 || percentage > 100) { 32 | throw 'Not an valid percentege ' + percentage; 33 | } 34 | 35 | if (percentage > 80) { 36 | throw 'Rebate above 80%'; 37 | } 38 | 39 | // do our logic 40 | return 'ABC123'; 41 | } 42 | 43 | function decreaseInventory1(product: string) { 44 | if (product.length !== 6) { 45 | // error 46 | } 47 | // do our logic 48 | } 49 | 50 | // ------- 51 | // Example: Validation inside function 52 | 53 | // percentage 54 | // Integer not Float 55 | // 0 - 100 56 | // max 80 for coupon 57 | function handle2(param: number) { 58 | const result = buy2(param); 59 | 60 | if (result.error) { 61 | // Mixed 62 | // Input validation error 63 | // Business error 64 | } else { 65 | decreaseInventory2(result.product); 66 | // ok with 67 | } 68 | } 69 | 70 | function buy2(percentage: number): { product?: string; error?: string } { 71 | // Validation handling 72 | if (!Number.isInteger(percentage) || percentage < 0 || percentage > 100) { 73 | return { error: 'Not an valid percentage ' + percentage }; 74 | } 75 | 76 | if (percentage > 80) { 77 | return { error: 'Rebate above 80%: ' + percentage }; 78 | } 79 | 80 | // do our logic 81 | return { product: 'ABC123' }; 82 | } 83 | 84 | function decreaseInventory2(product: string) { 85 | if (product.length !== 6) { 86 | // error 87 | } 88 | // do our logic 89 | } 90 | 91 | // ------- 92 | // Example: Duplicated Validation 93 | 94 | // percentage 95 | // Integer not Float 96 | // 0 - 100 97 | // max 80 for coupon 98 | function handle3(param: number) { 99 | if (!Number.isInteger(param) || param < 0 || param > 100) { 100 | // Input validation error 101 | } 102 | 103 | const result = buy2(param); 104 | 105 | if (result.error) { 106 | // Mixed 107 | // Input validation error 108 | // Business error 109 | } else { 110 | // ok 111 | decreaseInventory3(result.product); 112 | } 113 | } 114 | 115 | function buy3(percentage: number): { product?: string; error?: string } { 116 | // Validation handling 117 | if (!Number.isInteger(percentage) || percentage < 0 || percentage > 100) { 118 | return { error: 'Not an valid percentage ' + percentage }; 119 | } 120 | 121 | if (percentage > 80) { 122 | return { error: 'Rebate above 80%: ' + percentage }; 123 | } 124 | 125 | // do our logic 126 | return { product: 'ABC123' }; 127 | } 128 | 129 | function decreaseInventory3(product: string) { 130 | if (product.length !== 6) { 131 | // error 132 | } 133 | // do our logic 134 | } 135 | 136 | // ------- 137 | // Example: Validation with function 138 | 139 | export type Percentage = Tag<'percentage'>; 140 | export type ProductId = Tag<'product-id'>; 141 | 142 | // Resusable validation logic 143 | export function isPercentage(value: number): value is number & Percentage { 144 | return value >= 0 && value <= 100 && Number.isInteger(value); 145 | } 146 | 147 | // percentage 148 | // Integer not Float 149 | // 0 - 100 150 | // max 80 for coupon 151 | function handle4(param: number) { 152 | if (isPercentage(param) && lessEqualThan(param, 80)) { 153 | const product = buy4(param); 154 | // Business error handling 155 | decreaseInventory4(product); 156 | } else { 157 | // Validation error 158 | } 159 | } 160 | 161 | function buy4(percentage: number & Percentage & LessEqual<80>): string & ProductId { 162 | // do our logic 163 | // we know this is a product id 164 | return 'ABC123' as string & ProductId; 165 | } 166 | 167 | function decreaseInventory4(product: string & ProductId) { 168 | // do our logic 169 | } 170 | -------------------------------------------------------------------------------- /src/numeric.ts: -------------------------------------------------------------------------------- 1 | import { Tag } from "./tag"; 2 | 3 | // Range 4 | // IntervalOpen 5 | // IntervalOpenClosed 6 | // IntervalClosedOpen 7 | // IntervalClosed 8 | // Less 9 | // LessEqual 10 | // Greater 11 | // GreaterEqual 12 | // Positive 13 | // NonPositive 14 | // Negative 15 | // NonNegative 16 | // Modulo 17 | // Divisable 18 | // NonDivisable 19 | // Even 20 | // Odd 21 | // NotZero 22 | 23 | export declare class IntervalOpen { 24 | private __minValue: T; 25 | private __maxValue: T; 26 | } 27 | 28 | export function isInOpenInterval( 29 | value: number, 30 | minValue: T, 31 | maxValue: U, 32 | ): value is number & IntervalOpen { 33 | return value > minValue && value < maxValue; 34 | } 35 | 36 | export declare class IntervalClosed { 37 | private __minValue: T; 38 | private __maxValue: T; 39 | } 40 | 41 | export function isInClosedInterval( 42 | value: number, 43 | minValue: T, 44 | maxValue: U, 45 | ): value is number & IntervalClosed { 46 | return value >= minValue && value <= maxValue; 47 | } 48 | 49 | type Range = IntervalClosed; 50 | 51 | export function isInRange( 52 | value: number, 53 | minValue: T, 54 | maxValue: U, 55 | ): value is number & Range { 56 | return isInClosedInterval(value, minValue, maxValue); 57 | } 58 | 59 | export declare class IntervalOpenClosed { 60 | private __minValue: T; 61 | private __maxValue: T; 62 | } 63 | 64 | export function isInOpenClosedInterval( 65 | value: number, 66 | minValue: T, 67 | maxValue: U, 68 | ): value is number & IntervalOpenClosed { 69 | return value > minValue && value <= maxValue; 70 | } 71 | 72 | export declare class IntervalClosedOpen { 73 | private __minValue: T; 74 | private __maxValue: T; 75 | } 76 | 77 | export function isInClosedOpenInterval( 78 | value: number, 79 | minValue: T, 80 | maxValue: U, 81 | ): value is number & IntervalClosedOpen { 82 | return value >= minValue && value < maxValue; 83 | } 84 | 85 | export declare class GreaterEqual { 86 | private __minValue: T; 87 | } 88 | 89 | export function greaterEqualThan( 90 | value: number, 91 | minValue: T, 92 | ): value is number & GreaterEqual { 93 | return value >= minValue; 94 | } 95 | 96 | export declare class Greater { 97 | private __minValue: T; 98 | } 99 | 100 | export function greaterThan( 101 | value: number, 102 | minValue: T, 103 | ): value is number & Greater { 104 | return value > minValue; 105 | } 106 | 107 | export declare class LessEqual { 108 | private __maxValue: T; 109 | } 110 | 111 | export function lessEqualThan( 112 | value: number, 113 | maxValue: T, 114 | ): value is number & LessEqual { 115 | return value <= maxValue; 116 | } 117 | 118 | export declare class Less { 119 | private __maxValue: T; 120 | } 121 | 122 | export function lessThan( 123 | value: number, 124 | maxValue: T, 125 | ): value is number & LessEqual { 126 | return value < maxValue; 127 | } 128 | 129 | // values > 0 130 | export type Positive = Tag<"positive">; 131 | 132 | export function positive( 133 | value: number, 134 | ): value is number & Greater<0> & Positive { 135 | return value > 0; 136 | } 137 | 138 | // values < 0 139 | type Negative = Tag<"negative">; 140 | 141 | export function negative( 142 | value: number, 143 | ): value is number & Less<0> & Negative { 144 | return value < 0; 145 | } 146 | 147 | // values <= 0 148 | type NonPositive = Tag<"non-positive">; 149 | 150 | export function nonPositive( 151 | value: number, 152 | ): value is number & NonPositive & LessEqual<0> { 153 | return !positive(value); 154 | } 155 | 156 | // values >=0 157 | type NonNegative = Tag<"non-negative">; 158 | 159 | export function nonNegative( 160 | value: number, 161 | ): value is number & NonNegative & GreaterEqual<0> { 162 | return !negative(value); 163 | } 164 | 165 | export declare class Modulo { 166 | private __m1: T; 167 | private __m2: U; 168 | } 169 | 170 | export function isModulo( 171 | value: number, 172 | m1: T, 173 | m2: U, 174 | ): value is number & Modulo { 175 | return value % m1 === m2; 176 | } 177 | 178 | export declare class Divisible { 179 | private __divisor: T; 180 | } 181 | 182 | export function isDivisible( 183 | value: number, 184 | d: T, 185 | ): value is number & Divisible { 186 | return isModulo(value, d, 0); 187 | } 188 | 189 | export declare class NotDivisible { 190 | private __divisor: T; 191 | } 192 | 193 | export function isNotDivisible( 194 | value: number, 195 | d: T, 196 | ): value is number & NotDivisible { 197 | return !isDivisible(value, d); 198 | } 199 | 200 | type Odd = Tag<"odd">; 201 | 202 | export function isOdd(value: number): value is number & Odd { 203 | return isNotDivisible(value, 2); 204 | } 205 | 206 | type Even = Tag<"even">; 207 | 208 | export function isEven(value: number): value is number & Odd { 209 | return isDivisible(value, 2); 210 | } 211 | 212 | type NotZero = Tag<"not-zero">; 213 | 214 | export function isNotZero(value: number): value is number & NotZero { 215 | return value !== 0; 216 | } 217 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # taghiro 2 | 3 | **Tag types for Typescript** 4 | 5 | taghiro is a library for tag types in [Typescript](https://www.typescriptlang.org/). Our mission is to prevent bugs and make code more readable to developers. Tag types are types with which you can tag other types. 6 | 7 | taghiro implements them as intersection types in Typescript. For example `NotZero` is a tag type preventing `b` to be `0`. 8 | 9 | ```typescript 10 | function divide(a:number, b:number & NotZero) { 11 | .... 12 | } 13 | ``` 14 | 15 | For an introduction on tag types see [here](http://codemonkeyism.com/never-never-never-use-string-in-java-7-years-later/). 16 | 17 | ## Development 18 | 19 | Development happens at https://twitch.tv/kingofcoders 20 | 21 | ## Install 22 | 23 | ```bash 24 | npm install taghiro 25 | ``` 26 | 27 | ## Run tests 28 | 29 | ```bash 30 | npm test 31 | ``` 32 | 33 | ## Usage 34 | 35 | Lets look at an example where we send emails 36 | 37 | ```typescript 38 | function sendEmails(to:Array, html:string) { 39 | .... 40 | } 41 | ``` 42 | 43 | Here several things could go wrong. First the `to` array could be empty. Second the `html` could be empty, not contain any HTML or contain unsafe HTML. With tag types we can make sure the paramaters are save. 44 | 45 | ```typescript 46 | import { NonEmpty, isNotEmpty } from 'taghiro'; 47 | 48 | function sendEmails(to:Array & NonEmpty, html:string & NonEmpty) { 49 | .... 50 | } 51 | ``` 52 | 53 | Now the caller needs to ensure that the paramater satisfy the tag types. taghiro implements this with Typescript type guards. 54 | 55 | ```typescript 56 | import { NonEmpty, isNotEmpty } from "taghiro"; 57 | 58 | if (isNotEmpty(emails) && isNotEmpty(html)) { 59 | sendEmails(emails, html); 60 | } 61 | ``` 62 | 63 | You can write your own Tags. With leveraging libraries to checking emails and HTML we can easily implement `Email` and `SafeHtml`. With those we can make our method even safer. 64 | 65 | ```typescript 66 | function sendEmails( 67 | to:Array & NonEmpty, 68 | html:string & SafeHtml 69 | ) { 70 | .... 71 | } 72 | ``` 73 | 74 | For an extension with an `Email` tag see [taghiro-validator](https://github.com/sveseme/taghiro-validator). For implementing `SafeHtml` we could use [sanitize-html](https://www.npmjs.com/package/sanitize-html). 75 | 76 | ## Different sorts of tag types 77 | 78 | There are many different use cases for tag types. At least three use case groups for tag types are 79 | 80 | - Constraint (like MinSize, Positive) 81 | - State (like Empty, LoggedIn) 82 | - Semantic (like Netto, CustomerID) 83 | 84 | ## Supplied tag types 85 | 86 | taghiro brings ready to use tag types. The supplied tag types are inspired by the excellent [refined](https://github.com/fthomas/refined) library. 87 | 88 | ### Containers 89 | 90 | - Size 91 | - MinSize 92 | - MaxSize 93 | - Empty 94 | - NonEmpty 95 | - Sorted 96 | - Unsorted 97 | 98 | ### Numeric 99 | 100 | - IntervalOpen 101 | - IntervalOpenClosed 102 | - IntervalClosedOpen 103 | - IntervalClosed 104 | - Less 105 | - LessEqual 106 | - Greater 107 | - GreaterEqual 108 | - Positive 109 | - NonPositive 110 | - Negative 111 | - NonNegative 112 | - Modulo 113 | - Divisable 114 | - NonDivisable 115 | - Even 116 | - Odd 117 | - NotZero 118 | 119 | ### Strings 120 | 121 | - Ascii 122 | - Regex 123 | - Digits 124 | - Letters 125 | - LettersOrDigits 126 | - LowerCase 127 | - UpperCase 128 | - Trimmed 129 | - EndsWith 130 | - StartsWith 131 | - Url 132 | - Uuid 133 | - Json 134 | - Base64 135 | 136 | ## Custom tag types 137 | 138 | Tag types can be used to define custom domain concepts. One example is 139 | id. Here is an example based on string Uuid ids. 140 | 141 | ```typescript 142 | import { Tag, isUuid } from "taghiro"; 143 | 144 | export type CustomerId = Tag<"customer-id">; 145 | 146 | export function isCustomerId(value: string): value is string & CustomerId { 147 | return isUuid(value); 148 | } 149 | ``` 150 | 151 | One can define a custom Tag type to define more than one id tag. 152 | 153 | ```typescript 154 | import { isUuid } from "taghiro"; 155 | 156 | export interface Id { 157 | readonly __id: T; 158 | } 159 | 160 | export type CustomerId = Id<"customer">; 161 | 162 | export function isCustomerId(value: string): value is string & CustomerId { 163 | return isUuid(value); 164 | } 165 | 166 | export type AccountId = Id<"account">; 167 | 168 | export function isAccountId(value: string): value is string & AccountId { 169 | return isUuid(value); 170 | } 171 | ``` 172 | 173 | ## More tag types 174 | 175 | taghiro is easy to integrate with more validation libraries for example [Validator](https://www.npmjs.com/package/validator). 176 | 177 | ```typescript 178 | import { isEmail } from "validator"; 179 | import { Tag } from "taghiro"; 180 | 181 | export type Email = Tag<"email">; 182 | 183 | export function isEmail(value: string): value is string & Email { 184 | return isEmail(value); 185 | } 186 | ``` 187 | 188 | For a library that implements taghiro tag types with [Validator](https://www.npmjs.com/package/validator) see [taghiro-validator](https://github.com/sveseme/taghiro-validator). 189 | 190 | ## Tag types and bug prevention 191 | 192 | The introduction of generics prevented ClassCastException, replacing null with Option types prevents NullPointerexceptions and the introduction of tag types prevent IllegalArgumentExceptions. 193 | 194 | 195 | 196 | 197 | 200 | 203 | 204 | 205 | 208 | 218 | 232 | 233 | 234 | 237 | 250 | 260 | 261 |
198 | Method validation 199 | 201 | Tag types 202 |
206 | Call site 207 | 209 |
210 | sendEmail(
211 |   'stephan.schmidt@gmail.com', 
212 |   'Important'
213 | )
214 | // 1. user/validation error handling
215 | // 2. IO mail error handling
216 | 
217 |
219 |
220 | const email = 'stephan.schmidt@gmail.com';
221 | const body = 'Important';
222 | 
223 | if (isEmail(email) && isSafeHtml(body)) {
224 | sendEmail(email, body);
225 | // IO mail error handling
226 | } else {
227 | // show user error
228 | }
229 | 
230 | 
231 |
235 | Method site 236 | 238 |
239 | function sendEmail(to: string, body: string) {
240 |   if (isEmail(to), isHtml(body)) {
241 |     // send email
242 |   } else {
243 |     // what to do here?
244 |     // throw Exception?
245 |     // return error code?
246 |   }
247 | }
248 | 
249 |
251 |
252 | function sendEmail(
253 |   to: string & Email, 
254 |   body: string & SafeHtml
255 |   ) {
256 |   // send email
257 | }
258 | 
259 |
262 | 263 | ## License (MIT) 264 | 265 | ``` 266 | Copyright (c) 2019 Stephan Schmidt 267 | 268 | Permission is hereby granted, free of charge, to any person obtaining 269 | a copy of this software and associated documentation files (the 270 | "Software"), to deal in the Software without restriction, including 271 | without limitation the rights to use, copy, modify, merge, publish, 272 | distribute, sublicense, and/or sell copies of the Software, and to 273 | permit persons to whom the Software is furnished to do so, subject to 274 | the following conditions: 275 | 276 | The above copyright notice and this permission notice shall be 277 | included in all copies or substantial portions of the Software. 278 | 279 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 280 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 281 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 282 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 283 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 284 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 285 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 286 | ``` 287 | -------------------------------------------------------------------------------- /test/numeric.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | lessEqualThan, 3 | nonPositive, 4 | nonNegative, 5 | isModulo, 6 | isDivisible, 7 | isNotDivisible, 8 | isOdd, 9 | isEven, 10 | isNotZero, 11 | positive, 12 | negative, 13 | greaterEqualThan, 14 | LessEqual, 15 | greaterThan, 16 | lessThan, 17 | isInOpenInterval, 18 | isInClosedInterval, 19 | isInRange, 20 | isInOpenClosedInterval, 21 | isInClosedOpenInterval, 22 | } from "../src"; 23 | 24 | test("isInOpenInterval return true for a value inside the interval", () => { 25 | expect(isInOpenInterval(6, 2, 7)).toBe(true); 26 | }); 27 | 28 | test("isInOpenInterval return false for a value outside the interval", () => { 29 | expect(isInOpenInterval(9, 2, 7)).toBe(false); 30 | }); 31 | 32 | test("isInOpenInterval return false for a value at the ends of the interval", () => { 33 | expect(isInOpenInterval(2, 2, 7)).toBe(false); 34 | }); 35 | 36 | test("isInOpenInterval return false for a value at the ends of the interval", () => { 37 | expect(isInOpenInterval(7, 2, 7)).toBe(false); 38 | }); 39 | 40 | test("isInOpenInterval return false for a value inside the interval with a lower negative bound", () => { 41 | expect(isInOpenInterval(0, -2, 7)).toBe(true); 42 | }); 43 | 44 | test("isInClosedInterval return true for a value inside the interval", () => { 45 | expect(isInClosedInterval(6, 2, 7)).toBe(true); 46 | }); 47 | 48 | test("isInClosedInterval return true for a value at the lower end of the interval", () => { 49 | expect(isInClosedInterval(2, 2, 7)).toBe(true); 50 | }); 51 | 52 | test("isInClosedInterval return true for a value at the upper end of the interval", () => { 53 | expect(isInClosedInterval(7, 2, 7)).toBe(true); 54 | }); 55 | 56 | test("isInClosedInterval return false for a value inside the interval with a lower negative bound", () => { 57 | expect(isInClosedInterval(0, -2, 7)).toBe(true); 58 | }); 59 | 60 | test("isInRange return true for a value inside the interval", () => { 61 | expect(isInRange(6, 2, 7)).toBe(true); 62 | }); 63 | 64 | test("isInRange return true for a value at the lower end of the interval", () => { 65 | expect(isInRange(2, 2, 7)).toBe(true); 66 | }); 67 | 68 | test("isInRange return true for a value at the upper end of the interval", () => { 69 | expect(isInRange(7, 2, 7)).toBe(true); 70 | }); 71 | 72 | test("isInRange return false for a value inside the interval with a lower negative bound", () => { 73 | expect(isInRange(0, -2, 7)).toBe(true); 74 | }); 75 | 76 | test("isInOpenClosedInterval returns true for a value in the interval", () => { 77 | expect(isInOpenClosedInterval(5, 1, 9)).toBe(true); 78 | }); 79 | 80 | test("isInOpenClosedInterval returns false for a value outside the interval", () => { 81 | expect(isInOpenClosedInterval(14, 1, 9)).toBe(false); 82 | }); 83 | 84 | test("isInOpenClosedInterval returns false for a value at lower end of the interval", () => { 85 | expect(isInOpenClosedInterval(1, 1, 9)).toBe(false); 86 | }); 87 | 88 | test("isInOpenClosedInterval returns true for a value at upper end of the interval", () => { 89 | expect(isInOpenClosedInterval(9, 1, 9)).toBe(true); 90 | }); 91 | 92 | test("isInClosedOpenInteral returns true for value inside the interval", () => { 93 | expect(isInClosedOpenInterval(5, 4, 100)).toBe(true); 94 | }); 95 | 96 | test("isInClosedOpenInteral returns false for value outside the interval", () => { 97 | expect(isInClosedOpenInterval(1000, 4, 100)).toBe(false); 98 | }); 99 | 100 | test("isInClosedOpenInteral returns true for value at lower end of the interval", () => { 101 | expect(isInClosedOpenInterval(4, 4, 100)).toBe(true); 102 | }); 103 | 104 | test("isInClosedOpenInteral returns false for value at upper end of the interval", () => { 105 | expect(isInClosedOpenInterval(100, 4, 100)).toBe(false); 106 | }); 107 | 108 | test("lessEqualThan returns true for equal values", () => { 109 | const v = 3; 110 | expect(lessEqualThan(v, 3)).toBe(true); 111 | }); 112 | 113 | test("lessEqualThan returns true for less values", () => { 114 | const v = 2; 115 | expect(lessEqualThan(v, 3)).toBe(true); 116 | }); 117 | 118 | test("lessEqualThan returns false if greater value", () => { 119 | const v = 5; 120 | expect(lessEqualThan(v, 3)).toBe(false); 121 | }); 122 | 123 | test("lessThan returns false for equal values", () => { 124 | expect(lessThan(3, 3)).toBe(false); 125 | }); 126 | 127 | test("lessThan returns false for greater value", () => { 128 | expect(lessThan(4, 3)).toBe(false); 129 | }); 130 | 131 | test("lessThan returns true for smaller value", () => { 132 | expect(lessThan(2, 7)).toBe(true); 133 | }); 134 | 135 | test("lessThan returns true for smaller value and negative numbers", () => { 136 | expect(lessThan(-7, -2)).toBe(true); 137 | }); 138 | 139 | test("greaterThan return false for equal values", () => { 140 | expect(greaterThan(4, 4)).toBe(false); 141 | }); 142 | 143 | test("greaterThan return false for smaller value", () => { 144 | expect(greaterThan(4, 5)).toBe(false); 145 | }); 146 | 147 | test("greaterThan return true for larger value", () => { 148 | expect(greaterThan(5, 4)).toBe(true); 149 | }); 150 | 151 | test("greaterEqualThan return true for equal values", () => { 152 | expect(greaterEqualThan(4, 4)).toBe(true); 153 | }); 154 | 155 | test("greaterEqualThan return false for less value", () => { 156 | expect(greaterEqualThan(2, 3)).toBe(false); 157 | }); 158 | 159 | test("greaterEqualThan return true for equal value", () => { 160 | expect(greaterEqualThan(3, 3)).toBe(true); 161 | }); 162 | //---- 163 | 164 | test("positive returns true for 11", () => { 165 | expect(positive(11)).toBe(true); 166 | }); 167 | 168 | test("positive returns true for -11", () => { 169 | expect(positive(-11)).toBe(false); 170 | }); 171 | 172 | test("positive returns true for 0", () => { 173 | expect(positive(0)).toBe(false); 174 | }); 175 | 176 | test("negative returns true for -13", () => { 177 | expect(negative(-13)).toBe(true); 178 | }); 179 | 180 | test("negative returns false for 2", () => { 181 | expect(negative(2)).toBe(false); 182 | }); 183 | 184 | test("negative returns false for 0", () => { 185 | expect(negative(0)).toBe(false); 186 | }); 187 | 188 | test("nonPositive returns true for -2", () => { 189 | const v = -2; 190 | expect(nonPositive(v)).toBe(true); 191 | }); 192 | 193 | test("nonPositive returns true for 0", () => { 194 | const v = 0; 195 | expect(nonPositive(v)).toBe(true); 196 | }); 197 | 198 | test("nonPositive returns true for 5", () => { 199 | const v = 5; 200 | expect(nonPositive(v)).toBe(false); 201 | }); 202 | 203 | test("nonNegative returns false for -2", () => { 204 | const v = -2; 205 | expect(nonNegative(v)).toBe(false); 206 | }); 207 | 208 | test("nonNegative returns true for 0", () => { 209 | const v = 0; 210 | expect(nonNegative(v)).toBe(true); 211 | }); 212 | 213 | test("nonNegative returns true for 5", () => { 214 | const v = 5; 215 | expect(nonNegative(v)).toBe(true); 216 | }); 217 | 218 | test("isModulo returns true for 4 mod 3 = 1", () => { 219 | expect(isModulo(4, 3, 1)).toBe(true); 220 | }); 221 | 222 | test("isModulo returns true for 4 mod 3 = 2", () => { 223 | expect(isModulo(4, 3, 2)).toBe(false); 224 | }); 225 | 226 | test("isModulo returns true for 4 mod 4 = 0", () => { 227 | expect(isModulo(4, 4, 0)).toBe(true); 228 | }); 229 | 230 | test("isDivisible returns true for 9 and 0", () => { 231 | expect(isDivisible(9, 0)).toBe(false); 232 | }); 233 | 234 | test("isDivisible returns true for 9 and 3", () => { 235 | expect(isDivisible(9, 3)).toBe(true); 236 | }); 237 | 238 | test("isDivisible returns false for 9 and 2", () => { 239 | expect(isDivisible(9, 2)).toBe(false); 240 | }); 241 | 242 | test("isNotDivisible returns true for 9 and 2", () => { 243 | expect(isNotDivisible(9, 2)).toBe(true); 244 | }); 245 | 246 | test("isNotDivisible returns true for 9 and 3", () => { 247 | expect(isNotDivisible(9, 3)).toBe(false); 248 | }); 249 | 250 | test("isNotDivisible returns true for 9 and 0", () => { 251 | expect(isNotDivisible(9, 0)).toBe(true); 252 | }); 253 | 254 | test("isOdd returns true for 5", () => { 255 | expect(isOdd(5)).toBe(true); 256 | }); 257 | 258 | test("isOdd returns true for -5", () => { 259 | expect(isOdd(-5)).toBe(true); 260 | }); 261 | 262 | test("isOdd returns false for -4", () => { 263 | expect(isOdd(-4)).toBe(false); 264 | }); 265 | 266 | test("isOdd returns false for 4", () => { 267 | expect(isOdd(4)).toBe(false); 268 | }); 269 | 270 | test("isOdd returns false for 4", () => { 271 | expect(isOdd(4)).toBe(false); 272 | }); 273 | 274 | test("isOdd returns false for 0", () => { 275 | expect(isOdd(0)).toBe(false); 276 | }); 277 | 278 | test("isEven returns true for 4", () => { 279 | expect(isEven(4)).toBe(true); 280 | }); 281 | 282 | test("isEven returns false for 7", () => { 283 | expect(isEven(7)).toBe(false); 284 | }); 285 | 286 | test("isEven returns true for 0", () => { 287 | expect(isEven(0)).toBe(true); 288 | }); 289 | 290 | test("isEven returns true for -2", () => { 291 | expect(isEven(-2)).toBe(true); 292 | }); 293 | 294 | test("isEven returns false for -9", () => { 295 | expect(isEven(-9)).toBe(false); 296 | }); 297 | 298 | test("isNotZero returns true for 1", () => { 299 | expect(isNotZero(1)).toBe(true); 300 | }); 301 | 302 | test("isNotZero returns false for 0", () => { 303 | expect(isNotZero(0)).toBe(false); 304 | }); 305 | --------------------------------------------------------------------------------