├── README.md ├── index.js ├── package.json └── test └── types.js /README.md: -------------------------------------------------------------------------------- 1 | # Duckless 2 | 3 | Duckless is a wrapper for your functions, that allows you to use Haskell style function definitions. 4 | After that, while runtime, if your function was called in improper way it will be thrown a clear human-readable exception. 5 | 6 | Currently supports: 7 | * Number of arguments 8 | * Base JavaScript types 9 | * Complex types, based on JSON Schema 10 | * Returned value of the function 11 | 12 | ## How to use 13 | 14 | ```javascript 15 | let def = require('duckless').def; 16 | let Type = require('duckless').Type; 17 | 18 | new Type({ 19 | "title": "Human", 20 | "type": "object", 21 | "properties": { 22 | "firstName": { 23 | "type": "string" 24 | }, 25 | "lastName": { 26 | "type": "string" 27 | }, 28 | "age": { 29 | "description": "Age in years", 30 | "type": "integer", 31 | "minimum": 0 32 | } 33 | }, 34 | "required": ["firstName", "lastName"] 35 | }); 36 | 37 | let sayMyName = human => `${human.firstName} ${human.lastName}`; 38 | 39 | sayMyName = def(sayMyName, ':: Human -> String'); 40 | 41 | sayMyName({firstName: "Werner", lastName: "Heisenberg"}); // Werner Heisenberg 42 | sayMyName({firstName: "Werner"}); // Missing required property: lastName in 1 argument "Human" 43 | 44 | ``` 45 | 46 | ## Why? 47 | 48 | The first question may be: why not to use TypeScript, Flow or other typed languages for js? 49 | 50 | The answer is yes, please use them, they are really good ones, but if you don't want to bring transpilers into 51 | your projects, `duckless` maybe that what you need. 52 | 53 | The second point is that after compiling you will still have JavaScript and in some cases 54 | (for example when data comes externally) errors still can occur. 55 | Using `duckless` at least you will get a good point to debug the issue. 56 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var tv4 = require('tv4'); 2 | var TYPES = {}; 3 | function Type(schema) { 4 | if (!schema.title) { 5 | throw new Error('Please provide schema title'); 6 | } 7 | var schemaName = schema.title.toLowerCase(); 8 | if (TYPES[schemaName]) { 9 | throw new Error('Schema with title ' + schema.title + ' already defined'); 10 | } 11 | TYPES[schema.title.toLowerCase()] = schema; 12 | this.schema = schema; 13 | } 14 | 15 | function checkVarType(data, type, position) { 16 | checkType(type); 17 | var origType = type; 18 | type = type.toLowerCase(); 19 | var objSchema = TYPES[type]; 20 | if (objSchema) { 21 | if (typeof data !== 'object') throw new Error('Typed ' + position + ' "' + origType + '" must be an object'); 22 | var res = tv4.validate(data, objSchema); 23 | if (!res) { 24 | tv4.error.message = tv4.error.message + ' in ' + position + ' "' + origType + '"'; 25 | throw new Error(tv4.error); 26 | } 27 | } else if (typeof data === 'object') { 28 | throw new Error('Unknown type "' + origType + '"'); 29 | } else { 30 | if (typeof data !== type) throw new Error('Wrong type in ' + position + ', expected "' + origType + '", given "' + typeof data + '"'); 31 | } 32 | } 33 | 34 | /** 35 | * checkWithTypeVariables 36 | * @param args {Array} parts, params splitted by "->" 37 | * @return args {Array} parts 38 | **/ 39 | function checkWithTypeVariables(args) { 40 | var typedVarsRegexp = new RegExp('\\\(?(.*?)\\\)?\\\s*=>'); 41 | var typeVars = args[0].match(typedVarsRegexp); 42 | if (typeVars) { 43 | args[0] = args[0].replace(typedVarsRegexp, ''); 44 | typeVars = typeVars[1].split(/,\s*/).map(function(varWithType) { 45 | var split = varWithType.split(/\s+/); 46 | return {type: split[0], var: split[1]}; 47 | }); 48 | args = args.map(function (param) { 49 | param = param.trim(); 50 | typeVars.some(function(typeVar) { 51 | checkType(typeVar.type); 52 | if (param === typeVar.var) { 53 | param = typeVar.type; 54 | return true; 55 | } 56 | }); 57 | return param; 58 | }); 59 | } 60 | return args; 61 | } 62 | 63 | function compileDefinition(def) { 64 | def = def.replace(/^\s*::\s*/, ''); 65 | if (!def) { 66 | throw new Error('Function definition must contain at least returned value'); 67 | } 68 | var ps = def.split(/\s+\-\>\s/); 69 | ps = checkWithTypeVariables(ps); 70 | ps.forEach(arg => checkType(arg)); 71 | return ps; 72 | } 73 | 74 | function checkType(type) { 75 | if (['Object', 'Function', 'Array'].indexOf(type) !== -1) { 76 | throw new Error('Not allowed to use Object, Function and Array types'); 77 | } 78 | } 79 | 80 | var def = function (func, def) { 81 | var args = compileDefinition(def); 82 | return function () { 83 | if (arguments.length < args.length - 1) { 84 | throw new Error('Too few arguments'); 85 | } 86 | args.forEach((type, i) => { 87 | if (i === args.length - 1) return; 88 | var arg = arguments[i]; 89 | checkVarType(arg, type, `${i+1} argument`); 90 | }); 91 | var res = func.apply(func, arguments); 92 | checkVarType(res, args[args.length - 1], 'returned value'); 93 | return res; 94 | } 95 | }; 96 | 97 | module.exports = { 98 | def: def, 99 | Type: Type, 100 | compileDefinition: compileDefinition 101 | }; 102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "duckless", 3 | "version": "1.0.0", 4 | "description": "Runtime javascript typization", 5 | "main": "index.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "dependencies": { 10 | "tv4": "^1.2.7" 11 | }, 12 | "devDependencies": {}, 13 | "scripts": { 14 | "test": "mocha" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/yarax/duckless.git" 19 | }, 20 | "keywords": [ 21 | "javascript", 22 | "typization", 23 | "haskell" 24 | ], 25 | "author": "Roman Krivtsov", 26 | "license": "ISC", 27 | "bugs": { 28 | "url": "https://github.com/yarax/duckless/issues" 29 | }, 30 | "homepage": "https://github.com/yarax/duckless#readme" 31 | } 32 | -------------------------------------------------------------------------------- /test/types.js: -------------------------------------------------------------------------------- 1 | var def = require('../').def; 2 | var Type = require('../').Type; 3 | var compileDefinition = require('../').compileDefinition; 4 | var assert = require('assert'); 5 | 6 | var Human = new Type({ 7 | "title": "Human", 8 | "type": "object", 9 | "properties": { 10 | "firstName": { 11 | "type": "string" 12 | }, 13 | "lastName": { 14 | "type": "string" 15 | }, 16 | "age": { 17 | "description": "Age in years", 18 | "type": "integer", 19 | "minimum": 0 20 | } 21 | }, 22 | "required": ["firstName", "lastName"] 23 | }); 24 | 25 | describe('Types checks', function () { 26 | it('Should throw exception too few parameters', () => { 27 | var sayMyName = human => `${human.firstName} ${human.lastName}`; 28 | sayMyName = def(sayMyName, ':: Human -> String'); 29 | assert.throws( 30 | () => { 31 | sayMyName(); 32 | }, 33 | /Too few arguments/ 34 | ); 35 | }); 36 | 37 | it('Should throw exception Missing required property: lastName in 1 argument "Human"', () => { 38 | var sayMyName = human => `${human.firstName} ${human.lastName}`; 39 | sayMyName = def(sayMyName, ':: Human -> String'); 40 | assert.throws( 41 | () => { 42 | sayMyName({firstName: "Werner"}); 43 | }, 44 | /Missing required property: lastName in 1 argument "Human"/ 45 | ); 46 | }); 47 | 48 | it('Should throw exception title already defined', () => { 49 | assert.throws( 50 | () => { 51 | new Type({title: "Human"}) 52 | }, 53 | /Schema with title Human already defined/ 54 | ); 55 | }); 56 | 57 | it('Should throw exception Invalid type: number (expected string) in 1 argument "Human"', () => { 58 | var sayMyName = human => `${human.firstName} ${human.lastName}`; 59 | sayMyName = def(sayMyName, ':: Human -> String'); 60 | assert.throws( 61 | () => { 62 | sayMyName({firstName: "Werner", lastName: 5}); 63 | }, 64 | /Invalid type: number \(expected string\) in 1 argument "Human"/ 65 | ); 66 | }); 67 | 68 | it('Should throw exception Unknown type "Unknown"', () => { 69 | var sayMyName = human => `${human.firstName} ${human.lastName}`; 70 | sayMyName = def(sayMyName, ':: Unknown -> String'); 71 | assert.throws( 72 | () => { 73 | sayMyName({firstName: "Werner", lastName: "Heisenberg"}); 74 | }, 75 | /Unknown type "Unknown"/ 76 | ); 77 | }); 78 | 79 | it('Should throw exception Wrong type in returned value, expected "String", given "number"', () => { 80 | var sayMyName = human => 5; 81 | sayMyName = def(sayMyName, ':: String'); 82 | assert.throws( 83 | () => { 84 | sayMyName({firstName: "Werner", lastName: "Heisenberg"}); 85 | }, 86 | /Wrong type in returned value, expected "String", given "number"/ 87 | ); 88 | }); 89 | 90 | it('Should throw exception Function definition must contain at least returned value', () => { 91 | var sayMyName = human => 5; 92 | assert.throws( 93 | () => { 94 | sayMyName = def(sayMyName, ':: '); 95 | sayMyName({firstName: "Werner", lastName: "Heisenberg"}); 96 | }, 97 | /Function definition must contain at least returned value/ 98 | ); 99 | }); 100 | 101 | it('Return undefined', () => { 102 | var sayMyName = () => { 103 | }; 104 | sayMyName = def(sayMyName, ':: undefined'); 105 | sayMyName(); 106 | }); 107 | 108 | it('Should perform function', () => { 109 | var sayMyName = human => `${human.firstName} ${human.lastName}`; 110 | sayMyName = def(sayMyName, ':: Human -> String'); 111 | var res = sayMyName({firstName: 'Werner', lastName: 'Heisenberg'}); 112 | assert.equal(res, 'Werner Heisenberg'); 113 | }); 114 | 115 | it('Should parse definition with type variables', () => { 116 | assert.deepEqual([ 't', 'Eq', 'Num' ], compileDefinition(' :: (Eq a, Num a, Num a1) => t -> a -> a1')); 117 | }); 118 | 119 | it('Should parse definition with type variables (no parentheses)', () => { 120 | assert.deepEqual([ 't', 'Num' ], compileDefinition(':: Num a => t -> a')); 121 | }); 122 | 123 | it('Should parse definition without type variables', () => { 124 | assert.deepEqual([ 't', 'a' ], compileDefinition('::t -> a')); 125 | }); 126 | 127 | it('Should throw Object, Array, Function are not allowed', () => { 128 | var sayMyName = human => `${human.firstName} ${human.lastName}`; 129 | assert.throws( 130 | () => { 131 | def(sayMyName, ':: Human -> Object') 132 | }, 133 | /Not allowed to use Object, Function and Array types/ 134 | ); 135 | }); 136 | 137 | }); --------------------------------------------------------------------------------