├── .circleci └── config.yml ├── .editorconfig ├── .gitignore ├── .prettierrc ├── LICENCE.md ├── README.md ├── package.json ├── src ├── ComplexExample.ts ├── DisjointUnionExample.ts ├── DisjointUnionExample.validator.ts ├── Example.ts ├── __tests__ │ ├── build-parameters.test.ts │ ├── build-parameters │ │ ├── src │ │ │ ├── Example.ts │ │ │ └── index.ts │ │ ├── tsconfig.commonjs-interop.json │ │ ├── tsconfig.commonjs.json │ │ ├── tsconfig.es2015-interop.json │ │ ├── tsconfig.es2015.json │ │ ├── tsconfig.esnext-interop.json │ │ ├── tsconfig.esnext.json │ │ ├── tsconfig.system-interop.json │ │ ├── tsconfig.system.json │ │ ├── tsconfig.umd-interop.json │ │ └── tsconfig.umd.json │ ├── disjointUnion.test.ts │ ├── index.test.ts │ ├── output │ │ ├── ComplexExample.usage.ts │ │ └── ComplexExample.validator.ts │ ├── parse.test.ts │ └── printValidator.test.ts ├── cli.ts ├── index.ts ├── loadTsConfig.ts ├── normalizeSchema.ts ├── parse.ts ├── parseArgs.ts ├── prettierFile.ts ├── printValidator.ts ├── template.ts └── usage.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | refs: 4 | container: &container 5 | docker: 6 | - image: node:10 7 | working_directory: ~/repo 8 | steps: 9 | - &Versions 10 | run: 11 | name: Versions 12 | command: node -v && npm -v && yarn -v 13 | - &Install 14 | run: 15 | name: Install Dependencies 16 | command: yarn install --pure-lockfile 17 | - &Build 18 | run: 19 | name: Build 20 | command: yarn build 21 | - &Test 22 | run: 23 | name: Test 24 | command: yarn ci:test 25 | 26 | jobs: 27 | test: 28 | <<: *container 29 | steps: 30 | - checkout 31 | - *Versions 32 | - *Install 33 | - *Test 34 | - *Build 35 | 36 | publish: 37 | <<: *container 38 | steps: 39 | - checkout 40 | - *Versions 41 | - *Install 42 | - *Test 43 | - *Build 44 | - run: 45 | name: NPM Auth 46 | command: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc 47 | - run: 48 | name: Release 49 | command: | 50 | npx semantic-release && \ 51 | npx cross-ci :run \ 52 | npx commit-status success Version "'\${PROJECT_VERSION}'" 53 | workflows: 54 | version: 2 55 | all: 56 | jobs: 57 | - test: 58 | filters: 59 | branches: 60 | ignore: 61 | - master 62 | - gh-pages 63 | master: 64 | jobs: 65 | - publish: 66 | context: common-env 67 | filters: 68 | branches: 69 | only: master 70 | monthly: 71 | triggers: 72 | - schedule: 73 | cron: '0 0 1 * *' 74 | filters: 75 | branches: 76 | only: master 77 | jobs: 78 | - test: 79 | context: common-env 80 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Coverage directory used by tools like istanbul 11 | coverage 12 | 13 | # Compiled binary addons (http://nodejs.org/api/addons.html) 14 | build/Release 15 | 16 | # Dependency directory 17 | node_modules 18 | 19 | # Users Environment Variables 20 | .lock-wscript 21 | 22 | # Babel build output 23 | lib 24 | 25 | # Config files 26 | environment.toml 27 | .env 28 | 29 | src/__tests__/**/*.validator.ts -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "bracketSpacing": false, 3 | "singleQuote": true, 4 | "trailingComma": "all" 5 | } -------------------------------------------------------------------------------- /LICENCE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) 2019 [Forbes Lindesay](https://github.com/ForbesLindesay) 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 13 | > all 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TypeScript JSON Validator 2 | 3 | Automatically generate a validator using JSON Schema and AJV for any TypeScript type. 4 | 5 | ## Usage 6 | 7 | Define a type in `src/Example.ts`, e.g.: 8 | 9 | ```ts 10 | export default interface ExampleType { 11 | value: string; 12 | /** 13 | * @TJS-format email 14 | */ 15 | email?: string; 16 | /** 17 | * @default 42 18 | */ 19 | answer: number; 20 | } 21 | ``` 22 | 23 | To generate a validator, run: 24 | 25 | ``` 26 | npx typescript-json-validator src/Example.ts ExampleType 27 | ``` 28 | 29 | This will generate `src/Example.validator.ts`, which you can use: 30 | 31 | ```ts 32 | import {readFileSync} from 'fs'; 33 | import validate from './Example.validator.ts'; 34 | 35 | const value: unknown = JSON.parse(readFileSync(process.argv[2], 'utf8')); 36 | 37 | // this will through a clear error if `value` is not of the 38 | // correct type. It will also fill in any default values 39 | const validatedValue = validate(value); 40 | 41 | console.log(validatedValue.value); 42 | ``` 43 | 44 | Note that types will be validated automatically, but you can also use annotations to add extra runtime checks, such as e-mail formatting. For annotations see: https://github.com/YousefED/typescript-json-schema#annotations 45 | 46 | ## CLI Docs 47 | 48 | 49 | 50 | ``` 51 | Usage: typescript-json-schema 52 | 53 | Options: 54 | --help Show help [boolean] 55 | --version Show version number [boolean] 56 | --refs Create shared ref definitions. [boolean] [default: true] 57 | --aliasRefs Create shared ref definitions for the type aliases. 58 | [boolean] [default: false] 59 | --topRef Create a top-level ref definition. 60 | [boolean] [default: false] 61 | --titles Creates titles in the output schema. 62 | [boolean] [default: false] 63 | --defaultProps Create default properties definitions. 64 | [boolean] [default: true] 65 | --noExtraProps Disable additional properties in objects by default. 66 | [boolean] [default: false] 67 | --propOrder Create property order definitions. 68 | [boolean] [default: false] 69 | --typeOfKeyword Use typeOf keyword (https://goo.gl/DC6sni) for 70 | functions. [boolean] [default: false] 71 | --required Create required array for non-optional properties. 72 | [boolean] [default: true] 73 | --strictNullChecks Make values non-nullable by default. 74 | [boolean] [default: true] 75 | --ignoreErrors Generate even if the program has errors. 76 | [boolean] [default: false] 77 | --validationKeywords Provide additional validation keywords to include. 78 | [array] [default: []] 79 | --excludePrivate Exclude private members from the schema. 80 | [boolean] [default: false] 81 | --uniqueNames Use unique names for type symbols. 82 | [boolean] [default: false] 83 | --include Further limit tsconfig to include only matching files. 84 | [array] 85 | --rejectDateType Rejects Date fields in type definitions. 86 | [boolean] [default: false] 87 | --id ID of schema. [string] [default: ""] 88 | --uniqueItems Validate `uniqueItems` keyword [boolean] [default: true] 89 | --unicode calculate correct length of strings with unicode pairs 90 | (true by default). Pass false to use .length of strings 91 | that is faster, but gives "incorrect" lengths of strings 92 | with unicode pairs - each unicode pair is counted as two 93 | characters. [boolean] [default: true] 94 | --nullable support keyword "nullable" from Open API 3 95 | specification. [boolean] [default: true] 96 | --format formats validation mode ('fast' by default). Pass 'full' 97 | for more correct and slow validation or false not to 98 | validate formats at all. E.g., 25:00:00 and 2015/14/33 99 | will be invalid time and date in 'full' mode but it will 100 | be valid in 'fast' mode. 101 | [choices: "fast", "full"] [default: "fast"] 102 | --coerceTypes Change data type of data to match type keyword. e.g. 103 | parse numbers in strings [boolean] [default: false] 104 | --collection Process the file as a collection of types, instead of 105 | one single type. [boolean] [default: false] 106 | --useNamedExport Type name is a named export, rather than the default 107 | export of the file [boolean] [default: false] 108 | -* [default: []] 109 | 110 | ``` 111 | 112 | 113 | 114 | ## License 115 | 116 | MIT 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typescript-json-validator", 3 | "main": "lib/index.js", 4 | "types": "lib/index.d.ts", 5 | "bin": { 6 | "typescript-json-validator": "./lib/cli.js" 7 | }, 8 | "files": [ 9 | "lib/" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/ForbesLindesay/typescript-json-validator.git" 14 | }, 15 | "scripts": { 16 | "test": "jest --no-cache", 17 | "test:coverage": "yarn test --coverage", 18 | "test:watch": "yarn test --watch", 19 | "posttest": "tsc --noEmit", 20 | "clean": "rimraf lib && rimraf src/Example.validator.ts", 21 | "prebuild": "yarn clean", 22 | "build": "tsc", 23 | "build:watch": "yarn build -w", 24 | "postbuild": "node lib/usage && node lib/cli src/Example.ts ExampleType && node lib/cli src/DisjointUnionExample.ts --collection && rimraf lib/__tests__", 25 | "precommit": "pretty-quick --staged", 26 | "prepush": "yarn prettier:diff && yarn test", 27 | "prettier": "prettier --ignore-path .gitignore --write './**/*.{js,jsx,ts,tsx}'", 28 | "prettier:diff": "prettier --ignore-path .gitignore --list-different './**/*.{js,jsx,ts,tsx}'", 29 | "ci:test": "yarn prettier:diff && yarn test" 30 | }, 31 | "devDependencies": { 32 | "@types/jest": "^23.1.5", 33 | "@types/koa": "^2.0.48", 34 | "@types/koa-router": "^7.0.39", 35 | "@types/node": "^10.5.2", 36 | "@types/rimraf": "^2.0.2", 37 | "husky": "^0.14.3", 38 | "jest": "^23.3.0", 39 | "prettier": "^1.13.7", 40 | "pretty-quick": "^1.6.0", 41 | "rimraf": "^2.6.2", 42 | "ts-jest": "^23.0.0", 43 | "ts-node": "^8.3.0", 44 | "tslint": "^5.10.0", 45 | "typescript": "^3.4.5" 46 | }, 47 | "jest": { 48 | "watchPathIgnorePatterns": [ 49 | ".*/lib/.*", 50 | ".*/src/Example.validator\\.ts", 51 | ".*/src/__tests__/output/.*" 52 | ], 53 | "moduleFileExtensions": [ 54 | "ts", 55 | "js" 56 | ], 57 | "testEnvironment": "node", 58 | "transform": { 59 | "^.+\\.ts$": "ts-jest" 60 | }, 61 | "transformIgnorePatterns": [], 62 | "testRegex": ".*/__tests__/.*\\.(test|spec)\\.ts$" 63 | }, 64 | "dependencies": { 65 | "@types/ajv": "^1.0.0", 66 | "@types/cross-spawn": "^6.0.0", 67 | "@types/glob": "^7.1.1", 68 | "@types/json-stable-stringify": "^1.0.32", 69 | "@types/minimatch": "^3.0.3", 70 | "cross-spawn": "^6.0.5", 71 | "glob": "^7.1.3", 72 | "json-stable-stringify": "^1.0.1", 73 | "minimatch": "^3.0.4", 74 | "tsconfig-loader": "^1.1.0", 75 | "typescript-json-schema": "^0.38.3", 76 | "yargs": "^13.2.4" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/ComplexExample.ts: -------------------------------------------------------------------------------- 1 | export enum MyEnum { 2 | ValueA, 3 | ValueB, 4 | ValueC, 5 | } 6 | 7 | export interface TypeA { 8 | id: number; 9 | value: string; 10 | } 11 | 12 | export interface TypeB { 13 | id: number | null; 14 | /** 15 | * @format date-time 16 | */ 17 | value: string | null; 18 | } 19 | 20 | export interface RequestA { 21 | query: TypeA; 22 | body: TypeB; 23 | params: {e: MyEnum}; 24 | } 25 | 26 | export interface RequestB { 27 | query: TypeA; 28 | } 29 | -------------------------------------------------------------------------------- /src/DisjointUnionExample.ts: -------------------------------------------------------------------------------- 1 | export enum EntityTypes { 2 | TypeOne = 'TypeOne', 3 | TypeTwo = 'TypeTwo', 4 | TypeThree = 'TypeThree', 5 | } 6 | export interface EntityOne { 7 | type: EntityTypes.TypeOne; 8 | foo: string; 9 | } 10 | export interface EntityTwo { 11 | type: EntityTypes.TypeTwo; 12 | bar: string; 13 | } 14 | export type Entity = 15 | | EntityOne 16 | | EntityTwo 17 | | {type: EntityTypes.TypeThree; baz: number}; 18 | 19 | export type Value = 20 | | {number: 0; foo: string} 21 | | {number: 1; bar: string} 22 | | {number: 2; baz: string}; 23 | -------------------------------------------------------------------------------- /src/DisjointUnionExample.validator.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | // generated by typescript-json-validator 3 | import Ajv = require('ajv'); 4 | import { 5 | EntityTypes, 6 | EntityOne, 7 | EntityTwo, 8 | Entity, 9 | Value, 10 | } from './DisjointUnionExample'; 11 | export const ajv = new Ajv({ 12 | allErrors: true, 13 | coerceTypes: false, 14 | format: 'fast', 15 | nullable: true, 16 | unicode: true, 17 | uniqueItems: true, 18 | useDefaults: true, 19 | }); 20 | 21 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); 22 | 23 | export {EntityTypes, EntityOne, EntityTwo, Entity, Value}; 24 | export const Schema = { 25 | $schema: 'http://json-schema.org/draft-07/schema#', 26 | definitions: { 27 | Entity: { 28 | else: { 29 | else: { 30 | else: { 31 | properties: { 32 | type: { 33 | enum: ['TypeOne', 'TypeTwo', 'TypeThree'], 34 | type: 'string', 35 | }, 36 | }, 37 | required: ['type'], 38 | }, 39 | if: { 40 | properties: { 41 | type: { 42 | enum: ['TypeThree'], 43 | type: 'string', 44 | }, 45 | }, 46 | required: ['type'], 47 | }, 48 | then: { 49 | defaultProperties: [], 50 | properties: { 51 | baz: { 52 | type: 'number', 53 | }, 54 | type: { 55 | enum: ['TypeThree'], 56 | type: 'string', 57 | }, 58 | }, 59 | required: ['baz', 'type'], 60 | type: 'object', 61 | }, 62 | }, 63 | if: { 64 | properties: { 65 | type: { 66 | enum: ['TypeTwo'], 67 | type: 'string', 68 | }, 69 | }, 70 | required: ['type'], 71 | }, 72 | then: { 73 | $ref: '#/definitions/EntityTwo', 74 | }, 75 | }, 76 | if: { 77 | properties: { 78 | type: { 79 | enum: ['TypeOne'], 80 | type: 'string', 81 | }, 82 | }, 83 | required: ['type'], 84 | }, 85 | then: { 86 | $ref: '#/definitions/EntityOne', 87 | }, 88 | }, 89 | EntityOne: { 90 | defaultProperties: [], 91 | properties: { 92 | foo: { 93 | type: 'string', 94 | }, 95 | type: { 96 | enum: ['TypeOne'], 97 | type: 'string', 98 | }, 99 | }, 100 | required: ['foo', 'type'], 101 | type: 'object', 102 | }, 103 | EntityTwo: { 104 | defaultProperties: [], 105 | properties: { 106 | bar: { 107 | type: 'string', 108 | }, 109 | type: { 110 | enum: ['TypeTwo'], 111 | type: 'string', 112 | }, 113 | }, 114 | required: ['bar', 'type'], 115 | type: 'object', 116 | }, 117 | EntityTypes: { 118 | enum: ['TypeOne', 'TypeThree', 'TypeTwo'], 119 | type: 'string', 120 | }, 121 | Value: { 122 | else: { 123 | else: { 124 | else: { 125 | properties: { 126 | number: { 127 | enum: [0, 1, 2], 128 | type: 'number', 129 | }, 130 | }, 131 | required: ['number'], 132 | }, 133 | if: { 134 | properties: { 135 | number: { 136 | enum: [2], 137 | type: 'number', 138 | }, 139 | }, 140 | required: ['number'], 141 | }, 142 | then: { 143 | defaultProperties: [], 144 | properties: { 145 | baz: { 146 | type: 'string', 147 | }, 148 | number: { 149 | enum: [2], 150 | type: 'number', 151 | }, 152 | }, 153 | required: ['baz', 'number'], 154 | type: 'object', 155 | }, 156 | }, 157 | if: { 158 | properties: { 159 | number: { 160 | enum: [1], 161 | type: 'number', 162 | }, 163 | }, 164 | required: ['number'], 165 | }, 166 | then: { 167 | defaultProperties: [], 168 | properties: { 169 | bar: { 170 | type: 'string', 171 | }, 172 | number: { 173 | enum: [1], 174 | type: 'number', 175 | }, 176 | }, 177 | required: ['bar', 'number'], 178 | type: 'object', 179 | }, 180 | }, 181 | if: { 182 | properties: { 183 | number: { 184 | enum: [0], 185 | type: 'number', 186 | }, 187 | }, 188 | required: ['number'], 189 | }, 190 | then: { 191 | defaultProperties: [], 192 | properties: { 193 | foo: { 194 | type: 'string', 195 | }, 196 | number: { 197 | enum: [0], 198 | type: 'number', 199 | }, 200 | }, 201 | required: ['foo', 'number'], 202 | type: 'object', 203 | }, 204 | }, 205 | }, 206 | }; 207 | ajv.addSchema(Schema, 'Schema'); 208 | export function validate( 209 | typeName: 'EntityTypes', 210 | ): (value: unknown) => EntityTypes; 211 | export function validate(typeName: 'EntityOne'): (value: unknown) => EntityOne; 212 | export function validate(typeName: 'EntityTwo'): (value: unknown) => EntityTwo; 213 | export function validate(typeName: 'Entity'): (value: unknown) => Entity; 214 | export function validate(typeName: 'Value'): (value: unknown) => Value; 215 | export function validate(typeName: string): (value: unknown) => any { 216 | const validator: any = ajv.getSchema(`Schema#/definitions/${typeName}`); 217 | return (value: unknown): any => { 218 | if (!validator) { 219 | throw new Error( 220 | `No validator defined for Schema#/definitions/${typeName}`, 221 | ); 222 | } 223 | 224 | const valid = validator(value); 225 | 226 | if (!valid) { 227 | throw new Error( 228 | 'Invalid ' + 229 | typeName + 230 | ': ' + 231 | ajv.errorsText( 232 | validator.errors!.filter((e: any) => e.keyword !== 'if'), 233 | {dataVar: typeName}, 234 | ), 235 | ); 236 | } 237 | 238 | return value as any; 239 | }; 240 | } 241 | -------------------------------------------------------------------------------- /src/Example.ts: -------------------------------------------------------------------------------- 1 | export default interface ExampleType { 2 | value: string; 3 | /** 4 | * @TJS-format email 5 | */ 6 | email?: string; 7 | /** 8 | * @default 42 9 | */ 10 | answer: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters.test.ts: -------------------------------------------------------------------------------- 1 | jest.setTimeout(30000); 2 | import rimrafCB from 'rimraf'; 3 | import {exec as execCB, ExecOptions} from 'child_process'; 4 | import * as path from 'path'; 5 | import {promisify} from 'util'; 6 | 7 | jest.setTimeout(30000); 8 | 9 | const rimraf = promisify(rimrafCB); 10 | 11 | const testDir = path.join(__dirname, 'build-parameters'); 12 | 13 | const exec = (cmd: string, options: ExecOptions): Promise => 14 | new Promise((resolve, reject) => { 15 | execCB(cmd, options, (error, stdout, stderr) => { 16 | if (error) { 17 | reject(stderr || stdout || error.message); 18 | } else { 19 | resolve(stdout); 20 | } 21 | }); 22 | }); 23 | 24 | const buildProject = async (project: string) => { 25 | await exec(`cp tsconfig.${project}.json tsconfig.json`, {cwd: testDir}); 26 | 27 | await exec(`node ../../../lib/cli ./src/Example.ts ExampleType`, { 28 | cwd: testDir, 29 | }); 30 | await exec( 31 | `node ../../../lib/cli ./src/DisjointUnionExample.ts --collection`, 32 | { 33 | cwd: testDir, 34 | }, 35 | ); 36 | 37 | await exec(`npx tsc --project ./tsconfig.json`, { 38 | cwd: testDir, 39 | }); 40 | }; 41 | 42 | beforeAll(() => exec('yarn build', {cwd: process.cwd()})); 43 | 44 | afterEach(() => 45 | Promise.all([ 46 | rimraf(path.join(testDir, 'lib')), 47 | exec('rm tsconfig.json', {cwd: testDir}), 48 | exec('rm src/Example.validator.ts', {cwd: testDir}), 49 | ]), 50 | ); 51 | 52 | test('ESNext module settings', () => 53 | // We expect a project not to build correctly if it has ES module 54 | // target and no esModuleInterop. 55 | expect(buildProject('esnext')).rejects.toMatch('TS1202:')); 56 | 57 | test('ESNext interop module settings', () => buildProject('esnext-interop')); 58 | 59 | test('ES2015 module settings', () => 60 | expect(buildProject('es2015')).rejects.toMatch('TS1202:')); 61 | 62 | test('ES2015 interop module settings', () => buildProject('es2015-interop')); 63 | 64 | test('UMD module settings', () => buildProject('umd')); 65 | 66 | test('UMD interop module settings', () => buildProject('umd-interop')); 67 | 68 | test('System module settings', () => buildProject('system')); 69 | 70 | test('System interop module settings', () => buildProject('system-interop')); 71 | 72 | test('Common JS module settings', () => buildProject('commonjs')); 73 | 74 | test('Common JS interop module settings', () => 75 | buildProject('commonjs-interop')); 76 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/src/Example.ts: -------------------------------------------------------------------------------- 1 | export default interface ExampleType { 2 | value: string; 3 | /** 4 | * @TJS-format email 5 | */ 6 | email?: string; 7 | /** 8 | * @default 42 9 | */ 10 | answer: number; 11 | } 12 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/src/index.ts: -------------------------------------------------------------------------------- 1 | import '../../../../Example.validator'; 2 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.commonjs-interop.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"], 23 | "esModuleInterop": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.commonjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"] 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.es2015-interop.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "es2015", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"], 23 | "esModuleInterop": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "es2015", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"] 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.esnext-interop.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"], 23 | "esModuleInterop": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.esnext.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "esnext", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"] 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.system-interop.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "system", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"], 23 | "esModuleInterop": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.system.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "system", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"] 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.umd-interop.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "umd", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"], 23 | "esModuleInterop": true 24 | }, 25 | "include": ["src"] 26 | } 27 | -------------------------------------------------------------------------------- /src/__tests__/build-parameters/tsconfig.umd.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "umd", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "outDir": "lib", 14 | "forceConsistentCasingInFileNames": true, 15 | "noEmitOnError": false, 16 | "noErrorTruncation": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "noImplicitReturns": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "declaration": true, 22 | "lib": ["es2018"] 23 | }, 24 | "include": ["src"] 25 | } 26 | -------------------------------------------------------------------------------- /src/__tests__/disjointUnion.test.ts: -------------------------------------------------------------------------------- 1 | import {validate} from '../DisjointUnionExample.validator'; 2 | 3 | // let validate: any; 4 | 5 | test('Enum Keys', () => { 6 | expect(() => 7 | validate('Entity')({type: 'TypeOne'}), 8 | ).toThrowErrorMatchingInlineSnapshot( 9 | `"Invalid Entity: Entity should have required property 'foo'"`, 10 | ); 11 | expect(() => 12 | validate('Entity')({type: 'TypeTwo'}), 13 | ).toThrowErrorMatchingInlineSnapshot( 14 | `"Invalid Entity: Entity should have required property 'bar'"`, 15 | ); 16 | expect(() => 17 | validate('Entity')({type: 'TypeThree'}), 18 | ).toThrowErrorMatchingInlineSnapshot( 19 | `"Invalid Entity: Entity should have required property 'baz'"`, 20 | ); 21 | expect(() => 22 | validate('Entity')({type: 'TypeFour'}), 23 | ).toThrowErrorMatchingInlineSnapshot( 24 | `"Invalid Entity: Entity.type should be equal to one of the allowed values"`, 25 | ); 26 | }); 27 | 28 | test('Number Keys', () => { 29 | expect(() => 30 | validate('Value')({number: 0}), 31 | ).toThrowErrorMatchingInlineSnapshot( 32 | `"Invalid Value: Value should have required property 'foo'"`, 33 | ); 34 | expect(() => 35 | validate('Value')({number: 1}), 36 | ).toThrowErrorMatchingInlineSnapshot( 37 | `"Invalid Value: Value should have required property 'bar'"`, 38 | ); 39 | expect(() => 40 | validate('Value')({number: 2}), 41 | ).toThrowErrorMatchingInlineSnapshot( 42 | `"Invalid Value: Value should have required property 'baz'"`, 43 | ); 44 | expect(() => 45 | validate('Value')({type: 'TypeFour'}), 46 | ).toThrowErrorMatchingInlineSnapshot( 47 | `"Invalid Value: Value should have required property 'number'"`, 48 | ); 49 | }); 50 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | import run from '../'; 2 | 3 | // let validate: any; 4 | 5 | test('run', () => { 6 | run(['src/Example.ts', 'ExampleType']); 7 | // validate = require('../Example.validator').default; 8 | }); 9 | 10 | // test('valid', () => { 11 | // expect( 12 | // validate({ 13 | // value: 'Hello World', 14 | // }), 15 | // ).toMatchInlineSnapshot(` 16 | // Object { 17 | // "answer": 42, 18 | // "value": "Hello World", 19 | // } 20 | // `); 21 | // expect( 22 | // validate({ 23 | // value: 'Hello World', 24 | // email: 'forbes@lindesay.co.uk', 25 | // }), 26 | // ).toMatchInlineSnapshot(` 27 | // Object { 28 | // "answer": 42, 29 | // "email": "forbes@lindesay.co.uk", 30 | // "value": "Hello World", 31 | // } 32 | // `); 33 | // }); 34 | 35 | // test('invalid', () => { 36 | // expect(() => validate({})).toThrowErrorMatchingInlineSnapshot(` 37 | // "ExampleType should have required property 'value' 38 | // ExampleType value: 39 | 40 | // { answer: 42 }" 41 | // `); 42 | // expect(() => 43 | // validate({ 44 | // value: 'Hello World', 45 | // email: 'forbeslindesay.co.uk', 46 | // }), 47 | // ).toThrowErrorMatchingInlineSnapshot(` 48 | // "ExampleType.email should match format \\"email\\" 49 | // ExampleType value: 50 | 51 | // { value: 'Hello World', 52 | // email: 'forbeslindesay.co.uk', 53 | // answer: 42 }" 54 | // `); 55 | // }); 56 | -------------------------------------------------------------------------------- /src/__tests__/output/ComplexExample.usage.ts: -------------------------------------------------------------------------------- 1 | import {Context} from 'koa'; 2 | import {validateKoaRequest, RequestA} from './ComplexExample.validator'; 3 | 4 | declare const x: Context; 5 | export const y: RequestA = validateKoaRequest('RequestA')(x); 6 | -------------------------------------------------------------------------------- /src/__tests__/output/ComplexExample.validator.ts: -------------------------------------------------------------------------------- 1 | /* tslint:disable */ 2 | // generated by typescript-json-validator 3 | import Ajv = require('ajv'); 4 | import {MyEnum, TypeA, TypeB, RequestA, RequestB} from '../../ComplexExample'; 5 | import {inspect} from 'util'; 6 | export interface KoaContext { 7 | readonly request?: unknown; // {body?: unknown} 8 | readonly params?: unknown; 9 | readonly query?: unknown; 10 | throw(status: 400, message: string): unknown; 11 | } 12 | export const ajv = new Ajv({allErrors: true, coerceTypes: false}); 13 | 14 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); 15 | 16 | export {MyEnum, TypeA, TypeB, RequestA, RequestB}; 17 | export const Schema = { 18 | $schema: 'http://json-schema.org/draft-07/schema#', 19 | definitions: { 20 | MyEnum: { 21 | enum: [0, 1, 2], 22 | type: 'number', 23 | }, 24 | RequestA: { 25 | properties: { 26 | body: { 27 | $ref: '#/definitions/TypeB', 28 | }, 29 | params: { 30 | properties: { 31 | e: { 32 | $ref: '#/definitions/MyEnum', 33 | }, 34 | }, 35 | required: ['e'], 36 | type: 'object', 37 | }, 38 | query: { 39 | $ref: '#/definitions/TypeA', 40 | }, 41 | }, 42 | required: ['body', 'params', 'query'], 43 | type: 'object', 44 | }, 45 | RequestB: { 46 | properties: { 47 | query: { 48 | $ref: '#/definitions/TypeA', 49 | }, 50 | }, 51 | required: ['query'], 52 | type: 'object', 53 | }, 54 | TypeA: { 55 | properties: { 56 | id: { 57 | type: 'number', 58 | }, 59 | value: { 60 | type: 'string', 61 | }, 62 | }, 63 | required: ['id', 'value'], 64 | type: 'object', 65 | }, 66 | TypeB: { 67 | properties: { 68 | id: { 69 | type: ['null', 'number'], 70 | }, 71 | value: { 72 | format: 'date-time', 73 | type: ['null', 'string'], 74 | }, 75 | }, 76 | required: ['id', 'value'], 77 | type: 'object', 78 | }, 79 | }, 80 | }; 81 | ajv.addSchema(Schema, 'Schema'); 82 | export function validateKoaRequest( 83 | typeName: 'RequestA', 84 | ): ( 85 | ctx: KoaContext, 86 | ) => { 87 | params: RequestA['params']; 88 | query: RequestA['query']; 89 | body: RequestA['body']; 90 | }; 91 | export function validateKoaRequest( 92 | typeName: 'RequestB', 93 | ): ( 94 | ctx: KoaContext, 95 | ) => { 96 | params: unknown; 97 | query: RequestB['query']; 98 | body: unknown; 99 | }; 100 | export function validateKoaRequest( 101 | typeName: string, 102 | ): ( 103 | ctx: KoaContext, 104 | ) => { 105 | params: unknown; 106 | query: unknown; 107 | body: unknown; 108 | }; 109 | export function validateKoaRequest( 110 | typeName: string, 111 | ): ( 112 | ctx: KoaContext, 113 | ) => { 114 | params: any; 115 | query: any; 116 | body: any; 117 | } { 118 | const params = ajv.getSchema( 119 | `Schema#/definitions/${typeName}/properties/params`, 120 | ); 121 | const query = ajv.getSchema( 122 | `Schema#/definitions/${typeName}/properties/query`, 123 | ); 124 | const body = ajv.getSchema(`Schema#/definitions/${typeName}/properties/body`); 125 | const validateProperty = ( 126 | prop: string, 127 | validator: any, 128 | ctx: KoaContext, 129 | ): any => { 130 | const data = 131 | prop === 'body' 132 | ? ctx.request && (ctx.request as any).body 133 | : (ctx as any)[prop]; 134 | if (validator) { 135 | const valid = validator(data); 136 | 137 | if (!valid) { 138 | ctx.throw( 139 | 400, 140 | 'Invalid request: ' + 141 | ajv.errorsText( 142 | validator.errors!.filter((e: any) => e.keyword !== 'if'), 143 | {dataVar: prop}, 144 | ) + 145 | '\n\n' + 146 | inspect({ 147 | params: ctx.params, 148 | query: ctx.query, 149 | body: ctx.request && (ctx.request as any).body, 150 | }), 151 | ); 152 | } 153 | } 154 | return data; 155 | }; 156 | return ctx => { 157 | return { 158 | params: validateProperty('params', params, ctx), 159 | query: validateProperty('query', query, ctx), 160 | body: validateProperty('body', body, ctx), 161 | }; 162 | }; 163 | } 164 | export function validate(typeName: 'MyEnum'): (value: unknown) => MyEnum; 165 | export function validate(typeName: 'TypeA'): (value: unknown) => TypeA; 166 | export function validate(typeName: 'TypeB'): (value: unknown) => TypeB; 167 | export function validate(typeName: 'RequestA'): (value: unknown) => RequestA; 168 | export function validate(typeName: 'RequestB'): (value: unknown) => RequestB; 169 | export function validate(typeName: string): (value: unknown) => any { 170 | const validator: any = ajv.getSchema(`Schema#/definitions/${typeName}`); 171 | return (value: unknown): any => { 172 | if (!validator) { 173 | throw new Error( 174 | `No validator defined for Schema#/definitions/${typeName}`, 175 | ); 176 | } 177 | 178 | const valid = validator(value); 179 | 180 | if (!valid) { 181 | throw new Error( 182 | 'Invalid ' + 183 | typeName + 184 | ': ' + 185 | ajv.errorsText( 186 | validator.errors!.filter((e: any) => e.keyword !== 'if'), 187 | {dataVar: typeName}, 188 | ), 189 | ); 190 | } 191 | 192 | return value as any; 193 | }; 194 | } 195 | -------------------------------------------------------------------------------- /src/__tests__/parse.test.ts: -------------------------------------------------------------------------------- 1 | import parse from '../parse'; 2 | import Ajv from 'ajv'; 3 | import loadTsConfig from '../loadTsConfig'; 4 | 5 | test('parse', () => { 6 | expect( 7 | parse([__dirname + '/../ComplexExample.ts'], loadTsConfig()).getAllTypes(), 8 | ).toMatchInlineSnapshot(` 9 | Object { 10 | "schema": Object { 11 | "$schema": "http://json-schema.org/draft-07/schema#", 12 | "definitions": Object { 13 | "MyEnum": Object { 14 | "enum": Array [ 15 | 0, 16 | 1, 17 | 2, 18 | ], 19 | "type": "number", 20 | }, 21 | "RequestA": Object { 22 | "properties": Object { 23 | "body": Object { 24 | "$ref": "#/definitions/TypeB", 25 | }, 26 | "params": Object { 27 | "properties": Object { 28 | "e": Object { 29 | "$ref": "#/definitions/MyEnum", 30 | }, 31 | }, 32 | "required": Array [ 33 | "e", 34 | ], 35 | "type": "object", 36 | }, 37 | "query": Object { 38 | "$ref": "#/definitions/TypeA", 39 | }, 40 | }, 41 | "required": Array [ 42 | "body", 43 | "params", 44 | "query", 45 | ], 46 | "type": "object", 47 | }, 48 | "RequestB": Object { 49 | "properties": Object { 50 | "query": Object { 51 | "$ref": "#/definitions/TypeA", 52 | }, 53 | }, 54 | "required": Array [ 55 | "query", 56 | ], 57 | "type": "object", 58 | }, 59 | "TypeA": Object { 60 | "properties": Object { 61 | "id": Object { 62 | "type": "number", 63 | }, 64 | "value": Object { 65 | "type": "string", 66 | }, 67 | }, 68 | "required": Array [ 69 | "id", 70 | "value", 71 | ], 72 | "type": "object", 73 | }, 74 | "TypeB": Object { 75 | "properties": Object { 76 | "id": Object { 77 | "type": Array [ 78 | "null", 79 | "number", 80 | ], 81 | }, 82 | "value": Object { 83 | "format": "date-time", 84 | "type": Array [ 85 | "null", 86 | "string", 87 | ], 88 | }, 89 | }, 90 | "required": Array [ 91 | "id", 92 | "value", 93 | ], 94 | "type": "object", 95 | }, 96 | }, 97 | }, 98 | "symbols": Array [ 99 | "MyEnum", 100 | "TypeA", 101 | "TypeB", 102 | "RequestA", 103 | "RequestB", 104 | ], 105 | } 106 | `); 107 | }); 108 | 109 | test('ajv', () => { 110 | const parsed = parse([__dirname + '/../ComplexExample.ts'], loadTsConfig(), { 111 | titles: true, 112 | }); 113 | const {schema} = parsed.getAllTypes(); 114 | const ajv = new Ajv({coerceTypes: false, allErrors: true, useDefaults: true}); 115 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); 116 | ajv.addSchema(schema, 'root'); 117 | const validateMyEnum = ajv.getSchema('root#/definitions/MyEnum')!; 118 | expect(validateMyEnum(1)).toBe(true); 119 | expect(validateMyEnum(10)).toBe(false); 120 | expect( 121 | ajv.errorsText(validateMyEnum.errors, {dataVar: 'x'}), 122 | ).toMatchInlineSnapshot(`"x should be equal to one of the allowed values"`); 123 | 124 | const validateRequestA = ajv.getSchema('root#/definitions/RequestA')!; 125 | expect( 126 | validateRequestA({query: {id: 'x', value: 'y'}, params: {e: 42}}), 127 | ).toBe(false); 128 | expect( 129 | ajv.errorsText(validateRequestA.errors, {dataVar: 'req'}), 130 | ).toMatchInlineSnapshot( 131 | `"req.query.id should be number, req should have required property 'body', req.params.e should be equal to one of the allowed values"`, 132 | ); 133 | }); 134 | -------------------------------------------------------------------------------- /src/__tests__/printValidator.test.ts: -------------------------------------------------------------------------------- 1 | import parse from '../parse'; 2 | import {printTypeCollectionValidator} from '../printValidator'; 3 | import {writeFileSync} from 'fs'; 4 | import prettierFile from '../prettierFile'; 5 | import loadTsConfig from '../loadTsConfig'; 6 | 7 | let validate: typeof import('./output/ComplexExample.validator') = undefined as any; 8 | 9 | test('print', () => { 10 | const tsConfig = loadTsConfig(); 11 | const {symbols, schema} = parse( 12 | [__dirname + '/../ComplexExample.ts'], 13 | tsConfig, 14 | ).getAllTypes(); 15 | writeFileSync( 16 | __dirname + '/output/ComplexExample.validator.ts', 17 | printTypeCollectionValidator( 18 | symbols, 19 | schema, 20 | '../../ComplexExample', 21 | tsConfig, 22 | ), 23 | ); 24 | prettierFile(__dirname + '/output/ComplexExample.validator.ts'); 25 | writeFileSync( 26 | __dirname + '/output/ComplexExample.usage.ts', 27 | ` 28 | import {Context} from 'koa'; 29 | import {validateKoaRequest, RequestA} from './ComplexExample.validator'; 30 | 31 | declare const x: Context; 32 | export const y: RequestA = validateKoaRequest('RequestA')(x); 33 | `, 34 | ); 35 | prettierFile(__dirname + '/output/ComplexExample.usage.ts'); 36 | validate = require('./output/ComplexExample.validator'); 37 | }); 38 | test('validateValue', () => { 39 | expect(validate.validate('MyEnum')(0)).toBe(0); 40 | expect(() => 41 | validate.validate('MyEnum')(42), 42 | ).toThrowErrorMatchingInlineSnapshot( 43 | `"Invalid MyEnum: MyEnum should be equal to one of the allowed values"`, 44 | ); 45 | }); 46 | 47 | test('validateRequest', () => { 48 | expect(() => 49 | validate.validateKoaRequest('RequestA')({ 50 | params: {}, 51 | throw: (number: number, message: string) => { 52 | throw new Error(`${number} ${message}`); 53 | }, 54 | } as any), 55 | ).toThrowErrorMatchingInlineSnapshot(` 56 | "400 Invalid request: params should have required property 'e' 57 | 58 | { params: {}, query: undefined, body: undefined }" 59 | `); 60 | const {params, query, body} = validate.validateKoaRequest('RequestA')({ 61 | params: { 62 | e: validate.MyEnum.ValueB, 63 | }, 64 | query: { 65 | id: 0, 66 | value: 'hello', 67 | }, 68 | request: { 69 | body: { 70 | id: 1, 71 | value: '2019-01-25T19:09:28.179Z', 72 | }, 73 | }, 74 | throw: (number: number, message: string) => { 75 | throw new Error(`${number} ${message}`); 76 | }, 77 | } as any); 78 | expect({params, query, body}).toMatchInlineSnapshot(` 79 | Object { 80 | "body": Object { 81 | "id": 1, 82 | "value": "2019-01-25T19:09:28.179Z", 83 | }, 84 | "params": Object { 85 | "e": 1, 86 | }, 87 | "query": Object { 88 | "id": 0, 89 | "value": "hello", 90 | }, 91 | } 92 | `); 93 | }); 94 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import run from './'; 4 | 5 | run(); 6 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import {writeFileSync} from 'fs'; 2 | import {basename} from 'path'; 3 | import {parseArgs} from './parseArgs'; 4 | import parse from './parse'; 5 | import { 6 | printSingleTypeValidator, 7 | printTypeCollectionValidator, 8 | } from './printValidator'; 9 | import prettierFile from './prettierFile'; 10 | import loadTsConfig from './loadTsConfig'; 11 | import normalizeSchema from './normalizeSchema'; 12 | 13 | export { 14 | parse, 15 | parseArgs, 16 | printSingleTypeValidator, 17 | printTypeCollectionValidator, 18 | }; 19 | 20 | export default function run(args?: string[]) { 21 | const {files, options} = parseArgs(args); 22 | const tsConfig = loadTsConfig(); 23 | const parsed = parse( 24 | files.map(f => f.fileName), 25 | tsConfig, 26 | options.schema, 27 | ); 28 | 29 | files.forEach(({fileName, typeName}) => { 30 | const outputFileName = fileName.replace(/\.tsx?$/, '.validator.ts'); 31 | let validator: string; 32 | if (typeName) { 33 | const schema = parsed.getType(typeName); 34 | validator = printSingleTypeValidator( 35 | typeName, 36 | options.useNamedExport, 37 | normalizeSchema(schema), 38 | `./${basename(fileName, /\.ts$/.test(fileName) ? '.ts' : '.tsx')}`, 39 | tsConfig, 40 | options.ajv, 41 | ); 42 | } else { 43 | const {symbols, schema} = parsed.getAllTypes(); 44 | validator = printTypeCollectionValidator( 45 | symbols, 46 | normalizeSchema(schema), 47 | `./${basename(fileName, /\.ts$/.test(fileName) ? '.ts' : '.tsx')}`, 48 | tsConfig, 49 | options.ajv, 50 | ); 51 | } 52 | writeFileSync(outputFileName, validator); 53 | prettierFile(outputFileName); 54 | }); 55 | } 56 | -------------------------------------------------------------------------------- /src/loadTsConfig.ts: -------------------------------------------------------------------------------- 1 | import loadTsconfig from 'tsconfig-loader'; 2 | 3 | export default function loadTsConfig(cwd: string = process.cwd()) { 4 | const result = loadTsconfig({cwd}); 5 | const compilerOptions = result?.tsConfig.compilerOptions || {}; 6 | if ( 7 | compilerOptions.experimentalDecorators === false && 8 | compilerOptions.emitDecoratorMetadata === undefined 9 | ) { 10 | // typescript-json-schema sets emitDecoratorMetadata by default 11 | // we need to disable it if experimentalDecorators support is off 12 | compilerOptions.emitDecoratorMetadata = false; 13 | } 14 | if (compilerOptions.composite) { 15 | // the composite setting adds a few constraints that cause us all manner of problems 16 | compilerOptions.composite = false; 17 | } 18 | compilerOptions.incremental = false; 19 | 20 | // since composite and incremental are false, Typescript will not accept tsBuildInfoFile 21 | // https://github.com/microsoft/TypeScript/blob/dcb763f62435ebb015e7fa405eb067de3254f217/src/compiler/program.ts#L2847 22 | delete compilerOptions.tsBuildInfoFile; 23 | 24 | return compilerOptions; 25 | } 26 | -------------------------------------------------------------------------------- /src/normalizeSchema.ts: -------------------------------------------------------------------------------- 1 | import * as TJS from 'typescript-json-schema'; 2 | 3 | export default function normalizeSchema( 4 | schema: TJS.Definition, 5 | ): TJS.Definition { 6 | let result = schema; 7 | if (schema.anyOf && schema.definitions) { 8 | let {anyOf, ...extra} = schema; 9 | result = {...processAnyOf(anyOf, schema.definitions), ...extra}; 10 | } 11 | let outputDefinitions: { 12 | [key: string]: TJS.Definition; 13 | } = {}; 14 | if (schema.definitions) { 15 | const defs: { 16 | [key: string]: TJS.Definition; 17 | } = schema.definitions; 18 | Object.keys(defs).forEach(definition => { 19 | if ( 20 | defs[definition].anyOf && 21 | Object.keys(defs[definition]).length === 1 22 | ) { 23 | outputDefinitions[definition] = processAnyOf( 24 | defs[definition].anyOf!, 25 | defs, 26 | ); 27 | } else { 28 | outputDefinitions[definition] = defs[definition]; 29 | } 30 | }); 31 | } 32 | return { 33 | ...result, 34 | definitions: schema.definitions ? outputDefinitions : schema.definitions, 35 | }; 36 | return schema; 37 | } 38 | 39 | function processAnyOf( 40 | types: TJS.Definition[], 41 | definitions: { 42 | [key: string]: TJS.Definition; 43 | }, 44 | ): TJS.Definition { 45 | function resolve(ref: TJS.Definition) { 46 | let match; 47 | if ( 48 | ref.$ref && 49 | (match = /\#\/definitions\/([a-zA-Z0-9_]+)/.exec(ref.$ref)) && 50 | definitions[match[1]] 51 | ) { 52 | return definitions[match[1]]; 53 | } else { 54 | return ref; 55 | } 56 | } 57 | const resolved = types.map(resolve); 58 | const typeKeys = intersect(resolved.map(getCandidates)).filter(candidate => { 59 | const seen = new Set(); 60 | const firstType = getType(resolved[0], candidate); 61 | return resolved.every(type => { 62 | const v = getValue(type, candidate); 63 | if (seen.has(v) || getType(type, candidate) !== firstType) { 64 | return false; 65 | } else { 66 | seen.add(v); 67 | return true; 68 | } 69 | }); 70 | }); 71 | if (typeKeys.length !== 1) { 72 | return {anyOf: types}; 73 | } 74 | const key = typeKeys[0]; 75 | const type = getType(resolved[0], key); 76 | 77 | function recurse(remainingTypes: TJS.Definition[]): TJS.Definition { 78 | if (remainingTypes.length === 0) { 79 | return { 80 | properties: { 81 | [key]: { 82 | type, 83 | enum: resolved.map(type => getValue(type, key)), 84 | }, 85 | }, 86 | required: [key], 87 | }; 88 | } else { 89 | return { 90 | if: { 91 | properties: { 92 | [key]: {type, enum: [getValue(resolve(remainingTypes[0]), key)]}, 93 | }, 94 | required: [key], 95 | }, 96 | then: remainingTypes[0], 97 | else: recurse(remainingTypes.slice(1)), 98 | } as any; 99 | } 100 | } 101 | return recurse(types); 102 | } 103 | 104 | function getCandidates(type: TJS.Definition) { 105 | const required = type.required || []; 106 | return required.filter( 107 | key => 108 | type.properties && 109 | type.properties[key] && 110 | (type.properties[key].type === 'string' || 111 | type.properties[key].type === 'number') && 112 | type.properties[key].enum && 113 | type.properties[key].enum.length === 1, 114 | ); 115 | } 116 | function getType(type: TJS.Definition, key: string): string { 117 | return type.properties![key].type; 118 | } 119 | function getValue(type: TJS.Definition, key: string): string | number { 120 | return type.properties![key].enum[0]; 121 | } 122 | function intersect(values: string[][]): string[] { 123 | return values[0].filter(v => values.every(vs => vs.includes(v))); 124 | } 125 | -------------------------------------------------------------------------------- /src/parse.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import * as TJS from 'typescript-json-schema'; 3 | 4 | export default function parse( 5 | filenames: string[], 6 | tsConfig: any, 7 | settings: TJS.PartialArgs = {}, 8 | ) { 9 | filenames = filenames.map(f => resolve(f)); 10 | const program = TJS.getProgramFromFiles(filenames, tsConfig); 11 | 12 | const generator = TJS.buildGenerator(program, { 13 | rejectDateType: true, 14 | aliasRef: true, 15 | required: true, 16 | topRef: true, 17 | strictNullChecks: true, 18 | ...settings, 19 | }); 20 | 21 | if (!generator) { 22 | throw new Error('Did not expect generator to be null'); 23 | } 24 | 25 | return { 26 | getAllTypes(includeReffedDefinitions = true, ...fns: string[]) { 27 | const symbols = generator.getMainFileSymbols( 28 | program, 29 | fns.length ? fns : filenames, 30 | ); 31 | const schema = generator.getSchemaForSymbols( 32 | symbols, 33 | includeReffedDefinitions, 34 | ); 35 | 36 | return {symbols, schema}; 37 | }, 38 | getType(name: string) { 39 | return generator.getSchemaForSymbol(name); 40 | }, 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /src/parseArgs.ts: -------------------------------------------------------------------------------- 1 | import {basename} from 'path'; 2 | import {sync as globSync} from 'glob'; 3 | import { 4 | getDefaultArgs, 5 | Args as TypeScriptJsonSchemaArgs, 6 | } from 'typescript-json-schema'; 7 | import Ajv from 'ajv'; 8 | 9 | export interface Options { 10 | schema: Pick< 11 | TypeScriptJsonSchemaArgs, 12 | Exclude 13 | >; 14 | ajv: Ajv.Options; 15 | useNamedExport: boolean; 16 | } 17 | export interface File { 18 | fileName: string; 19 | typeName?: string; 20 | } 21 | export interface ParsedArgs { 22 | files: File[]; 23 | options: Options; 24 | } 25 | export function parseArgs(args?: string[]): ParsedArgs { 26 | var helpText = 27 | 'Usage: typescript-json-schema '; 28 | const defaultArgs = getDefaultArgs(); 29 | const parsedArgs = require('yargs') 30 | .usage(helpText) 31 | .demand(1) 32 | 33 | // typescript-json-schema options 34 | 35 | .boolean('refs') 36 | .default('refs', defaultArgs.ref) 37 | .describe('refs', 'Create shared ref definitions.') 38 | .boolean('aliasRefs') 39 | .default('aliasRefs', defaultArgs.aliasRef) 40 | .describe( 41 | 'aliasRefs', 42 | 'Create shared ref definitions for the type aliases.', 43 | ) 44 | .boolean('topRef') 45 | .default('topRef', defaultArgs.topRef) 46 | .describe('topRef', 'Create a top-level ref definition.') 47 | .boolean('titles') 48 | .default('titles', defaultArgs.titles) 49 | .describe('titles', 'Creates titles in the output schema.') 50 | .boolean('defaultProps') 51 | // default to enabling default props 52 | .default('defaultProps', true) 53 | .describe('defaultProps', 'Create default properties definitions.') 54 | .boolean('noExtraProps') 55 | .default('noExtraProps', defaultArgs.noExtraProps) 56 | .describe( 57 | 'noExtraProps', 58 | 'Disable additional properties in objects by default.', 59 | ) 60 | .boolean('propOrder') 61 | .default('propOrder', defaultArgs.propOrder) 62 | .describe('propOrder', 'Create property order definitions.') 63 | .boolean('typeOfKeyword') 64 | .default('typeOfKeyword', defaultArgs.typeOfKeyword) 65 | .describe( 66 | 'typeOfKeyword', 67 | 'Use typeOf keyword (https://goo.gl/DC6sni) for functions.', 68 | ) 69 | .boolean('required') 70 | // default to requiring non-optional props 71 | .default('required', true) 72 | .describe('required', 'Create required array for non-optional properties.') 73 | .boolean('strictNullChecks') 74 | // default to strict null checks 75 | .default('strictNullChecks', true) 76 | .describe('strictNullChecks', 'Make values non-nullable by default.') 77 | .boolean('ignoreErrors') 78 | .default('ignoreErrors', defaultArgs.ignoreErrors) 79 | .describe('ignoreErrors', 'Generate even if the program has errors.') 80 | .array('validationKeywords') 81 | .default('validationKeywords', defaultArgs.validationKeywords) 82 | .describe( 83 | 'validationKeywords', 84 | 'Provide additional validation keywords to include.', 85 | ) 86 | .boolean('excludePrivate') 87 | .default('excludePrivate', defaultArgs.excludePrivate) 88 | .describe('excludePrivate', 'Exclude private members from the schema.') 89 | .boolean('uniqueNames') 90 | .default('uniqueNames', defaultArgs.uniqueNames) 91 | .describe('uniqueNames', 'Use unique names for type symbols.') 92 | .array('include') 93 | .default('*', defaultArgs.include) 94 | .describe( 95 | 'include', 96 | 'Further limit tsconfig to include only matching files.', 97 | ) 98 | .boolean('rejectDateType') 99 | .default('rejectDateType', defaultArgs.rejectDateType) 100 | .describe('rejectDateType', 'Rejects Date fields in type definitions.') 101 | .string('id') 102 | .default('id', defaultArgs.id) 103 | .describe('id', 'ID of schema.') 104 | 105 | // ajv options 106 | 107 | .boolean('uniqueItems') 108 | .default('uniqueItems', true) 109 | .describe('uniqueItems', 'Validate `uniqueItems` keyword') 110 | .boolean('unicode') 111 | .default('unicode', true) 112 | .describe( 113 | 'unicode', 114 | 'calculate correct length of strings with unicode pairs (true by default). Pass false to use .length of strings that is faster, but gives "incorrect" lengths of strings with unicode pairs - each unicode pair is counted as two characters.', 115 | ) 116 | .boolean('nullable') 117 | .default('nullable', true) 118 | .describe( 119 | 'nullable', 120 | 'support keyword "nullable" from Open API 3 specification.', 121 | ) 122 | .choices('format', ['fast', 'full']) 123 | .default('format', 'fast') 124 | .describe( 125 | 'format', 126 | "formats validation mode ('fast' by default). Pass 'full' for more correct and slow validation or false not to validate formats at all. E.g., 25:00:00 and 2015/14/33 will be invalid time and date in 'full' mode but it will be valid in 'fast' mode.", 127 | ) 128 | .boolean('coerceTypes') 129 | .default('coerceTypes', false) 130 | .describe( 131 | 'coerceTypes', 132 | 'Change data type of data to match type keyword. e.g. parse numbers in strings', 133 | ) 134 | 135 | // specific to typescript-json-validator 136 | 137 | .boolean('collection') 138 | .default('collection', false) 139 | .describe( 140 | 'collection', 141 | 'Process the file as a collection of types, instead of one single type.', 142 | ) 143 | .boolean('useNamedExport') 144 | .default('useNamedExport', false) 145 | .describe( 146 | 'useNamedExport', 147 | 'Type name is a named export, rather than the default export of the file', 148 | ) 149 | .parse(args); 150 | 151 | const isCollection: boolean = parsedArgs.collection; 152 | const files: File[] = []; 153 | 154 | globSync(parsedArgs._[0]) 155 | .filter(filename => !/\.validator\.tsx?$/.test(filename)) 156 | .forEach(fileName => { 157 | if (isCollection) { 158 | files.push({fileName}); 159 | } else { 160 | const typeName = parsedArgs._[1] || basename(fileName, '.ts'); 161 | files.push({fileName, typeName}); 162 | } 163 | }); 164 | 165 | return { 166 | files, 167 | options: { 168 | schema: { 169 | ref: parsedArgs.refs, 170 | aliasRef: parsedArgs.aliasRefs, 171 | topRef: parsedArgs.topRef, 172 | titles: parsedArgs.titles, 173 | defaultProps: parsedArgs.defaultProps, 174 | noExtraProps: parsedArgs.noExtraProps, 175 | propOrder: parsedArgs.propOrder, 176 | typeOfKeyword: parsedArgs.useTypeOfKeyword, 177 | required: parsedArgs.required, 178 | strictNullChecks: parsedArgs.strictNullChecks, 179 | ignoreErrors: parsedArgs.ignoreErrors, 180 | validationKeywords: parsedArgs.validationKeywords, 181 | include: parsedArgs.include, 182 | excludePrivate: parsedArgs.excludePrivate, 183 | uniqueNames: parsedArgs.uniqueNames, 184 | rejectDateType: parsedArgs.rejectDateType, 185 | id: parsedArgs.id, 186 | }, 187 | ajv: { 188 | coerceTypes: parsedArgs.coerceTypes, 189 | format: parsedArgs.format, 190 | nullable: parsedArgs.nullable, 191 | unicode: parsedArgs.unicode, 192 | uniqueItems: parsedArgs.uniqueItems, 193 | useDefaults: parsedArgs.defaultProps, 194 | }, 195 | useNamedExport: parsedArgs.useNamedExport, 196 | }, 197 | }; 198 | } 199 | -------------------------------------------------------------------------------- /src/prettierFile.ts: -------------------------------------------------------------------------------- 1 | import {sync as spawnSync} from 'cross-spawn'; 2 | let prettierPath: string | undefined = undefined; 3 | try { 4 | prettierPath = require.resolve('.bin/prettier'); 5 | } catch (ex) {} 6 | 7 | export default function prettierFile(fileName: string) { 8 | if (prettierPath) { 9 | spawnSync(prettierPath, [fileName, '--write']); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/printValidator.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import * as TJS from 'typescript-json-schema'; 3 | import * as t from './template'; 4 | 5 | function isKoaType(typeDefinition: TJS.Definition) { 6 | return ( 7 | typeDefinition && 8 | typeDefinition.properties && 9 | KoaProperties.some(property => property in typeDefinition.properties!) && 10 | Object.keys(typeDefinition.properties).every(property => 11 | KoaProperties.includes(property), 12 | ) 13 | ); 14 | } 15 | const KoaProperties = ['params', 'query', 'body']; 16 | export function printTypeCollectionValidator( 17 | symbols: string[], 18 | schema: TJS.Definition, 19 | relativePath: string, 20 | tsConfig: any, 21 | options: Ajv.Options = {}, 22 | ) { 23 | const koaTypes = symbols.filter(typeName => { 24 | return isKoaType(schema.definitions && schema.definitions[typeName]); 25 | }); 26 | return [ 27 | t.TSLINT_DISABLE, 28 | t.GENERATED_COMMENT, 29 | t.IMPORT_AJV(tsConfig), 30 | t.importNamedTypes(symbols, relativePath), 31 | ...(koaTypes.length ? [t.IMPORT_INSPECT, t.DECLARE_KOA_CONTEXT] : []), 32 | t.declareAJV(options), 33 | t.exportNamed(symbols), 34 | t.declareSchema('Schema', schema), 35 | t.addSchema('Schema'), 36 | ...koaTypes.map(s => t.validateKoaRequestOverload(s, schema)), 37 | ...(koaTypes.length 38 | ? [t.VALIDATE_KOA_REQUEST_FALLBACK, t.VALIDATE_KOA_REQUEST_IMPLEMENTATION] 39 | : []), 40 | ...symbols.map(s => t.validateOverload(s)), 41 | t.VALIDATE_IMPLEMENTATION, 42 | ].join('\n'); 43 | } 44 | 45 | export function printSingleTypeValidator( 46 | typeName: string, 47 | isNamedExport: boolean, 48 | schema: TJS.Definition, 49 | relativePath: string, 50 | tsConfig: any, 51 | options: Ajv.Options = {}, 52 | ) { 53 | return [ 54 | t.TSLINT_DISABLE, 55 | t.GENERATED_COMMENT, 56 | t.IMPORT_INSPECT, 57 | t.IMPORT_AJV(tsConfig), 58 | t.importType(typeName, relativePath, {isNamedExport}), 59 | t.declareAJV(options), 60 | t.exportNamed([typeName]), 61 | t.declareSchema(typeName + 'Schema', schema), 62 | // TODO: koa implementation 63 | t.DECLARE_VALIDATE_TYPE, 64 | t.validateFn(typeName, typeName + 'Schema'), 65 | ].join('\n'); 66 | } 67 | -------------------------------------------------------------------------------- /src/template.ts: -------------------------------------------------------------------------------- 1 | import Ajv from 'ajv'; 2 | import stringify from 'json-stable-stringify'; 3 | import * as TJS from 'typescript-json-schema'; 4 | 5 | export const TSLINT_DISABLE = `/* tslint:disable */`; 6 | export const GENERATED_COMMENT = `// generated by typescript-json-validator`; 7 | 8 | export const IMPORT_INSPECT = `import {inspect} from 'util';`; 9 | 10 | export const IMPORT_AJV = (tsConfig: any): string => { 11 | return tsConfig.allowSyntheticDefaultImports || 12 | (tsConfig.esModuleInterop && /^es/.test(tsConfig.module)) || 13 | tsConfig.module === 'system' 14 | ? `import Ajv from 'ajv';` 15 | : `import Ajv = require('ajv');`; 16 | }; 17 | 18 | export const DECLARE_KOA_CONTEXT = `export interface KoaContext { 19 | readonly request?: unknown; // {body?: unknown} 20 | readonly params?: unknown; 21 | readonly query?: unknown; 22 | throw(status: 400, message: string): unknown; 23 | }`; 24 | 25 | export const importNamedTypes = (names: string[], relativePath: string) => 26 | `import {${names.join(', ')}} from '${relativePath}';`; 27 | export const importDefaultType = (name: string, relativePath: string) => 28 | `import ${name} from '${relativePath}';`; 29 | export const importType = ( 30 | name: string, 31 | relativePath: string, 32 | {isNamedExport}: {isNamedExport: boolean}, 33 | ) => 34 | isNamedExport 35 | ? importNamedTypes([name], relativePath) 36 | : importDefaultType(name, relativePath); 37 | 38 | export const declareAJV = (options: Ajv.Options) => 39 | `export const ajv = new Ajv(${stringify({ 40 | coerceTypes: false, 41 | allErrors: true, 42 | ...options, 43 | })}); 44 | 45 | ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); 46 | `; 47 | 48 | export const exportNamed = (names: string[]) => `export {${names.join(', ')}};`; 49 | 50 | export const declareSchema = (name: string, schema: TJS.Definition) => 51 | `export const ${name} = ${stringify(schema, {space: 2})};`; 52 | 53 | export const addSchema = (name: string) => `ajv.addSchema(${name}, '${name}')`; 54 | 55 | export const DECLARE_VALIDATE_TYPE = `export type ValidateFunction = ((data: unknown) => data is T) & Pick`; 56 | export const validateType = (typeName: string) => 57 | `ValidateFunction<${typeName}>`; 58 | 59 | export const compileSchema = (schemaName: string, typeName: string) => 60 | `ajv.compile(${schemaName}) as ${validateType(typeName)}`; 61 | 62 | export const validateFn = ( 63 | typeName: string, 64 | schemaName: string, 65 | ) => `export const is${typeName} = ${compileSchema(schemaName, typeName)}; 66 | export default function validate(value: unknown): ${typeName} { 67 | if (is${typeName}(value)) { 68 | return value; 69 | } else { 70 | throw new Error( 71 | ajv.errorsText(is${typeName}.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: '${typeName}'}) + 72 | '\\n\\n' + 73 | inspect(value), 74 | ); 75 | } 76 | } 77 | `; 78 | 79 | function typeOf(typeName: string, property: string, schema: TJS.Definition) { 80 | if (schema.definitions && schema.definitions[typeName]) { 81 | const typeSchema: TJS.Definition = schema.definitions[typeName]; 82 | if ( 83 | typeSchema.properties && 84 | Object.keys(typeSchema.properties).includes(property) 85 | ) { 86 | return `${typeName}['${property}']`; 87 | } 88 | } 89 | return 'unknown'; 90 | } 91 | 92 | export const validateKoaRequestOverload = ( 93 | typeName: string, 94 | schema: TJS.Definition, 95 | ) => 96 | `export function validateKoaRequest(typeName: '${typeName}'): (ctx: KoaContext) => { 97 | params: ${typeOf(typeName, 'params', schema)}, 98 | query: ${typeOf(typeName, 'query', schema)}, 99 | body: ${typeOf(typeName, 'body', schema)}, 100 | };`; 101 | 102 | export const VALIDATE_KOA_REQUEST_FALLBACK = `export function validateKoaRequest(typeName: string): (ctx: KoaContext) => { 103 | params: unknown, 104 | query: unknown, 105 | body: unknown, 106 | };`; 107 | 108 | export const VALIDATE_KOA_REQUEST_IMPLEMENTATION = `export function validateKoaRequest(typeName: string): (ctx: KoaContext) => { 109 | params: any, 110 | query: any, 111 | body: any, 112 | } { 113 | const params = ajv.getSchema(\`Schema#/definitions/\${typeName}/properties/params\`); 114 | const query = ajv.getSchema(\`Schema#/definitions/\${typeName}/properties/query\`); 115 | const body = ajv.getSchema(\`Schema#/definitions/\${typeName}/properties/body\`); 116 | const validateProperty = ( 117 | prop: string, 118 | validator: any, 119 | ctx: KoaContext, 120 | ): any => { 121 | const data = prop === 'body' ? ctx.request && (ctx.request as any).body : (ctx as any)[prop]; 122 | if (validator) { 123 | const valid = validator(data); 124 | 125 | if (!valid) { 126 | ctx.throw( 127 | 400, 128 | 'Invalid request: ' + ajv.errorsText(validator.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: prop}) + '\\n\\n' + inspect({params: ctx.params, query: ctx.query, body: ctx.request && (ctx.request as any).body}), 129 | ); 130 | } 131 | } 132 | return data; 133 | }; 134 | return (ctx) => { 135 | return { 136 | params: validateProperty('params', params, ctx), 137 | query: validateProperty('query', query, ctx), 138 | body: validateProperty('body', body, ctx), 139 | } 140 | }; 141 | }`; 142 | 143 | export const validateOverload = (typeName: string) => 144 | `export function validate(typeName: '${typeName}'): (value: unknown) => ${typeName};`; 145 | export const VALIDATE_IMPLEMENTATION = `export function validate(typeName: string): (value: unknown) => any { 146 | const validator: any = ajv.getSchema(\`Schema#/definitions/\${typeName}\`); 147 | return (value: unknown): any => { 148 | if (!validator) { 149 | throw new Error(\`No validator defined for Schema#/definitions/\${typeName}\`) 150 | } 151 | 152 | const valid = validator(value); 153 | 154 | if (!valid) { 155 | throw new Error( 156 | 'Invalid ' + typeName + ': ' + ajv.errorsText(validator.errors!.filter((e: any) => e.keyword !== 'if'), {dataVar: typeName}), 157 | ); 158 | } 159 | 160 | return value as any; 161 | }; 162 | }`; 163 | -------------------------------------------------------------------------------- /src/usage.ts: -------------------------------------------------------------------------------- 1 | import {readFileSync, writeFileSync} from 'fs'; 2 | import {spawnSync} from 'child_process'; 3 | 4 | const DELIMITER = ''; 5 | const README = readFileSync('README.md', 'utf8').split(DELIMITER); 6 | const result = spawnSync('node', [__dirname + '/cli', '--help']); 7 | if (result.error) { 8 | throw result.error; 9 | } 10 | if (result.status !== 0) { 11 | throw new Error('cli --help exited with non zero code'); 12 | } 13 | 14 | README[1] = '\n```\n' + result.stdout.toString() + '\n```\n'; 15 | writeFileSync('README.md', README.join(DELIMITER)); 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2018", 4 | "module": "commonjs", 5 | "moduleResolution": "Node", 6 | "noImplicitAny": true, 7 | "skipLibCheck": true, 8 | "experimentalDecorators": false, 9 | "importHelpers": false, 10 | "pretty": true, 11 | "sourceMap": true, 12 | "strict": true, 13 | "esModuleInterop": true, 14 | "outDir": "lib", 15 | "forceConsistentCasingInFileNames": true, 16 | "noEmitOnError": false, 17 | "noErrorTruncation": true, 18 | "noFallthroughCasesInSwitch": false, 19 | "noImplicitReturns": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "declaration": true, 23 | "lib": ["es2018"], 24 | "incremental": false 25 | }, 26 | "include": ["src"], 27 | "exclude": ["src/__tests__/build-parameters"] 28 | } 29 | --------------------------------------------------------------------------------