Skip to content

Commit

Permalink
Merge pull request #17 from SimonHarmonicMinor/feature/#14-implement-…
Browse files Browse the repository at this point in the history
…try-monad

Feature/#14 implement try monad
  • Loading branch information
SimonHarmonicMinor authored Jul 12, 2021
2 parents 246f209 + a85851a commit b5ef0f0
Show file tree
Hide file tree
Showing 10 changed files with 209 additions and 16 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ branches:
- master

script:
- sonar-scanner
- npm run build
- sonar-scanner

deploy:
- provider: script
Expand Down
3 changes: 2 additions & 1 deletion cleanDist.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires,no-undef
const fs = require('fs');
fs.rmdirSync('dist', {recursive: true});
fs.rmdirSync('dist', { recursive: true });
fs.rmdirSync('coverage', { recursive: true });
2 changes: 1 addition & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module.exports = {
},
collectCoverage: true,
collectCoverageFrom: [
'**/*.{js,jsx}',
'**/*.{js,jsx,ts,tsx}',
'!**/node_modules/**',
'!**/vendor/**',
'!**/dist/**',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"clean": "node ./cleanDist.js",
"lint": "eslint ./src --ext .ts,.tsx,.js,.jsx --max-warnings=0",
"test": "jest",
"prebuild": "run-s clean lint",
"prebuild": "run-s clean lint test",
"build": "tsc --build tsconfig.json",
"semantic-release": "semantic-release",
"prepare": "husky install"
Expand Down
6 changes: 2 additions & 4 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
sonar.projectKey=SimonHarmonicMinor_try-monad
sonar.organization=simonharmonicminor
sonar.projectName=try-monad
sonar.typescript.lcov.reportPaths=coverage/lcov.info
sonar.sources=src
sonar.sourceEncoding=UTF-8
sonar.coverage.exclusions=**/*.test.*,**/index.ts,**/index.tsx
sonar.javascript.lcov.reportPaths=coverage/lcov.info
sonar.sourceEncoding=UTF-8
7 changes: 0 additions & 7 deletions src/__tests__/index.test.ts

This file was deleted.

119 changes: 119 additions & 0 deletions src/__tests__/try.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import Try from '../index';

describe('"Try" monad test suites', () => {
it('"success" should return successfully evaluated monad', () => {
expect(Try.success(1).orElseThrow()).toEqual(1);
});

it('"error" should return failure monad', () => {
expect(() => Try.error().orElseThrow()).toThrowError();
});

it('Should be evaluated lazily until terminal operation call', () => {
const mockFunction = jest.fn();
const tryMonad = Try.of(() => 1)
.map((v) => {
mockFunction();
return v + 1;
})
.map((v) => {
mockFunction();
return v.toString();
});
expect(mockFunction).toHaveBeenCalledTimes(0);
expect(tryMonad.orElse('')).not.toBeNull();
expect(mockFunction).toHaveBeenCalledTimes(2);
});

it('Should map the input and return the result value', () => {
const tryMonad = Try.of(() => 1)
.map((v) => v + 200)
.map((v) => v.toString())
.map((v) => v + 'ty');
expect(tryMonad.orElse('wrong value')).toEqual('201ty');
});

it('Should map the input and return the default value', () => {
const tryMonad = Try.of(() => 1)
.map((v) => v + 200)
.map((v) => v.toString())
.map((v) => v + 'ty')
.map<string>(() => {
throw new Error('error');
})
.map((v) => v.length);
expect(tryMonad.orElse(-10)).toEqual(-10);
});

it('Should flat mat the input and return the result value', () => {
const tryMonad = Try.of(() => 1)
.flatMap((v) => Try.success(v + 1))
.flatMap((v) => Try.success(v.toString() + 'ui'));
expect(tryMonad.orElse('wrong value')).toEqual('2ui');
});

it('Should flat mat the input and return the default value', () => {
const tryMonad = Try.of(() => 1)
.flatMap((v) => Try.success(v + 1))
.flatMap<string>(() => Try.error())
.flatMap((v) => Try.success(v.toString() + 'ui'));
expect(tryMonad.orElse('wrong value')).toEqual('wrong value');
});

it('Should filter the value and return the result', () => {
const tryMonad = Try.of(() => 'str')
.filter((value) => value.length > 0)
.filter((value) => value.includes('st'));
expect(tryMonad.orElse('wrong value')).toEqual('str');
});

it('Should filter the value and return the default one', () => {
const tryMonad = Try.of(() => 'str')
.filter((value) => value.length === 5)
.filter((value) => value.includes('st'));
expect(tryMonad.orElse('wrong value')).toEqual('wrong value');
});

it('Should return the first "Try" if it succeeds', () => {
const tryMonad = Try.of(() => 1)
.map((value) => value + 600)
.orElseTry(() => -1);
expect(tryMonad.orElse(-2)).toEqual(601);
});

it('Should return the last "Try" if the first one fails', () => {
const tryMonad = Try.of(() => 1)
.map<number>(() => {
throw new Error('error');
})
.orElseTry(() => -1);
expect(tryMonad.orElse(-2)).toEqual(-1);
});

it('Should provide the error that led to the bug', () => {
const bug = new Error('the error that led to the bug');
const tryMonad = Try.of(() => 22)
.map((value) => value + 67)
.map<string>(() => {
throw bug;
});
expect(
tryMonad.orElseGet((error) => {
expect(error).toStrictEqual(bug);
return 'default string';
})
).toEqual('default string');
});

it('Should throw the provided error if "Try" fails', () => {
const tryMonad = Try.of(() => 22)
.map((value) => value + 67)
.map<string>(() => {
throw new Error('error');
});
const errorToThrow = new Error('error to throw');
expect(() => tryMonad.orElseThrow(() => errorToThrow)).toThrowError(
errorToThrow
);
});
});
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
export default class Try {}
import { Try } from './try';

export default Try;
79 changes: 79 additions & 0 deletions src/try.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
export class Try<T> {
private readonly supplier: () => T;

private constructor(supplier: () => T) {
this.supplier = supplier;
}

public static of<T>(supplier: () => T): Try<T> {
return new Try<T>(supplier);
}

public static success<T>(value: T): Try<T> {
return new Try<T>(() => value);
}

public static error<T>(exception?: Error): Try<T> {
return new Try<T>(() => {
if (exception) {
throw exception;
}
throw new Error('Try is empty');
});
}

public map<U>(mapper: (value: T) => U): Try<U> {
return new Try<U>(() => mapper(this.supplier()));
}

public flatMap<U>(mapper: (value: T) => Try<U>): Try<U> {
return new Try<U>(() => mapper(this.supplier()).orElseThrow());
}

public filter(predicate: (value: T) => boolean): Try<T> {
return new Try<T>(() => {
const value = this.supplier();
if (predicate(value)) {
return value;
}
throw new Error('Predicate returned false');
});
}

public orElseTry(elseCondition: () => T): Try<T> {
return new Try<T>(() => {
try {
return this.supplier();
} catch {
return elseCondition();
}
});
}

public orElse(value: T): T {
try {
return this.supplier();
} catch {
return value;
}
}

public orElseGet(elseValueSupplier: (error: unknown) => T): T {
try {
return this.supplier();
} catch (error) {
return elseValueSupplier(error);
}
}

public orElseThrow(errorSupplier?: () => Error): T {
try {
return this.supplier();
} catch (error) {
if (errorSupplier) {
throw errorSupplier();
}
throw error;
}
}
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compilerOptions": {
"target": "es5",
"inlineSourceMap": true,
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
Expand Down

0 comments on commit b5ef0f0

Please sign in to comment.