├── .editorconfig ├── .eslintrc ├── .gitignore ├── README.md ├── package.json ├── src ├── custom.spec.ts ├── index.spec.ts └── index.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.ts] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "eslint:recommended", 5 | "plugin:import/errors", 6 | "plugin:import/warnings" 7 | ], 8 | "env": { 9 | "es6": true, 10 | "mocha": true 11 | }, 12 | "ecmaFeatures": { 13 | "modules": true 14 | }, 15 | "rules": { 16 | "indent": ["warn", 2, { "SwitchCase": 1 }], 17 | "quotes": [1, "single", {"avoidEscape": true, "allowTemplateLiterals": true}], 18 | "no-console": 1, 19 | "no-debugger": 1, 20 | "no-var": 1, 21 | "prefer-const": 1, 22 | "semi": [1, "never"], 23 | "no-trailing-spaces": 0, 24 | "eol-last": 0, 25 | "no-unused-vars": 1, 26 | "no-underscore-dangle": 0, 27 | "no-alert": 1, 28 | "no-lone-blocks": 0, 29 | "no-empty": 1, 30 | "no-empty-pattern": 1, 31 | "no-unreachable": 1, 32 | "no-constant-condition": 1, 33 | "jsx-quotes": 1, 34 | "comma-dangle": 1, 35 | "import/no-named-as-default": 0 36 | }, 37 | "globals": { 38 | "console": true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Runtypes-generate 2 | 3 | [![standard-readme compliant](https://img.shields.io/badge/standard--readme-OK-green.svg?style=flat-square)](https://github.com/RichardLitt/standard-readme) 4 | [![npm version](https://badge.fury.io/js/runtypes-generate.svg)](https://badge.fury.io/js/runtypes-generate) 5 | 6 | `Runtypes-generate` convert [`runtypes` type](https://github.com/pelotom/runtypes) to [jsverify arbitrary](https://github.com/jsverify/jsverify). 7 | 8 | ## Table of Contents 9 | 10 | - [Background](#background) 11 | - [Install](#install) 12 | - [Usage](#usage) 13 | - [API](#api) 14 | - [Contribute](#contribute) 15 | - [License](#license) 16 | 17 | ## Background 18 | 19 | Property-based testing is very awesome approach for analyze and verification program. But this approach requires the writing of generators for all datatypes in our program. This process is very time-consuming, error-prone and not DRY. 20 | 21 | Example: 22 | 23 | ```js 24 | import { Number, Literal, Array, Tuple, Record } from 'runtypes' 25 | const AsteroidType = Record({ 26 | type: Literal('asteroid'), 27 | location: Tuple(Number, Number, Number), 28 | mass: Number, 29 | }) 30 | 31 | const AsteroidArbitrary = jsc.record({ 32 | type: jsc.constant('asteroid'), 33 | location: jsc.tuple(jsc.number, jsc.number, jsc.number), 34 | mass: jsc.number 35 | }) 36 | ``` 37 | 38 | But with `runtypes-generate` we can get `AsteroidArbitrary` from `AsteroidType`: 39 | 40 | ```js 41 | import { makeJsverifyArbitrary } from 'runtypes-generate' 42 | const AsteroidType = Record({ 43 | type: Literal('asteroid'), 44 | location: Tuple(Number, Number, Number), 45 | mass: Number, 46 | }) 47 | const AsteroidArbitrary = makeJsverifyArbitrary(AsteroidType) 48 | ``` 49 | 50 | ## Install 51 | 52 | ``` 53 | npm install --save runtypes-generate 54 | ``` 55 | 56 | ## Usage 57 | 58 | - [Core runtypes](https://github.com/typeetfunc/runtypes-generate/blob/master/src/index.spec.ts) 59 | - [Custom runtypes](https://github.com/typeetfunc/runtypes-generate/blob/master/src/custom.spec.ts) 60 | 61 | ## API 62 | 63 | - `makeJsverifyArbitrary(type: Reflect): jsc.Arbitrary` - convert `runtypes` to `jsverify` arbitrary 64 | - `addTypeToRegistry(tag: string, (x:Reflect) => jsc.Arbitrary): void` - add new generator for [`Constraint` type](https://github.com/pelotom/runtypes#constraint-checking) with [`tag` in `args` attribute](https://github.com/typeetfunc/runtypes-generate/blob/master/src/custom.spec.ts#L23-L32) 65 | - `addTypeToIntersectRegistry(tags: string[], generator: (x: Reflect) => jsc.Arbitrary): void)` - add new generator for `Intersect` or custom `Constraint` types. TODO example 66 | - `generateAndCheck(rt: Reflect, opts?: jsc.Options): () => void` - run `jsc.assert` for property `rt.check(generatedData)` for all `generatedData` obtained from `makeJsverifyArbitrary(rt)`. Uses for verification custom generators for custom `Constraint` type. See [example](https://github.com/typeetfunc/runtypes-generate/blob/master/src/custom.spec.ts#L112-L118) in tests. 67 | 68 | ## Contribute 69 | 70 | PRs accepted. 71 | 72 | If you had questions just make issue or ask them in [my telegram](https://telegram.me/bracketsarrows) 73 | 74 | Small note: If editing the Readme, please conform to the [standard-readme](https://github.com/RichardLitt/standard-readme) specification. 75 | 76 | 77 | ## License 78 | 79 | MIT © typeetfunc -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "runtypes-generate", 3 | "version": "0.4.1", 4 | "description": "Integrate runtypes with property-based testing", 5 | "main": "./lib/index.js", 6 | "types": "./lib/index.d.ts", 7 | "scripts": { 8 | "build": "tsc --pretty", 9 | "test": "npm run build && jest --verbose", 10 | "develop": "jest --watchAll", 11 | "release": "npm run release:patch", 12 | "release:patch": "xyz --increment patch", 13 | "release:minor": "xyz --increment minor", 14 | "release:major": "xyz --increment major" 15 | }, 16 | "author": "Andrey Melnikov", 17 | "license": "MIT", 18 | "dependencies": { 19 | "jsverify": "^0.8.2", 20 | "runtypes": "^0.11.0", 21 | "lodash": "^4.17.4", 22 | "@types/lodash": "^4.14.66" 23 | }, 24 | "devDependencies": { 25 | "@types/jest": "^18.1.1", 26 | "jest": "^19.0.2", 27 | "ts-jest": "^19.0.0", 28 | "typescript": "2.2.1", 29 | "xyz": "^2.1.0", 30 | "ts-node": "2.1.0" 31 | }, 32 | "keywords:": [ 33 | "runtime", 34 | "type", 35 | "validation", 36 | "typescript", 37 | "spec", 38 | "runtypes", 39 | "property-based", 40 | "jsverify" 41 | ], 42 | "repository": { 43 | "type": "git", 44 | "url": "https://github.com/typeetfunc/runtypes-generate" 45 | }, 46 | "jest": { 47 | "verbose": false, 48 | "testRegex": ".*/*.spec.ts$", 49 | "moduleFileExtensions": [ 50 | "js", 51 | "ts" 52 | ], 53 | "transform": { 54 | "\\.(ts|tsx)$": "/node_modules/ts-jest/preprocessor" 55 | }, 56 | "testEnvironment": "node" 57 | }, 58 | "files": [ 59 | "/lib" 60 | ] 61 | } 62 | -------------------------------------------------------------------------------- /src/custom.spec.ts: -------------------------------------------------------------------------------- 1 | import { Boolean, String, Literal, Array, Record, Partial, Union, Void, Constraint } from 'runtypes' 2 | import { generateAndCheck } from './index'; 3 | import * as jsc from 'jsverify'; 4 | import { range, zip } from 'lodash' 5 | import { makeJsverifyArbitrary, addTypeToRegistry } from './index' 6 | 7 | function contains(list, args, eqFn, lessFn, moreFn) { 8 | const finded = list.filter(i => args.item.guard(i)) 9 | const res = { 10 | needCount: args.count === undefined ? 1 : args.count, 11 | count: finded.length, 12 | item: args.item 13 | } 14 | if (res.needCount === res.count) { 15 | return eqFn(list, res) 16 | } else if (res.needCount > res.count) { 17 | return lessFn(list, res) 18 | } else if (res.needCount < res.count) { 19 | return moreFn(list, res) 20 | } 21 | } 22 | 23 | const ArrayWithContains = (arrRt, item, count) => (args => Constraint(arrRt, list => { 24 | const res = contains( 25 | list, args, 26 | () => true, 27 | (_, res) => `Array contains less than ${res.needCount} items`, 28 | (_, res) => `Array contains more than ${res.needCount} items` 29 | ); 30 | 31 | return res; 32 | }, args))({tag: contains.name, count, item}) 33 | 34 | 35 | function generatorContains(rt: Constraint) { 36 | const { underlying, args } = rt 37 | return makeJsverifyArbitrary(underlying).smap(coll => { 38 | const res = contains( 39 | coll, args, 40 | list => list, 41 | (list, res) => { 42 | const howMuch = res.needCount - res.count 43 | const idxs = list.length ? 44 | jsc.sampler(jsc.elements(range(list.length)))(howMuch) as any as number[] : 45 | range(howMuch) 46 | const elements = jsc.sampler(makeJsverifyArbitrary(res.item))(howMuch) as any[] 47 | zip(idxs, elements).forEach(([idx, element]) => { 48 | list.splice(idx, 0, element) 49 | }); 50 | return list 51 | }, 52 | (list, res) => { 53 | return list.reduce((acc, item) => { 54 | const isItem = res.item.guard(item) 55 | if (!isItem || acc.count < res.needCount) { 56 | acc.list.push(item) 57 | } 58 | if (isItem) { 59 | acc.count++ 60 | } 61 | return acc; 62 | }, {list: [], count: 0}).list 63 | } 64 | ) 65 | return res; 66 | }, x => x) 67 | } 68 | 69 | addTypeToRegistry(contains.name, generatorContains) 70 | 71 | describe('FamilyObject', () => { 72 | const StringOrVoid = String.Or(Void) 73 | const Fio = Partial({ 74 | firstname: StringOrVoid, 75 | lastname: StringOrVoid, 76 | middlename: StringOrVoid 77 | }) 78 | const MemberWithRole = role => Record({ 79 | role, 80 | fio: Fio 81 | }).And( 82 | Partial({ 83 | dependant: Boolean.Or(Void) 84 | }) 85 | ) 86 | const Spouse = MemberWithRole(Literal('spouse')) 87 | const NotSpouse = MemberWithRole(Union( 88 | Literal('sibling'), 89 | Literal('child'), 90 | Literal('parent'), 91 | Void 92 | )) 93 | const Member = Spouse.Or(NotSpouse) 94 | const FamilyWithTypeAndMember = (type, countSpouse) => Record({ 95 | type, 96 | members: ArrayWithContains(Array(Member), Spouse, countSpouse) 97 | }) 98 | const FamilyWithSpouse = FamilyWithTypeAndMember( 99 | Literal('espoused'), 100 | 1 101 | ) 102 | const FamilyWithoutSpouse = FamilyWithTypeAndMember( 103 | Union( 104 | Literal('single'), 105 | Literal('common_law_marriage'), 106 | Void 107 | ), 108 | 0 109 | ) 110 | const membersWithSpouse = ArrayWithContains(Array(Member), Spouse, 1) 111 | const FamilyObject = Union(FamilyWithSpouse, FamilyWithoutSpouse) 112 | test('Fio', generateAndCheck(Fio)) 113 | test('Member', generateAndCheck(Member)) 114 | test('Member', generateAndCheck(Member)) 115 | test('MemberWithSpouse', generateAndCheck(membersWithSpouse)) 116 | test('FamilyWithSpouse', generateAndCheck(FamilyWithSpouse)) 117 | test('FamilyWithoutSpouse', generateAndCheck(FamilyWithoutSpouse)) 118 | test('FamilyObject', generateAndCheck(FamilyObject)) 119 | }); 120 | 121 | -------------------------------------------------------------------------------- /src/index.spec.ts: -------------------------------------------------------------------------------- 1 | import { Boolean, Number, String, Literal, Array, Tuple, Record, Union } from 'runtypes' 2 | import { generateAndCheck } from './index'; 3 | 4 | describe('SpaceObject', () => { 5 | const Vector = Tuple(Number, Number, Number) 6 | 7 | const Asteroid = Record({ 8 | type: Literal('asteroid'), 9 | location: Vector, 10 | mass: Number, 11 | }) 12 | 13 | const Planet = Record({ 14 | type: Literal('planet'), 15 | location: Vector, 16 | mass: Number, 17 | population: Number, 18 | habitable: Boolean, 19 | }) 20 | 21 | const Rank = Union( 22 | Literal('captain'), 23 | Literal('first mate'), 24 | Literal('officer'), 25 | Literal('ensign'), 26 | ) 27 | 28 | const CrewMember = Record({ 29 | name: String, 30 | age: Number, 31 | rank: Rank, 32 | home: Planet, 33 | }) 34 | 35 | const Ship = Record({ 36 | type: Literal('ship'), 37 | location: Vector, 38 | mass: Number, 39 | name: String, 40 | crew: Array(CrewMember), 41 | }) 42 | 43 | const SpaceObject = Union(Asteroid, Planet, Ship) 44 | 45 | test('Vector', generateAndCheck(Vector)); 46 | test('Asteroid', generateAndCheck(Asteroid)); 47 | test('Rank', generateAndCheck(Rank)); 48 | test('Ship', generateAndCheck(Ship)); 49 | test('SpaceObject', generateAndCheck(SpaceObject)); 50 | }); 51 | 52 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import * as jsc from 'jsverify'; 2 | import { Reflect } from 'runtypes'; 3 | import { flatten, every, some, find, identity, pick, keys } from 'lodash'; 4 | 5 | declare module 'jsverify' { 6 | function record(spec: { [s: string]: T }): jsc.Arbitrary; 7 | function oneof(gs: jsc.Arbitrary[]): jsc.Arbitrary; 8 | } 9 | 10 | function guardEvery(rts, x) { 11 | return every(rts, rt => (rt as any).guard(x)) 12 | } 13 | 14 | function findIntersectInRegistry(intersectees, registry, getTag) { 15 | return registry.reduce( 16 | (acc, [setTags, func]) => { 17 | if (every(intersectees, intersect => setTags.has(getTag(intersect)))){ 18 | return func; 19 | } 20 | return acc; 21 | }, 22 | null 23 | ); 24 | } 25 | 26 | function defaultIntersectHandle({ intersectees }) { 27 | const allIntersectees = jsc.tuple( 28 | intersectees.map(intersect => makeJsverifyArbitrary(intersect)) 29 | ); 30 | 31 | return jsc.suchthat( 32 | allIntersectees, 33 | tuple => some(tuple, x => guardEvery(intersectees, x)) 34 | ) 35 | .smap( 36 | tuple => find(tuple, x => guardEvery(intersectees, x)), 37 | identity 38 | ); 39 | } 40 | 41 | const CUSTOM_REGISTRY = {}; 42 | const CUSTOM_INTERSECT_REGISTRY: any[] = []; 43 | const INTERSECT_REGISTRY = [ 44 | [new Set(['partial', 'record']), ({ intersectees }) => jsc.tuple(intersectees.map(intersect => makeJsverifyArbitrary(intersect))) 45 | .smap( 46 | tupleOfTypes => tupleOfTypes.reduce( 47 | (acc, item) => Object.assign(acc, item) 48 | ), 49 | object => intersectees.map(({ fields }) => pick(object, keys(fields))) 50 | ) 51 | ], 52 | [new Set(['union']), ({ intersectees }) => { 53 | const alternatives = flatten( 54 | intersectees.map(intersect => intersect.alternatives) 55 | ) 56 | const allAltArb = jsc.tuple(alternatives.map(makeJsverifyArbitrary)) 57 | 58 | return jsc.bless({ 59 | ...allAltArb, 60 | generator: allAltArb.generator.flatmap(tuple => { 61 | const onlyIntersectees = tuple.filter( 62 | x => guardEvery(intersectees, x) 63 | ) 64 | 65 | return jsc.elements(onlyIntersectees).generator; 66 | }) 67 | }) 68 | }], 69 | [new Set(['constraint']), ({ intersectees }) => { 70 | const handler = findIntersectInRegistry( 71 | intersectees, 72 | CUSTOM_INTERSECT_REGISTRY, 73 | x => x.args && x.args.tag 74 | ) || defaultIntersectHandle; 75 | 76 | return handler({ intersectees }) 77 | }] 78 | ]; 79 | 80 | 81 | const REGISTRY = { 82 | always: () => jsc.json, 83 | array: ({ element }) => jsc.array(makeJsverifyArbitrary(element)), 84 | boolean: () => jsc.bool, 85 | constraint: ({ constraint, underlying, args }) => { 86 | if (args) { 87 | if (CUSTOM_REGISTRY[args.tag]) { 88 | return CUSTOM_REGISTRY[args.tag]({ constraint, underlying, args }); 89 | } else { 90 | throw new Error(`Please add generator for ${args.tag} with addTypeToRegistry`); 91 | } 92 | } else { 93 | return jsc.suchthat(makeJsverifyArbitrary(underlying), constraint) 94 | } 95 | }, 96 | dictionary: ({ value }) => jsc.dict(makeJsverifyArbitrary(value)), 97 | function: () => jsc.fn(jsc.json), 98 | intersect: ({ intersectees }) => { 99 | const handler = findIntersectInRegistry( 100 | intersectees, 101 | INTERSECT_REGISTRY, 102 | x => x.tag 103 | ) || defaultIntersectHandle; 104 | 105 | return handler({ intersectees }); 106 | }, 107 | literal: ({ value }) => jsc.constant(value), 108 | number: () => jsc.number, 109 | partial: ({ fields }) => { 110 | var spec = {}; 111 | Object.keys(fields).forEach(fieldName => { 112 | spec[fieldName] = jsc.oneof([ 113 | makeJsverifyArbitrary(fields[fieldName]), 114 | jsc.constant(undefined) 115 | ]) 116 | }); 117 | return jsc.record(spec) 118 | .smap(rec => { 119 | const recWithoutEmpty = {...rec}; 120 | Object.keys(recWithoutEmpty).forEach(key => { 121 | if (recWithoutEmpty[key] === undefined) { 122 | delete recWithoutEmpty[key]; 123 | } 124 | }) 125 | return recWithoutEmpty; 126 | }, identity); 127 | }, 128 | record: ({ fields }) => { 129 | var spec = {}; 130 | Object.keys(fields).forEach(fieldName => { 131 | spec[fieldName] = makeJsverifyArbitrary(fields[fieldName]); 132 | }); 133 | return jsc.record(spec); 134 | }, 135 | string: () => jsc.string, 136 | tuple: ({ components }) => jsc.tuple(components.map(component => makeJsverifyArbitrary(component))), 137 | union: ({ alternatives }) => jsc.oneof(alternatives.map(alternative => makeJsverifyArbitrary(alternative))), 138 | void: () => jsc.elements([null, undefined]) 139 | }; 140 | 141 | export function makeJsverifyArbitrary(type: T): jsc.Arbitrary { 142 | if (type.tag && REGISTRY.hasOwnProperty(type.tag)) { 143 | return REGISTRY[type.tag](type); 144 | } 145 | throw new Error('Can not generate this type'); 146 | } 147 | 148 | export function addTypeToRegistry(tag: string, generator: (x: T) => jsc.Arbitrary): void { 149 | CUSTOM_REGISTRY[tag] = generator; 150 | } 151 | 152 | export function addTypeToIntersectRegistry(tags: string[], generator: (x: T) => jsc.Arbitrary): void { 153 | CUSTOM_INTERSECT_REGISTRY.push([ 154 | new Set(tags), generator 155 | ]); 156 | } 157 | 158 | export function generateAndCheck(rt: T, opts?: jsc.Options) { 159 | return () => { 160 | const arbitrary = makeJsverifyArbitrary(rt) 161 | jsc.assert(jsc.forall(arbitrary, function arbitraryIsChecked(anything) { 162 | rt.check(anything) 163 | return true; 164 | }), opts) 165 | }; 166 | } 167 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "noImplicitAny": false, 6 | "strictNullChecks": true, 7 | "noUnusedLocals": true, 8 | "noUnusedParameters": true, 9 | "declaration": true, 10 | "sourceMap": false, 11 | "outDir": "lib", 12 | "lib": ["es5", "es2015"] 13 | }, 14 | "include": [ 15 | "src/**/*" 16 | ] 17 | } 18 | --------------------------------------------------------------------------------