├── .circleci └── config.yml ├── .eslintrc.json ├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build ├── Enum.js ├── Type.js └── utils.js ├── codecov.yml ├── docs ├── README.md ├── enum_type.md ├── getting_started.md ├── react.md └── type.md ├── package-lock.json ├── package.json ├── src ├── Enum.js ├── Type.js └── utils.js ├── test ├── Enum.test.js ├── Types.test.js ├── createConstructor.test.js └── utils.test.js ├── types.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.13 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run linter! 37 | - run: yarn lint 38 | 39 | # run tests! 40 | - run: yarn test:ci 41 | 42 | - run: 43 | name: Coverage 44 | command: yarn coverage:ci 45 | environment: 46 | CODECOV_TOKEN: 15e88c8e-052b-4369-91d0-355c3891e15b 47 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es6": true, 5 | "node": true 6 | }, 7 | "extends": "eslint:recommended", 8 | "parser": "babel-eslint", 9 | "parserOptions": { 10 | "ecmaVersion": 2016, 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "strict": 0, 15 | "indent": [ 16 | "error", 17 | 4 18 | ], 19 | "linebreak-style": [ 20 | "error", 21 | "unix" 22 | ], 23 | "quotes": [ 24 | "error", 25 | "single" 26 | ], 27 | "semi": [ 28 | "error", 29 | "always" 30 | ] 31 | } 32 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .vscode 3 | node_modules 4 | *.map 5 | *.log 6 | 7 | coverage 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | docs/ 3 | test/ 4 | src/ 5 | 6 | .eslintrc 7 | 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | 8 | ## [1.0.0] - 2018-11-23 9 | ### Added 10 | - Adds documentation for types in enum_type.md 11 | 12 | ### Changed 13 | - Fixes zero argument typevalidation issue 14 | 15 | 16 | ## [1.0.0-alpha.2] - 2018-11-11 17 | ### Added 18 | - Adds and exposes Type Sum Type to allow shape and argument validation 19 | - Adds argument validation to Enum 20 | 21 | ### Changed 22 | - Refactors and reduces some of the functions to improve build size and performace 23 | 24 | 25 | ## [1.0.0-alpha.0] - 2018-11-10 26 | ### Added 27 | - Adds `cata` and `reduce` aliases for `caseOf` 28 | - Changelogs 29 | 30 | ### Changed 31 | - Moves documentation from Github Wiki to /docs to be easier to maintain 32 | - Some code refactoring to improve code quality and performance 33 | - Internal semantics to be more consistent in the way we describe the functions 34 | 35 | ### Removed 36 | - useReducer and reducerComponent HOC, to trim the fat as both the functionalities are just one of the use cases and can be provided as a custom wrapper. The documentation includes the internals of the functions if anyone wants to use them 37 | - Removes some properties and methods from the instances to make it lighter and expose a simpler api 38 | 39 | 40 | [1.0.0]: https://github.com/phenax/enum-fp/compare/v1.0.0-alpha.2...v1.0.0 41 | [1.0.0-alpha.2]: https://github.com/phenax/enum-fp/compare/v1.0.0-alpha.0...v1.0.0-alpha.2 42 | [1.0.0-alpha.0]: https://github.com/phenax/enum-fp/compare/v0.5.0...v1.0.0-alpha.0 43 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at phenax5@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via an issue or any other method with the owners of this repository before making a change. 4 | 5 | Please note we have a [code of conduct](https://github.com/phenax/enum-fp/blob/master/CODE_OF_CONDUCT.md), please follow it in all your interactions with the project. 6 | 7 | ## Pull Request Process 8 | 9 | 1. Create an issue describing the problem or start a discussion on an exisiting, related issue. 10 | 2. Create a pull request. 11 | 3. Don't forget to write tests! 12 | 4. Update the README.md with details of changes to the interface. 13 | 5. You don't have to change the version number. It'll be handled in the publishing stage. 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Akshay Nair 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Enum-FP 3 | Functional Enum type / Sum type for javascript with simple pattern matching 4 | 5 | [![CircleCI](https://img.shields.io/circleci/project/github/phenax/enum-fp/master.svg?style=for-the-badge)](https://circleci.com/gh/phenax/enum-fp) 6 | [![npm bundle size (minified + gzip)](https://img.shields.io/bundlephobia/minzip/enum-fp.svg?style=for-the-badge)](https://www.npmjs.com/package/enum-fp) 7 | [![Codecov](https://img.shields.io/codecov/c/github/phenax/enum-fp.svg?style=for-the-badge)](https://codecov.io/gh/phenax/enum-fp) 8 | 9 | 10 | [Checkout the docs for more information](./docs) 11 | 12 | [Medium article on SumTypes using EnumFP](https://medium.com/@phenax5/writing-cleaner-and-safer-javascript-with-sum-types-bec9c68ba7aa) 13 | 14 | ## Install 15 | 16 | #### To add the project to your project 17 | ```bash 18 | yarn add enum-fp 19 | ``` 20 | 21 | ## Usage 22 | 23 | #### Import it to your file 24 | ```js 25 | import Enum from 'enum-fp'; 26 | ``` 27 | 28 | #### Create an enum type 29 | ```js 30 | const Action = Enum([ 'Add', 'Edit', 'Delete', 'Get' ]); 31 | 32 | // Or with a fixed number of arguments 33 | const Maybe = Enum({ 34 | Just: [ 'value' ], 35 | Nothing: [], 36 | }); 37 | ``` 38 | 39 | #### Create an instance of the type using one of the contructors 40 | ```js 41 | const action = Action.Edit(2, 'Hello world and India'); 42 | ``` 43 | 44 | #### Pattern matching 45 | ```js 46 | const Action = Enum([ 'Add', 'Edit', 'Delete', 'DeleteAll', 'Get' ]); 47 | 48 | const logMessage = action => console.log('>>', 49 | Action.match(action, { 50 | Edit: (id, message) => `Editing [${id}] to "${message}"`, 51 | Add: message => `Adding "${message}"`, 52 | Delete: id => `Deleting [${id}]`, 53 | DeleteAll: () => 'Deleting all entries', 54 | _: () => 'Unknown action', // To handle default cases, use _ 55 | }) 56 | ); 57 | 58 | logMessage(Action.Add('Earth')); // >> Adding "Earth" 59 | logMessage(Action.Add('Earth 2')); // >> Adding "Earth 2" 60 | logMessage(Action.Add('Pluto')); 61 | logMessage(Action.Add('Pluto')); // >> Adding "Pluto1" 62 | logMessage(Action.Edit(1, 'Mars')); // >> Editing [2] to "Mars" 63 | logMessage(Action.Delete(2)); // >> Deleting [3] 64 | logMessage(Action.Add('Pluto')); // >> Adding "Pluto" 65 | logMessage(Action.DeleteAll()); // >> Deleting all entries 66 | 67 | // As Get action is not handled in the pattern, it will execute the default 68 | logMessage(Action.Get()); // >> Unknown action 69 | ``` 70 | 71 | #### Type validation 72 | You can add strict type validation instead of argument descriptions. You can read more about types module [here](./docs/react.md) 73 | 74 | ```js 75 | import T from 'enum-fp/types'; 76 | 77 | const TodoAction = Enum({ 78 | Add: [ T.String('message') ], 79 | SetChecked: [ T.Number('id'), T.Bool('isChecked') ], 80 | Delete: [ T.Number('id') ], 81 | Edit: [ T.Number('id'), T.String('message') ], 82 | DeleteAll: [], 83 | }); 84 | ``` 85 | 86 | NOTE: The string passed to the functions are just for documentation purposes and are optional. It won't affect the behavior of the type in any way. 87 | 88 | 89 | 90 | 91 | ### Enum use cases 92 | 93 | #### In the react world 94 | `You can use it to manage react component state!` [Checkout the documentation](./docs/react.md) 95 | 96 | 97 | #### Safely work with empty/invalid states 98 | 99 | * Working with invalid values 100 | ```js 101 | // Just an example. You should use `Maybe` functor in cases like these 102 | const Value = Enum({ Invalid: [], Valid: ['value'] }); 103 | 104 | const extractName = user => user && user.name 105 | ? Value.Valid(user.name) 106 | : Value.Invalid(); 107 | 108 | const splitBySpace = Value.cata({ 109 | Valid: name => name.split(' '), 110 | Invalid: () => [], 111 | }); 112 | 113 | const getNameSplit = compose(splitBySpace, extractName); 114 | 115 | const [ firstName, lastName ] = getNameSplit({ name: 'Akshay Nair' }); // >> returns ['Akshay','Nair'] 116 | ``` 117 | 118 | 119 | #### In the functional world 120 | If you are unfamiliar with `functors`, you can read [Functors in JS](https://hackernoon.com/functors-in-javascript-20a647b8f39f) blog post. 121 | 122 | * **Maybe** 123 | 124 | `Maybe` functor is used to handle null. 125 | 126 | ```js 127 | const Maybe = Enum({ Just: ['value'], Nothing: [] }); 128 | 129 | const fmap = fn => Maybe.cata({ 130 | Just: compose(Maybe.Just, fn), 131 | Nothing: Maybe.Nothing, 132 | }); 133 | ``` 134 | 135 | * **Either** 136 | 137 | `Either` functor is used for handling exceptions 138 | 139 | ```js 140 | const Either = Enum({ Left: ['error'], Right: ['value'] }); 141 | 142 | const fmap = fn => Either.cata({ 143 | Left: Either.Left, 144 | Right: compose(Either.Right, fn), 145 | }); 146 | const fmapFail = fn => Either.cata({ 147 | Left: compose(Either.Left, fn), 148 | Right: Either.Right, 149 | }); 150 | ``` 151 | -------------------------------------------------------------------------------- /build/Enum.js: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,"__esModule",{value:true});Object.defineProperty(exports,"T",{enumerable:true,get:function get(){return _Type.default}});exports.default=exports.createConstructor=void 0;var _utils=require("./utils");var _Type=_interopRequireWildcard(require("./Type"));function _interopRequireWildcard(obj){if(obj&&obj.__esModule){return obj}else{var newObj={};if(obj!=null){for(var key in obj){if(Object.prototype.hasOwnProperty.call(obj,key)){var desc=Object.defineProperty&&Object.getOwnPropertyDescriptor?Object.getOwnPropertyDescriptor(obj,key):{};if(desc.get||desc.set){Object.defineProperty(newObj,key,desc)}else{newObj[key]=obj[key]}}}}newObj.default=obj;return newObj}}var createConstructor=function createConstructor(Type,_ref){var name=_ref.name,props=_ref.props;return function(){for(var _len=arguments.length,args=new Array(_len),_key=0;_key<_len;_key++){args[_key]=arguments[_key]}if(props?!(0,_Type.validateArgs)(props,args):false)throw new TypeError("Invalid number of args passed to constructor ".concat(name));var self={args:args,name:name,props:props,is:function is(otherType){return[otherType,otherType.name].indexOf(name)!==-1},match:function match(pattern){return Type.match(self,pattern)}};return self}};exports.createConstructor=createConstructor;var _default=(0,_utils.createEnumFactory)({createConstructor:createConstructor});exports.default=_default; -------------------------------------------------------------------------------- /build/Type.js: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.default=exports.validateArgs=exports.isOfType=exports.validateRecord=void 0;var _utils=require("./utils");function _toArray(arr){return _arrayWithHoles(arr)||_iterableToArray(arr)||_nonIterableRest()}function _nonIterableRest(){throw new TypeError("Invalid attempt to destructure non-iterable instance")}function _arrayWithHoles(arr){if(Array.isArray(arr))return arr}function _toConsumableArray(arr){return _arrayWithoutHoles(arr)||_iterableToArray(arr)||_nonIterableSpread()}function _nonIterableSpread(){throw new TypeError("Invalid attempt to spread non-iterable instance")}function _iterableToArray(iter){if(Symbol.iterator in Object(iter)||Object.prototype.toString.call(iter)==="[object Arguments]")return Array.from(iter)}function _arrayWithoutHoles(arr){if(Array.isArray(arr)){for(var i=0,arr2=new Array(arr.length);i0?list.length===_toConsumableArray(list).filter(isOfType(innerType)).length:true)};var validateRecord=function validateRecord(shape,obj){return validateArgs((0,_utils.values)(shape),(0,_utils.values)(obj))};exports.validateRecord=validateRecord;var isOfType=function isOfType(type){return function(value){if(typeof type==="string"&&value!==undefined)return true;if(Type.isConstructor(type)){return!!Type.match(type,{Any:function Any(){return true},String:function String(){return typeof value==="string"},Number:function Number(){return typeof value==="number"},Bool:function Bool(){return typeof value==="boolean"},Func:function Func(){return typeof value==="function"},List:function List(innerType){return validateList(innerType,value)},Map:function Map(innerType){return innerType&&(0,_utils.isObject)(value)&&validateList(innerType,(0,_utils.values)(value))},Record:function Record(shape){return(0,_utils.isObject)(value)&&(shape?validateRecord(shape,value):true)},OneOf:function OneOf(typeList){return!!_toConsumableArray(typeList).filter(function(type){return isOfType(type)(value)}).length},Enum:function Enum(InnerType){return value&&InnerType.isConstructor(value)}})}return false}};exports.isOfType=isOfType;var validateArgs=function validateArgs(typeList,valueList){if(typeList.length!==valueList.length)return false;if(typeList.length===0)return true;var _typeList=_toArray(typeList),type=_typeList[0],types=_typeList.slice(1);var _valueList=_toArray(valueList),val=_valueList[0],vals=_valueList.slice(1);if(!isOfType(type)(val))return false;return types.length>0?validateArgs(types,vals):true};exports.validateArgs=validateArgs;var _default=Type;exports.default=_default; -------------------------------------------------------------------------------- /build/utils.js: -------------------------------------------------------------------------------- 1 | "use strict";Object.defineProperty(exports,"__esModule",{value:true});exports.values=exports.isObject=exports.isList=exports.createEnumFactory=exports.prop=exports.Constructor=void 0;function _objectSpread(target){for(var i=1;i EnumType 32 | ``` 33 | 34 | 35 | * `#{SubType}` 36 | 37 | ```javascript 38 | const point = Canvas.Point(20, 25); 39 | const circle = Canvas.Circle(20, 50, 50); 40 | const clearScreen = Canvas.Clear(); 41 | ``` 42 | 43 | 44 | * `#match` 45 | 46 | Pattern matching for the sub-types 47 | ```haskell 48 | match :: (SubType, Object ((...*) -> b)) ~> b 49 | ``` 50 | 51 | ```javascript 52 | const circle = Canvas.Circle(20, 50, 50); 53 | 54 | const result = Canvas.match(circle, { 55 | Circle: (radius, x, y) => drawCircle(radius, x, y), 56 | Point: (x, y) => drawPoint(x, y), 57 | Clear: () => clearCanvasScreen(), 58 | }); 59 | // `result` is the result of the matched operation 60 | ``` 61 | 62 | * `#cata` 63 | 64 | An alternate api for match. The arguments are flipped and curried for a nice, point-free experience. 65 | 66 | ```haskell 67 | cata :: Object ((...*) -> b) ~> SubType -> b 68 | ``` 69 | ```javascript 70 | const draw = Canvas.cata({ 71 | Circle: (radius, x, y) => drawCircle(radius, x, y), 72 | Point: (x, y) => drawPoint(x, y), 73 | Clear: () => clearCanvasScreen(), 74 | }); 75 | 76 | draw(Canvas.Clear()); 77 | draw(Canvas.Circle(20, 50, 50)); 78 | draw(Canvas.Point(20, 25)); 79 | ``` 80 | 81 | 82 | * `#caseOf` 83 | 84 | Alias for cata. Same api as cata. 85 | 86 | ```haskell 87 | caseOf :: Object ((...*) -> b) ~> SubType -> b 88 | ``` 89 | 90 | ```javascript 91 | const draw = Canvas.caseOf({ 92 | Circle: (radius, x, y) => drawCircle(radius, x, y), 93 | Point: (x, y) => drawPoint(x, y), 94 | Clear: () => clearCanvasScreen(), 95 | }); 96 | 97 | draw(Canvas.Clear()); 98 | draw(Canvas.Circle(20, 50, 50)); 99 | draw(Canvas.Point(20, 25)); 100 | ``` 101 | 102 | 103 | * `#reduce` 104 | Alias for cata. Same api as cata. 105 | 106 | ```haskell 107 | reduce :: Object ((...*) -> b) ~> SubType -> b 108 | ``` 109 | 110 | ```javascript 111 | const draw = Canvas.reduce({ 112 | Circle: (radius, x, y) => drawCircle(radius, x, y), 113 | Point: (x, y) => drawPoint(x, y), 114 | Clear: () => clearCanvasScreen(), 115 | }); 116 | 117 | draw(Canvas.Clear()); 118 | draw(Canvas.Circle(20, 50, 50)); 119 | draw(Canvas.Point(20, 25)); 120 | ``` 121 | 122 | [#### Explanation of sum-types](https://medium.com/@phenax5/writing-cleaner-and-safer-javascript-with-sum-types-bec9c68ba7aa) 123 | 124 | [#### Next topic](./type.md) 125 | -------------------------------------------------------------------------------- /docs/getting_started.md: -------------------------------------------------------------------------------- 1 | # Enum-FP 2 | Functional enum type for javascript with pattern matching 3 | 4 | [Read more about SumTypes using EnumFP in this blog post](https://medium.com/@phenax5/writing-cleaner-and-safer-javascript-with-sum-types-bec9c68ba7aa) 5 | 6 | ## Getting started 7 | **To add this package to your project** 8 | ```bash 9 | yarn add enum-fp 10 | ``` 11 | Or if you are one of those npm or pnpm nuts 12 | ```bash 13 | npm i --save enum-fp 14 | ``` 15 | ```bash 16 | pnpm i --save enum-fp 17 | ``` 18 | **Import it to your file** 19 | ```javascript 20 | import Enum from 'enum-fp'; 21 | ``` 22 | 23 | #### Create an enum type 24 | ```js 25 | const Action = Enum([ 'Add', 'Edit', 'Delete', 'Get' ]); 26 | 27 | // Or with a fixed number of arguments 28 | const Maybe = Enum({ 29 | Just: [ 'value' ], 30 | Nothing: [], 31 | }); 32 | ``` 33 | 34 | #### Create an instance of the type using one of the contructors 35 | ```js 36 | const action = Action.Edit(2, 'Hello world'); 37 | ``` 38 | 39 | [Next topic >](./enum_type.md) 40 | -------------------------------------------------------------------------------- /docs/react.md: -------------------------------------------------------------------------------- 1 | # Create a react hook (React > 16.7) [Recommended] 2 | 3 | ## Usage 4 | 5 | ```javascript 6 | // A wrapper for React's useReducer react hook 7 | import { useReducer as useReactReducer } from 'react'; 8 | 9 | export const useReducer = (reducer, initialState) => 10 | useReactReducer((state, action) => reducer(action)(state), initialState); 11 | ``` 12 | 13 | ## API 14 | 15 | ```haskell 16 | useReducer :: (SubType -> State, State) -> [State, SubType -> ()] 17 | ``` 18 | 19 | ## Example usage 20 | 21 | ```javascript 22 | const Action = Enum({ 23 | Increment: ['by'], 24 | Decrement: ['by'], 25 | }); 26 | 27 | const initialState = { count: 0 }; 28 | 29 | const reducer = Action.caseOf({ 30 | Increment: by => ({ count }) => ({ count: count + by }), 31 | Decrement: by => reducer(Action.Increment(-by)), 32 | }); 33 | 34 | const CounterComponent = () => { 35 | const [{ count }, dispatch] = useReducer(reducer, initialState); 36 | return ( 37 |
38 |
{count}
39 | 40 | 41 |
42 | ); 43 | } 44 | ``` 45 | 46 | 47 | # reducerComponent HOC 48 | 49 | ## Usage 50 | 51 | ```javascript 52 | export const reducerComponent = (reducer, state) => Component => 53 | class ReducerComponent extends React.Component { 54 | static displayName = `ReducerComponent(${Component.displayName || Component.name || 'Unknown'})`; 55 | 56 | state = { ...state }; 57 | dispatch = action => this.setState(reducer(action)); 58 | render = () => ; 59 | }; 60 | ``` 61 | 62 | ## API 63 | 64 | ```haskell 65 | reducerComponent :: (State -> State, State) -> Component -> Component 66 | ``` 67 | 68 | The passed component will receive the following additional props 69 | 70 | * `state` The current state of the component 71 | ```haskell 72 | state :: State 73 | ``` 74 | * `dispatch` Dispatch an action. This calls the reducer and sets the next state of the application. 75 | ```haskell 76 | dispatch :: SubType -> () 77 | ``` 78 | 79 | ## Example usage 80 | 81 | ```javascript 82 | const Action = Enum({ 83 | Increment: ['by'], 84 | Decrement: ['by'], 85 | }); 86 | 87 | const initialState = { count: 0 }; 88 | 89 | const reducer = Action.caseOf({ 90 | Increment: by => ({ count }) => ({ count: count + by }), 91 | Decrement: by => reducer(Action.Increment(-by)), 92 | }); 93 | 94 | const CounterComponent = reducerComponent({ state: initialState, reducer })( 95 | ({ state: { count }, dispatch }) => ( 96 |
97 |
{count}
98 | 99 | 100 |
101 | ), 102 | ); 103 | ``` -------------------------------------------------------------------------------- /docs/type.md: -------------------------------------------------------------------------------- 1 | 2 | # Types 3 | The types module can be used directly to validate records, function arguments, lists, single values, etc. 4 | 5 | You can import it as 6 | ```javascript 7 | import T, { validateRecord } from 'enum-fp/types'; 8 | ``` 9 | 10 | The Type module exposes the following - 11 | 12 | * [T](#type-sum-type) 13 | * [isOfType](#isOfType) 14 | * [validateRecord](#validateRecord) 15 | 16 | 17 | ## Type sum-type 18 | ```haskell 19 | data T = Any | String | Number | Bool | Record (Object T) | Map T | List T? | Enum EnumType | OneOf [T]; 20 | ``` 21 | 22 | You can check if a value is of a particular type by using the isOfType function (Refer to section below for more info about isOfType) 23 | 24 | ```javascript 25 | const isString = isOfType(T.String()); 26 | 27 | isString('Hello world') // > true 28 | isString(1) // > false 29 | ``` 30 | 31 | ## isOfType 32 | ```haskell 33 | isOfType :: T -> a -> Boolean 34 | ``` 35 | 36 | You can use this function to write helper functions for validating types 37 | 38 | ```javascript 39 | const isString = isOfType(T.String()); 40 | const isNumber = isOfType(T.Number()); 41 | 42 | const isUser = isOfType(T.Record({ 43 | name: T.String(), 44 | age: T.Number(), 45 | })); 46 | 47 | isUser({ name: 'Akshay Nair', age: 21 }); // > true 48 | isUser({ name: 'Akshay Nair', age: '21' }); // > false 49 | isUser({ name: 'Akshay Nair' }); // > false 50 | ``` 51 | 52 | 53 | ## validateRecord 54 | ```haskell 55 | validateRecord :: (Object T, a) -> Boolean 56 | ``` 57 | 58 | Validates if the passed object is of the specified shape. The check is strict so missing fields in an object means the object is invalid. (Kind of an alternate api for `isOfType(T.Record({ /* shape */ }))`) 59 | 60 | Here's the above isOfType example for user 61 | ```javascript 62 | const isUser = user => validateRecord({ 63 | name: T.String(), 64 | age: T.Number(), 65 | }, user); 66 | ``` 67 | 68 | 69 | [#### Next topic](./react.md) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "enum-fp", 3 | "version": "1.0.2", 4 | "description": "Functional enum type for javascript with simple pattern matching", 5 | "main": "./build/Enum.js", 6 | "repository": "https://github.com/phenax/enum-fp", 7 | "author": "Akshay Nair ", 8 | "license": "MIT", 9 | "keywords": [ 10 | "functional", 11 | "fp", 12 | "enum", 13 | "type", 14 | "js", 15 | "pattern", 16 | "matching", 17 | "switch" 18 | ], 19 | "scripts": { 20 | "build": "babel src --out-dir build --minified --compact --no-comments --delete-dir-on-start", 21 | "watch": "yarn build --watch", 22 | "lint": "eslint ./src", 23 | "test": "jest", 24 | "test:ci": "jest", 25 | "coverage": "jest --coverage", 26 | "coverage:ci": "yarn coverage && codecov" 27 | }, 28 | "babel": { 29 | "presets": [ 30 | "@babel/preset-env" 31 | ], 32 | "plugins": [ 33 | "@babel/plugin-proposal-class-properties" 34 | ] 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.1.0", 38 | "@babel/core": "^7.1.0", 39 | "@babel/plugin-proposal-class-properties": "^7.1.0", 40 | "@babel/preset-env": "^7.1.0", 41 | "babel-core": "^7.0.0-bridge", 42 | "babel-eslint": "^10.0.1", 43 | "babel-jest": "^25.0.0", 44 | "codecov": "^3.1.0", 45 | "eslint": "^5.9.0", 46 | "jest": "^25.0.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Enum.js: -------------------------------------------------------------------------------- 1 | import { createEnumFactory } from './utils'; 2 | import T, { validateArgs } from './Type'; 3 | 4 | // type TypeConstructor = ...a -> EnumTagType 5 | 6 | // createConstructor :: (Enum, ConstructorDescription) -> TypeConstructor 7 | export const createConstructor = (Type, { name, props }) => (...args) => { 8 | if(props ? !validateArgs(props, args) : false) 9 | throw new TypeError(`Invalid number of args passed to constructor ${name}`); 10 | 11 | const self = { 12 | // args :: Array * 13 | args, 14 | // name :: String 15 | name, 16 | // props :: ?Array String 17 | props, 18 | // is :: String | EnumTagType | ConstructorDescription ~> Boolean 19 | is: otherType => [otherType, otherType.name].indexOf(name) !== -1, 20 | // match :: Object (* -> b) ~> b 21 | match: pattern => Type.match(self, pattern), 22 | }; 23 | return self; 24 | }; 25 | 26 | // Enum :: Array String | Object * -> Enum 27 | export default createEnumFactory({ createConstructor }); 28 | 29 | // Type 30 | export { T }; -------------------------------------------------------------------------------- /src/Type.js: -------------------------------------------------------------------------------- 1 | import { values, isList, isObject, createEnumFactory } from './utils'; 2 | 3 | // Tiny ArgLessEnum to bypass the circular dependency shithole 4 | const ArgLessEnum = createEnumFactory({ 5 | createConstructor: (Type, constr) => (...args) => ({ ...constr, args }), 6 | }); 7 | 8 | // type Type = Type|String; 9 | 10 | // Cant use Type to define Type so ArgLessEnum 11 | const Type = ArgLessEnum([ 12 | 'Any', 13 | 'String', 14 | 'Number', 15 | 'Bool', 16 | 17 | 'List', 18 | 'Map', 19 | 'Record', 20 | 21 | 'Func', 22 | 'Enum', 23 | 'OneOf', 24 | ]); 25 | 26 | // validateList :: (Type, [a]) -> Boolean 27 | const validateList = (innerType, list) => 28 | isList(list) && ( 29 | (innerType && list.length > 0) 30 | ? list.length === [...list].filter(isOfType(innerType)).length 31 | : true 32 | ); 33 | 34 | // validateRecord :: Object Type -> Object a -> Boolean 35 | export const validateRecord = (shape, obj) => validateArgs(values(shape), values(obj)); 36 | 37 | // isOfType :: Type -> a -> Boolean 38 | export const isOfType = type => value => { 39 | // Dynamic argument description 40 | if(typeof type === 'string' && value !== undefined) 41 | return true; 42 | 43 | if (Type.isConstructor(type)) { 44 | return !!Type.match(type, { 45 | Any: () => true, 46 | String: () => typeof value === 'string', 47 | Number: () => typeof value === 'number', 48 | Bool: () => typeof value === 'boolean', 49 | Func: () => typeof value === 'function', 50 | 51 | List: innerType => validateList(innerType, value), 52 | Map: innerType => innerType && isObject(value) && validateList(innerType, values(value)), 53 | Record: shape => isObject(value) && (shape ? validateRecord(shape, value) : true), 54 | 55 | OneOf: typeList => !![...typeList].filter(type => isOfType(type)(value)).length, 56 | Enum: InnerType => value && InnerType.isConstructor(value), 57 | }); 58 | } 59 | 60 | return false; 61 | }; 62 | 63 | // validateArgs :: ([Type], [a]) -> Bool 64 | export const validateArgs = (typeList, valueList) => { 65 | if(typeList.length !== valueList.length) return false; 66 | if(typeList.length === 0) return true; 67 | 68 | const [type, ...types] = typeList; 69 | const [val, ...vals] = valueList; 70 | 71 | if(!isOfType(type)(val)) return false; 72 | 73 | return types.length > 0 ? validateArgs(types, vals) : true; 74 | }; 75 | 76 | export default Type; 77 | 78 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | function identity(x) { return x; } 3 | 4 | // data Constructor = { name: String, props: [Type|String] }; 5 | export const Constructor = identity; 6 | 7 | // prop :: Array -> Object 8 | export const prop = (path, defaultVal) => obj => path.reduce( 9 | (o, key) => (o || {}).hasOwnProperty(key) ? o[key] : defaultVal, 10 | obj, 11 | ); 12 | 13 | // normalizeSumType :: Array String | Object [a] -> Constructor 14 | const normalizeSumType = sumType => 15 | isList(sumType) 16 | ? sumType.map(name => Constructor({ name })) 17 | : Object.keys(sumType) 18 | .map(name => Constructor({ name, props: sumType[name] })); 19 | 20 | // match :: EnumTagType -> Pattern -> b 21 | const match = (instance, pattern) => { 22 | if (!instance || !instance.name) throw new Error('Invalid instance passed'); 23 | 24 | const action = pattern[instance.name] || pattern._; 25 | 26 | if(!action) throw new Error('Non-Exhaustive pattern. You must pass fallback case `_` in the pattern'); 27 | 28 | return action(...instance.args); 29 | }; 30 | 31 | // listToObject :: (a -> String, a -> b, [a]) -> Object b 32 | const listToObject = (toKey, toValue, list) => 33 | list.reduce((obj, item) => ({ ...obj, [toKey(item)]: toValue(item) }), {}); 34 | 35 | // createEnumFactory :: Options -> Array String | Object Type -> Enum 36 | export const createEnumFactory = options => sumTypeBody => { 37 | const constructors = normalizeSumType(sumTypeBody); 38 | const { createConstructor } = options; 39 | 40 | const typeNames = constructors.map(prop(['name'])); 41 | 42 | // isConstructor :: String ~> Boolean 43 | const isConstructor = t => !t ? false : 44 | typeNames.indexOf(t) !== -1 || typeNames.indexOf(t.name) !== -1; 45 | 46 | // cata :: Pattern ~> EnumTagType -> b 47 | const cata = pattern => instance => match(instance, pattern); 48 | 49 | let self = { 50 | isConstructor, 51 | match, 52 | cata, 53 | caseOf: cata, 54 | reduce: cata, 55 | constructors: listToObject(prop(['name']), identity, constructors), 56 | length: constructors.length, 57 | forEach: constructors.forEach.bind(constructors), 58 | }; 59 | 60 | return { 61 | // {String} :: TypeConstructor 62 | ...listToObject( 63 | prop(['name']), 64 | constr => createConstructor(self, constr), 65 | constructors, 66 | ), 67 | ...self, 68 | }; 69 | }; 70 | 71 | // isObjectOfType :: String -> a -> Boolean 72 | const isObjectOfType = typeName => a => ({}).toString.call(a) === `[object ${typeName}]`; 73 | 74 | // isList :: * -> Boolean 75 | export const isList = isObjectOfType('Array'); 76 | 77 | // isObject:: * -> Boolean[object 78 | export const isObject = isObjectOfType('Object'); 79 | 80 | // values :: Object a -> [a] 81 | export const values = obj => Object.keys(obj).sort().map(k => obj[k]); 82 | -------------------------------------------------------------------------------- /test/Enum.test.js: -------------------------------------------------------------------------------- 1 | 2 | import Enum from '../src/Enum'; 3 | 4 | describe('Enum', () => { 5 | describe('constructor', () => { 6 | it('should return an instance without errors', () => { 7 | const instance = Enum([ 'Action1', 'Action2' ]); 8 | }); 9 | 10 | it('should create constructors for each of the actions', () => { 11 | const instance = Enum([ 'Action1', 'Action2' ]); 12 | 13 | expect(instance.Action1).toBeInstanceOf(Function); 14 | expect(instance.Action2).toBeInstanceOf(Function); 15 | 16 | instance.Action1(); 17 | instance.Action2(); 18 | }); 19 | 20 | it('should expose `constructors` array to read the exposed constructors', () => { 21 | const instance1 = Enum([ 'Action1', 'Action2' ]); 22 | const instance2 = Enum({ 23 | ActionA: [], 24 | ActionB: ['arg1', 'arg2'], 25 | }); 26 | 27 | expect(instance1.constructors).toEqual({ 28 | Action1: { name: 'Action1' }, 29 | Action2: { name: 'Action2' }, 30 | }); 31 | 32 | expect(instance2.constructors).toEqual({ 33 | ActionA: { name: 'ActionA', props: [] }, 34 | ActionB: { name: 'ActionB', props: ['arg1', 'arg2'] }, 35 | }); 36 | }); 37 | 38 | it('should allow iterating over the constructors', () => { 39 | const instance1 = Enum([ 'Action1', 'Action2' ]); 40 | const instance2 = Enum({ 41 | ActionA: [], 42 | ActionB: ['arg1', 'arg2'], 43 | }); 44 | 45 | expect(instance1.length).toBe(2); 46 | expect(instance2.length).toBe(2); 47 | 48 | instance1.forEach(val => { 49 | expect(val).toBe(instance1.constructors[val.name]); 50 | }); 51 | 52 | instance2.forEach(val => { 53 | expect(val).toBe(instance2.constructors[val.name]); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('#match', () => { 59 | it('should match the correct function and call it', () => { 60 | const Type = Enum([ 'Add', 'Delete' ]); 61 | 62 | const action = Type.Add(); 63 | 64 | const onAdd = jest.fn(() => 'Adding'); 65 | const result = Type.match(action, { 66 | Add: onAdd, 67 | Delete: () => 'Deleting', 68 | _: () => 'Default', 69 | }); 70 | 71 | expect(result).toBe('Adding'); 72 | expect(onAdd).toHaveBeenCalledTimes(1); 73 | }); 74 | 75 | it('should call the default function when the action is not specified', () => { 76 | const Type = Enum([ 'Add', 'Delete' ]); 77 | 78 | const action = Type.Delete(); 79 | 80 | const handleDefault = jest.fn(() => 'Default'); 81 | const result = Type.match(action, { 82 | Add: () => 'Adding', 83 | _: handleDefault, 84 | }); 85 | 86 | expect(result).toBe('Default'); 87 | expect(handleDefault).toHaveBeenCalledTimes(1); 88 | }); 89 | 90 | it('should call the default function when the action is not specified', () => { 91 | const Type = Enum([ 'Add', 'Delete' ]); 92 | 93 | const action = Type.Delete(); 94 | 95 | expect(() => Type.match(action, { 96 | Add: () => 'Adding', 97 | })).toThrowError(); 98 | }); 99 | 100 | it('should match the correct function and call it with the constructor arguements', () => { 101 | const Type = Enum([ 'Add', 'Delete' ]); 102 | 103 | const action = Type.Add('Hello', 'World'); 104 | 105 | const onAdd = jest.fn((str1, str2) => `Adding - ${str1} ${str2}`); 106 | const result = Type.match(action, { 107 | Add: onAdd, 108 | Delete: () => 'Deleting', 109 | _: () => 'Default', 110 | }); 111 | 112 | expect(result).toBe('Adding - Hello World'); 113 | expect(onAdd).toHaveBeenCalledTimes(1); 114 | }); 115 | 116 | it('should throw error when the action is invalid', () => { 117 | const Type = Enum([ 'Add', 'Delete' ]); 118 | 119 | expect(() => Type.match(null, {})).toThrowError(); 120 | }); 121 | 122 | it('should throw error when the pattern is not defined', () => { 123 | const Type = Enum([ 'Add', 'Delete' ]); 124 | const OtherType = Enum([ 'Hello', 'World' ]); 125 | 126 | expect(() => Type.match(OtherType.Hello(), {})).toThrowError(); 127 | }); 128 | 129 | it('should match the correct function and call it with the constructor arguements', () => { 130 | const Type = Enum({ 131 | Add: [ 'id', 'text' ], 132 | Delete: [ 'id' ], 133 | }); 134 | 135 | const pattern = { 136 | Add: jest.fn((id, name) => `Adding - [${id}] ${name}`), 137 | Delete: jest.fn(id => `Deleting - [${id}]`), 138 | }; 139 | const resultOnAdd = Type.match(Type.Add(5, 'Hello World'), pattern); 140 | const resultOnDelete = Type.match(Type.Delete(5), pattern); 141 | 142 | expect(resultOnAdd).toBe('Adding - [5] Hello World'); 143 | expect(pattern.Add).toHaveBeenCalledTimes(1); 144 | expect(resultOnDelete).toBe('Deleting - [5]'); 145 | expect(pattern.Delete).toHaveBeenCalledTimes(1); 146 | }); 147 | 148 | it('should match the correct function and call it with the constructor arguements (for forced 0 arguments)', () => { 149 | const Type = Enum({ 150 | Add: [], 151 | Delete: [], 152 | }); 153 | 154 | const pattern = { 155 | Add: jest.fn(() => 'Adding'), 156 | Delete: jest.fn(() => 'Deleting'), 157 | }; 158 | const resultOnAdd = Type.match(Type.Add(), pattern); 159 | const resultOnDelete = Type.match(Type.Delete(), pattern); 160 | 161 | expect(resultOnAdd).toBe('Adding'); 162 | expect(pattern.Add).toHaveBeenCalledTimes(1); 163 | expect(resultOnDelete).toBe('Deleting'); 164 | expect(pattern.Delete).toHaveBeenCalledTimes(1); 165 | }); 166 | }); 167 | 168 | describe('#caseOf|#cata', () => { 169 | const Type = Enum([ 'Add', 'Delete' ]); 170 | const action = Type.Add(); 171 | 172 | it('(caseOf) should match the correct function and call it', () => { 173 | 174 | const onAdd = jest.fn(() => 'Adding'); 175 | const result = Type.caseOf({ 176 | Add: onAdd, 177 | Delete: () => 'Deleting', 178 | _: () => 'Default', 179 | })(action); 180 | 181 | expect(result).toBe('Adding'); 182 | expect(onAdd).toHaveBeenCalledTimes(1); 183 | }); 184 | it('(cata) should match the correct function and call it', () => { 185 | const onAdd = jest.fn(() => 'Adding'); 186 | const result = Type.cata({ 187 | Add: onAdd, 188 | Delete: () => 'Deleting', 189 | _: () => 'Default', 190 | })(action); 191 | 192 | expect(result).toBe('Adding'); 193 | expect(onAdd).toHaveBeenCalledTimes(1); 194 | }); 195 | }); 196 | 197 | describe('#isConstructor', () => { 198 | it('should return true for the right constructor', () => { 199 | const Type = Enum([ 'Add', 'Delete' ]); 200 | 201 | const action = Type.Add(); 202 | 203 | const onAdd = jest.fn(() => 'Adding'); 204 | const result = Type.caseOf({ 205 | Add: onAdd, 206 | Delete: () => 'Deleting', 207 | _: () => 'Default', 208 | })(action); 209 | 210 | expect(result).toBe('Adding'); 211 | expect(onAdd).toHaveBeenCalledTimes(1); 212 | }); 213 | }); 214 | }); 215 | 216 | -------------------------------------------------------------------------------- /test/Types.test.js: -------------------------------------------------------------------------------- 1 | import Enum from '../src/Enum'; 2 | import T, { isOfType, validateArgs, validateRecord } from '../src/Type'; 3 | 4 | describe('Types', () => { 5 | 6 | describe('Create types', () => { 7 | it('should allow to create instances using all constructors', () => { 8 | expect(T.Any().name).toBe('Any'); 9 | expect(T.String().name).toBe('String'); 10 | expect(T.Number().name).toBe('Number'); 11 | 12 | expect(T.List().name).toBe('List'); 13 | expect(T.Map().name).toBe('Map'); 14 | expect(T.Record().name).toBe('Record'); 15 | 16 | expect(T.Enum().name).toBe('Enum'); 17 | expect(T.OneOf().name).toBe('OneOf'); 18 | }); 19 | }); 20 | 21 | describe('isOfType', () => { 22 | 23 | describe('Basic types', () => { 24 | it('should match ANY', () => { 25 | expect(isOfType(T.Any())('Hello world')).toBe(true); 26 | expect(isOfType(T.Any())(5)).toBe(true); 27 | expect(isOfType(T.Any())(undefined)).toBe(true); 28 | expect(isOfType(T.Any())(null)).toBe(true); 29 | expect(isOfType(T.Any())(NaN)).toBe(true); 30 | expect(isOfType(T.Any())(Infinity)).toBe(true); 31 | expect(isOfType(T.Any())({})).toBe(true); 32 | expect(isOfType(T.Any())([])).toBe(true); 33 | }); 34 | 35 | it('should match string', () => { 36 | expect(isOfType(T.String())('Hello world')).toBe(true); 37 | expect(isOfType(T.String())(5)).toBe(false); 38 | expect(isOfType(T.String())(undefined)).toBe(false); 39 | expect(isOfType(T.String())(null)).toBe(false); 40 | expect(isOfType(T.String())(NaN)).toBe(false); 41 | expect(isOfType(T.String())(Infinity)).toBe(false); 42 | expect(isOfType(T.String())({})).toBe(false); 43 | expect(isOfType(T.String())([])).toBe(false); 44 | }); 45 | 46 | it('should match number', () => { 47 | expect(isOfType(T.Number())('Hello world')).toBe(false); 48 | expect(isOfType(T.Number())(5)).toBe(true); 49 | expect(isOfType(T.Number())(undefined)).toBe(false); 50 | expect(isOfType(T.Number())(null)).toBe(false); 51 | expect(isOfType(T.Number())(NaN)).toBe(true); 52 | expect(isOfType(T.Number())(Infinity)).toBe(true); 53 | expect(isOfType(T.Number())({})).toBe(false); 54 | expect(isOfType(T.Number())([])).toBe(false); 55 | }); 56 | 57 | it('should match boolean', () => { 58 | expect(isOfType(T.Bool())(true)).toBe(true); 59 | expect(isOfType(T.Bool())(false)).toBe(true); 60 | expect(isOfType(T.Bool())(5)).toBe(false); 61 | expect(isOfType(T.Bool())(undefined)).toBe(false); 62 | expect(isOfType(T.Bool())(null)).toBe(false); 63 | expect(isOfType(T.Bool())(NaN)).toBe(false); 64 | expect(isOfType(T.Bool())(Infinity)).toBe(false); 65 | expect(isOfType(T.Bool())({})).toBe(false); 66 | expect(isOfType(T.Bool())([])).toBe(false); 67 | }); 68 | 69 | it('should match function', () => { 70 | expect(isOfType(T.Func())(() => null)).toBe(true); 71 | expect(isOfType(T.Func())(function() {})).toBe(true); 72 | expect(isOfType(T.Func())(class {})).toBe(true); 73 | expect(isOfType(T.Func())(true)).toBe(false); 74 | expect(isOfType(T.Func())(false)).toBe(false); 75 | expect(isOfType(T.Func())(5)).toBe(false); 76 | expect(isOfType(T.Func())(undefined)).toBe(false); 77 | expect(isOfType(T.Func())(null)).toBe(false); 78 | expect(isOfType(T.Func())(NaN)).toBe(false); 79 | expect(isOfType(T.Func())(Infinity)).toBe(false); 80 | expect(isOfType(T.Func())({})).toBe(false); 81 | expect(isOfType(T.Func())([])).toBe(false); 82 | }); 83 | }); 84 | 85 | describe('Record types', () => { 86 | it('should match List', () => { 87 | expect(isOfType(T.List())([])).toBe(true); 88 | expect(isOfType(T.List())([null])).toBe(true); 89 | expect(isOfType(T.List())([undefined])).toBe(true); 90 | expect(isOfType(T.List())(['wow'])).toBe(true); 91 | expect(isOfType(T.List())([1, 'wow'])).toBe(true); 92 | 93 | expect(isOfType(T.List(T.String()))([])).toBe(true); 94 | expect(isOfType(T.List(T.String()))(['Hello'])).toBe(true); 95 | expect(isOfType(T.List(T.String()))([1, 'HEllo'])).toBe(false); 96 | expect(isOfType(T.List(T.String()))([[]])).toBe(false); 97 | expect(isOfType(T.List(T.String()))([null])).toBe(false); 98 | expect(isOfType(T.List(T.String()))([undefined])).toBe(false); 99 | 100 | expect(isOfType(T.List())(null)).toBe(false); 101 | expect(isOfType(T.List())(undefined)).toBe(false); 102 | expect(isOfType(T.List())('Hello')).toBe(false); 103 | expect(isOfType(T.List())(23)).toBe(false); 104 | expect(isOfType(T.List())({})).toBe(false); 105 | }); 106 | 107 | it('should match Map', () => { 108 | expect(isOfType(T.Map(T.String()))({})).toBe(true); 109 | expect(isOfType(T.Map(T.String()))({ v: 'Hello' })).toBe(true); 110 | expect(isOfType(T.Map(T.String()))({ v: 1 })).toBe(false); 111 | expect(isOfType(T.Map(T.String()))({ a: '4', b: 3 })).toBe(false); 112 | expect(isOfType(T.Map(T.String()))([{ a: undefined }])).toBe(false); 113 | expect(isOfType(T.Map(T.String()))([{ a: null }])).toBe(false); 114 | 115 | // No innerType will return false for all values 116 | expect(isOfType(T.Map())(null)).toBe(false); 117 | expect(isOfType(T.Map())(undefined)).toBe(false); 118 | expect(isOfType(T.Map())('Hello')).toBe(false); 119 | expect(isOfType(T.Map())(23)).toBe(false); 120 | expect(isOfType(T.Map())({})).toBe(false); 121 | }); 122 | 123 | it('should match Record', () => { 124 | expect(isOfType(T.Record())([])).toBe(false); 125 | expect(isOfType(T.Record())({})).toBe(true); 126 | expect(isOfType(T.Record())(null)).toBe(false); 127 | expect(isOfType(T.Record())(undefined)).toBe(false); 128 | expect(isOfType(T.Record())('Hello')).toBe(false); 129 | expect(isOfType(T.Record())(23)).toBe(false); 130 | 131 | const shape = { 132 | name: T.String(), 133 | age: T.Number(), 134 | }; 135 | expect(isOfType(T.Record(shape))({})).toBe(false); 136 | expect(isOfType(T.Record(shape))([])).toBe(false); 137 | expect(isOfType(T.Record(shape))({ 138 | name: 'Hello world', 139 | age: 50, 140 | })).toBe(true); 141 | expect(isOfType(T.Record(shape))({ 142 | name: 'Hello world', 143 | age: '50', 144 | })).toBe(false); 145 | expect(isOfType(T.Record(shape))({ 146 | name: {}, 147 | age: '50', 148 | })).toBe(false); 149 | expect(isOfType(T.Record(shape))({ name: 'Hllo world' })).toBe(false); 150 | }); 151 | }); 152 | 153 | describe('Compound types', () => { 154 | it('should match OneOf', () => { 155 | const numOrStr = T.OneOf([ T.String(), T.Number() ]); 156 | expect(isOfType(numOrStr)(5)).toBe(true); 157 | expect(isOfType(numOrStr)('Hello world')).toBe(true); 158 | expect(isOfType(numOrStr)([1, 'HEllo'])).toBe(false); 159 | expect(isOfType(numOrStr)({})).toBe(false); 160 | expect(isOfType(numOrStr)(null)).toBe(false); 161 | expect(isOfType(numOrStr)(undefined)).toBe(false); 162 | }); 163 | 164 | it('should match Enum', () => { 165 | const Maybe = Enum([ 'Just', 'Nothing' ]); 166 | const maybeType = T.Enum(Maybe); 167 | 168 | expect(isOfType(maybeType)(Maybe.Just(5))).toBe(true); 169 | expect(isOfType(maybeType)(Maybe.Just())).toBe(true); 170 | expect(isOfType(maybeType)(Maybe.Nothing())).toBe(true); 171 | 172 | expect(isOfType(maybeType)('Hello world')).toBe(false); 173 | expect(isOfType(maybeType)(1)).toBe(false); 174 | expect(isOfType(maybeType)([1, 'HEllo'])).toBe(false); 175 | expect(isOfType(maybeType)([])).toBe(false); 176 | expect(isOfType(maybeType)({})).toBe(false); 177 | expect(isOfType(maybeType)(NaN)).toBe(false); 178 | expect(isOfType(maybeType)(null)).toBe(false); 179 | expect(isOfType(maybeType)(undefined)).toBe(false); 180 | }); 181 | }); 182 | }); 183 | 184 | describe('validateArgs', () => { 185 | it('should validate list', () => { 186 | const props = [ T.String(), T.Number(), T.Bool(), T.Any() ]; 187 | expect(validateArgs(props, ['2', 3, true, 5])).toBe(true); 188 | expect(validateArgs(props, ['2', 3, false, '5'])).toBe(true); 189 | expect(validateArgs(props, ['2', 3, 5, '5'])).toBe(false); 190 | expect(validateArgs(props, [2, '4', false, '5'])).toBe(false); 191 | expect(validateArgs(props, ['2', null, false, '5'])).toBe(false); 192 | 193 | expect(validateArgs(props, ['2', 3, true])).toBe(false); 194 | expect(validateArgs(props, ['2', 3, true, new RegExp()])).toBe(true); 195 | }); 196 | }); 197 | 198 | describe('validateRecord', () => { 199 | it('should validate any shape (nested shape)', () => { 200 | const UserShape = { 201 | name: T.String(), 202 | age: T.Number(), 203 | data: 'someData', 204 | dob: T.Record({ 205 | date: T.Number(), 206 | month: T.Number(), 207 | year: T.Number(), 208 | }), 209 | }; 210 | 211 | expect(validateRecord(UserShape, { 212 | name: 'Akshay Nair', 213 | age: 21, 214 | data: 'fire', 215 | dob: { 216 | date: 1, 217 | month: 5, 218 | year: 1997, 219 | }, 220 | })).toBe(true); 221 | expect(validateRecord(UserShape, { 222 | name: 'Akshay Nair', 223 | age: '21', 224 | data: 5, 225 | dob: { 226 | date: 1, 227 | month: 5, 228 | year: 1997, 229 | }, 230 | })).toBe(false); 231 | expect(validateRecord(UserShape, { 232 | name: 'Akshay Nair', 233 | age: 21, 234 | data: 5, 235 | })).toBe(false); 236 | expect(validateRecord(UserShape, { 237 | name: 'Akshay Nair', 238 | age: 21, 239 | data: 'fire', 240 | dob: { 241 | date: 1, 242 | month: 5, 243 | }, 244 | })).toBe(false); 245 | }); 246 | 247 | it('should validate any shape (with list)', () => { 248 | const UserShape = { 249 | name: T.String(), 250 | age: T.Number(), 251 | data: 'someData', 252 | comments: T.List(T.Record({ 253 | message: T.String(), 254 | date: T.String(), 255 | })), 256 | }; 257 | 258 | expect(validateRecord(UserShape, { 259 | name: 'Akshay Nair', 260 | age: 21, 261 | data: 'fire', 262 | comments: [], 263 | })).toBe(true); 264 | expect(validateRecord(UserShape, { 265 | name: 'Akshay Nair', 266 | age: 21, 267 | data: 'fire', 268 | comments: [ 269 | { message: 'Helo world', date: 'today' }, 270 | ], 271 | })).toBe(true); 272 | expect(validateRecord(UserShape, { 273 | name: 'Akshay Nair', 274 | age: 21, 275 | data: 'fire', 276 | comments: [ 277 | { message: 'Helo world', date: 5 }, 278 | ], 279 | })).toBe(false); 280 | expect(validateRecord(UserShape, { 281 | name: 'Akshay Nair', 282 | age: 21, 283 | data: 'fire', 284 | comments: [ 285 | { message: 'Helo world', date: 'today' }, 286 | { message: 'Helo world', date: 'today' }, 287 | { message: 'Helo world', date: 5 }, 288 | ], 289 | })).toBe(false); 290 | expect(validateRecord(UserShape, { 291 | name: 'Akshay Nair', 292 | age: 21, 293 | data: 'fire', 294 | comments: [ 295 | { message: 'Helo world' }, 296 | ], 297 | })).toBe(false); 298 | expect(validateRecord(UserShape, { 299 | name: 'Akshay Nair', 300 | age: 21, 301 | data: 'fire', 302 | comments: [ 303 | 20, 304 | ], 305 | })).toBe(false); 306 | }); 307 | }); 308 | }); 309 | -------------------------------------------------------------------------------- /test/createConstructor.test.js: -------------------------------------------------------------------------------- 1 | 2 | import Enum from '../src/Enum'; 3 | import { createConstructor } from '../src/Enum'; 4 | import { Constructor as Constr } from '../src/utils'; 5 | 6 | const TestType = Enum([ 'Type', 'TypeWithArgs', 'Tag', 'NewTag' ]); 7 | 8 | describe('createConstructor', () => { 9 | 10 | describe('#constructor', () => { 11 | 12 | it('should have name, props and args', () => { 13 | const Tag = createConstructor(TestType, Constr({ name: 'Type' })); 14 | const TagWithArgs = createConstructor(TestType, Constr({ 15 | name: 'TypeWithArgs', 16 | props: [ 'id', 'message' ], 17 | })); 18 | 19 | const tag = Tag(); 20 | const tagWithArgs = TagWithArgs(5, 'Hello world'); 21 | 22 | expect(tag.name).toBe('Type'); 23 | expect(tag.props).toBeFalsy(); 24 | expect(tag.args).toHaveLength(0); 25 | 26 | expect(tagWithArgs.name).toBe('TypeWithArgs'); 27 | expect(tagWithArgs.props).toHaveLength(2); 28 | expect(tagWithArgs.args).toHaveLength(2); 29 | }); 30 | 31 | it('should throw error if there is a mismatch in the props and arguements length', () => { 32 | const Tag = createConstructor(TestType, Constr({ 33 | name: 'Type', 34 | props: [ 'a', 'b' ], 35 | })); 36 | 37 | expect(() => Tag(1, 2)).not.toThrowError(); 38 | expect(() => Tag()).toThrowError(); 39 | expect(() => Tag(1)).toThrowError(); 40 | expect(() => Tag(1, 2, 3)).toThrowError(); 41 | }); 42 | 43 | // it('should validate types and throw error for invalid ones (basic types)', () => { 44 | // const Tag = createConstructor(TestType, Constr({ 45 | // name: 'Type', 46 | // props: [ T.Number(), T.String() ], 47 | // })); 48 | 49 | // expect(() => Tag(0, '')).toThrowError(); 50 | // expect(() => Tag(1, 'Helo world')).toThrowError(); 51 | // expect(() => Tag(1, 2)).not.toThrowError(); 52 | // expect(() => Tag()).toThrowError(); 53 | // expect(() => Tag(1)).toThrowError(); 54 | // expect(() => Tag(1, 2, 3)).toThrowError(); 55 | // }); 56 | }); 57 | 58 | describe('#is', () => { 59 | 60 | it('should return true for equivalent tokens and false otherwise', () => { 61 | 62 | const Tag = createConstructor(TestType, Constr({ name: 'Tag' })); 63 | const Tag1 = createConstructor(TestType, Constr({ name: 'Tag' })); 64 | const Tag2 = createConstructor(TestType, Constr({ name: 'NewTag' })); 65 | 66 | expect(Tag().is(Tag1())).toBeTruthy(); 67 | expect(Tag().is(Tag2())).not.toBeTruthy(); 68 | }); 69 | }); 70 | 71 | describe('#match', () => { 72 | 73 | it('should match the correct function and call it', () => { 74 | const Type = Enum([ 'Add', 'Delete' ]); 75 | 76 | const action = Type.Add(); 77 | 78 | const onAdd = jest.fn(() => 'Adding'); 79 | const result = action.match({ 80 | Add: onAdd, 81 | Delete: () => 'Deleting', 82 | _: () => 'Default', 83 | }); 84 | 85 | expect(result).toBe('Adding'); 86 | expect(onAdd).toHaveBeenCalledTimes(1); 87 | }); 88 | 89 | it('should call the default function when the action is not specified', () => { 90 | const Type = Enum([ 'Add', 'Delete' ]); 91 | 92 | const action = Type.Delete(); 93 | 94 | const handleDefault = jest.fn(() => 'Default'); 95 | const result = action.match({ 96 | Add: () => 'Adding', 97 | _: handleDefault, 98 | }); 99 | 100 | expect(result).toBe('Default'); 101 | expect(handleDefault).toHaveBeenCalledTimes(1); 102 | }); 103 | 104 | it('should call the default function when the action is not specified', () => { 105 | const Type = Enum([ 'Add', 'Delete' ]); 106 | 107 | const action = Type.Delete(); 108 | 109 | expect(() => action.match({ 110 | Add: () => 'Adding', 111 | })).toThrowError(); 112 | }); 113 | 114 | it('should match the correct function and call it with the constructor arguements', () => { 115 | const Type = Enum([ 'Add', 'Delete' ]); 116 | 117 | const action = Type.Add('Hello', 'World'); 118 | 119 | const onAdd = jest.fn((str1, str2) => `Adding - ${str1} ${str2}`); 120 | const result = action.match({ 121 | Add: onAdd, 122 | Delete: () => 'Deleting', 123 | _: () => 'Default', 124 | }); 125 | 126 | expect(result).toBe('Adding - Hello World'); 127 | expect(onAdd).toHaveBeenCalledTimes(1); 128 | }); 129 | 130 | it('should match the correct function and call it with the constructor arguements', () => { 131 | const Type = Enum({ 132 | Add: [ 'id', 'text' ], 133 | Delete: [ 'id' ], 134 | }); 135 | 136 | const pattern = { 137 | Add: jest.fn((id, name) => `Adding - [${id}] ${name}`), 138 | Delete: jest.fn(id => `Deleting - [${id}]`), 139 | }; 140 | const resultOnAdd = Type.Add(5, 'Hello World').match(pattern); 141 | const resultOnDelete = Type.Delete(5).match(pattern); 142 | 143 | expect(resultOnAdd).toBe('Adding - [5] Hello World'); 144 | expect(pattern.Add).toHaveBeenCalledTimes(1); 145 | expect(resultOnDelete).toBe('Deleting - [5]'); 146 | expect(pattern.Delete).toHaveBeenCalledTimes(1); 147 | }); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /test/utils.test.js: -------------------------------------------------------------------------------- 1 | 2 | import Enum from '../src/Enum'; 3 | import { prop } from '../src/utils'; 4 | 5 | const TestType = Enum([ 'Action1', 'Action2', 'Action3' ]); 6 | 7 | describe('utils', () => { 8 | 9 | describe('prop', () => { 10 | 11 | it('should return property value', () => { 12 | 13 | const obj = { a: 'b', c: 'd' }; 14 | 15 | expect(prop(['a'])(obj)).toBe('b'); 16 | expect(prop(['c'])(obj)).toBe('d'); 17 | expect(prop(['unknown'])(obj)).toBeUndefined(); 18 | }); 19 | 20 | it('should return property value for nested object', () => { 21 | const obj = { a: { b: { c: 'd' } } }; 22 | 23 | expect(prop(['a', 'b'])(obj)).toEqual({ c: 'd' }); 24 | expect(prop(['a', 'b', 'c'])(obj)).toBe('d'); 25 | }); 26 | 27 | it('should return default value for non-existant properties', () => { 28 | const obj = { a: { b: 'c' } }; 29 | 30 | expect(prop(['a', 'b'])(obj)).toBe('c'); 31 | expect(prop(['a', 'some', 'unknown', 'territory'], 'UNKNOWN')(obj)).toBe('UNKNOWN'); 32 | expect(prop(['prop','here','non'], 'NOPE')()).toBe('NOPE'); 33 | expect(prop(['prop','here','non'], 'NOPE')(null)).toBe('NOPE'); 34 | expect(prop(['prop','here','non'], 'NOPE')(0)).toBe('NOPE'); 35 | expect(prop(['prop','here','non'], 'NOPE')(6)).toBe('NOPE'); 36 | expect(prop(['prop','here','non'], 'NOPE')('Wow')).toBe('NOPE'); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /types.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./build/Type'); --------------------------------------------------------------------------------