diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41288f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules +.DS_Store +npm-debug* +yarn-error* +package-lock.json +built diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..2f4aea6 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +sudo: required + +language: node_js + +node_js: + - "9" + +before_script: + - npm install + +script: + - npm run test \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..b78f062 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# Inquiry Monad +### v0.1.0 + +Experiment with aggregate Left/Right monad running parallel. More details when it is better fleshed out. \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..075027c --- /dev/null +++ b/package.json @@ -0,0 +1,28 @@ +{ + "name": "inquiry-monad", + "version": "0.1.0", + "description": "", + "main": "built/index.js", + "scripts": { + "test": "find ./built -name '*.spec.js' | NODE_ENV=test xargs node ./node_modules/jasmine/bin/jasmine.js", + "build": "node ./node_modules/typescript/bin/tsc", + "postinstall": "npm run build" + }, + "keywords": [ + "monad", + "inquiry", + "validation", + "left", + "right" + ], + "author": "Rob Porter ", + "license": "MIT", + "devDependencies": { + "@types/jasmine": "2.8.6", + "@types/ramda": "0.25.24", + "jasmine": "3.1.0", + "ramda": "0.25.0", + "ts-node": "6.0.0", + "typescript": "2.8.3" + } +} diff --git a/src/index.spec.ts b/src/index.spec.ts new file mode 100644 index 0000000..c1eaae7 --- /dev/null +++ b/src/index.spec.ts @@ -0,0 +1,41 @@ +import { Inquiry, Fail, Pass } from "./index"; +import * as R from "ramda"; +import "jasmine"; + +const oldEnough = (a: any) => + a.age > 13 ? Pass(["old enough"]) : Fail(["not old enough"]); +const findHeight = () => Pass([{ height: 110, in: "cm" }]); +const nameSpelledRight = (a: any) => + a.name === "Ron" + ? Pass("Spelled correctly") + : Fail(["Name wasn't spelled correctly"]); +const hasRecords = () => Pass([{ records: [1, 2, 3] }]); +const mathGrade = () => Fail(["Failed at math"]); + +describe("The module", () => { + it("should be able to make many checks", () => { + const result = (Inquiry as any) + .of({ name: "test", age: 10, description: "blah" }) + .inquire(oldEnough) + .inquire(findHeight) + .inquire(nameSpelledRight) + .inquire(hasRecords) + .inquire(mathGrade) + .fork( + (x: FailMonad) => { + expect(x.inspect()).toBe( + "Fail(not old enough,Name wasn't spelled correctly,Failed at math)" + ); + return x.join(); + }, + (y: PassMonad) => { + expect(y.inspect()).toBe( + "Pass([object Object],[object Object])" + ); + return y.join(); + } + ); + + expect(result.pass[0].height).toBe(110); + }); +}); diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..d02aff5 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,68 @@ +import * as R from "ramda"; + +const Pass = (x: any): PassMonad => ({ + map: (f: Function) => Pass(f(x)), + chain: (f: Function) => f(x), + fold: (f: Function, _: Function) => f(x), + fork: (_: Function, f: Function) => f(x), + join: () => x, + inspect: () => `Pass(${x})`, + concat: (o: PassFailMonad) => o.fold((r: any) => Pass(x.concat(r)), null), + ap: (y: PassFailMonad) => (y.isPass ? y.concat(Pass(x)) : Pass(x)), + isPass: true, + isFail: false, + isInquiry: true +}); + +const Fail = (x: any): FailMonad => ({ + map: (f: Function) => Fail(f(x)), + chain: (f: Function) => f(x), + fold: (_: Function, f: Function) => f(x), + fork: (f: Function, _: Function) => f(x), + join: () => x, + inspect: () => `Fail(${x})`, + concat: (o: PassFailMonad) => o.fork((r: any) => Fail(x.concat(r)), null), + ap: (y: PassFailMonad) => (y.isPass ? Fail(x) : y.concat(Fail(x))), + isPass: false, + isFail: true, + isInquiry: true +}); + +const _failInquire = (x: Inquiry, y: FailMonad) => + Inquiry({ fail: x.fail.concat(y), pass: x.pass }); +const _passInquire = (x: Inquiry, y: PassMonad) => + Inquiry({ fail: x.fail, pass: x.pass.concat(y) }); + +const Inquiry = (x: Inquiry) => ({ + isInquiry: true, + inquire: ( + f: Function // @todo memoize or something + ) => (f(x).isPass ? _passInquire(x, f(x)) : _failInquire(x, f(x))), + swap: (): InquiryMonad => + Inquiry({ + fail: Fail(x.pass.join()), + pass: Pass(x.fail.join()) + }), + inspect: (): string => `Inquiry(${x.fail.inspect()} ${x.pass.inspect()}`, + map: (f: Function): Inquiry => (Inquiry as any).of(f(x)), // cast required for now + ap: (y: Monad) => y.map(x), + chain: (f: Function) => f(x), + + // unwraps : Fork and Fold may need renaming as they don't act as you'd expect. + // because they will run BOTH tracks + fold: (f: Function, g: Function): Inquiry => ({ + pass: f(x.pass), + fail: g(x.fail) + }), + fork: (f: Function, g: Function): Inquiry => ({ + fail: f(x.fail), + pass: g(x.pass) + }), + zip: (f: Function): Array => f(x.fail.join().concat(x.pass.join())), // bring together + join: (): any => x +}); + +Inquiry.constructor.prototype["of"] = (x: any) => + R.prop("isInquiry", x) ? x : Inquiry({ fail: Fail([]), pass: Pass([]) }); + +export {Inquiry, Fail, Pass}; \ No newline at end of file diff --git a/src/types.d.ts b/src/types.d.ts new file mode 100644 index 0000000..c6a23c8 --- /dev/null +++ b/src/types.d.ts @@ -0,0 +1,40 @@ +interface Monad { + map: Function; + chain: Function; + join: Function; + inspect(): string; + ap: Function; +} + +interface PassFailMonad extends Monad { + fold: Function; + fork: Function; + concat: Function; + isPass: boolean; + isFail: boolean; + isInquiry: true; +} + +interface PassMonad extends PassFailMonad { + isPass: true; + isFail: false; +} + +interface FailMonad extends PassFailMonad { + isPass: false; + isFail: true; +} + +interface Inquiry { + fail: FailMonad; + pass: PassMonad; + of?: Function; +} + +interface InquiryMonad extends Monad { + inquire: Function; + zip: Function; + swap: Function; + fold: Function; + fork: Function; +} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a43b4df --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,60 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": + "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, + "module": + "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./built" /* Redirect output structure to the directory. */, + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true /* Enable all strict type-checking options. */, + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + "strictNullChecks": true /* Enable strict null checks. */, + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + }, + "include": ["./src/**/*"] +}