Skip to content

Commit

Permalink
Fallback (#4)
Browse files Browse the repository at this point in the history
* feat: fallback
  • Loading branch information
artiompopcov authored and dimadeveatii committed Nov 13, 2019
1 parent 9a03950 commit f4f6dcb
Show file tree
Hide file tree
Showing 3 changed files with 289 additions and 3 deletions.
37 changes: 37 additions & 0 deletions examples/fallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { fallback } from '../lib/fallback';

class Service {

public fallbackValue = 'fallback value';

@fallback(function () { return `${this.fallbackValue} provider`; })
public syncExample() {
throw new Error('error message');
}

@fallback('fallback value')
public asyncExample() {
return new Promise((resolve, reject) => {
setTimeout(() => reject(new Error('error message')), 100);
});
}

@fallback('fallback value', { errorFilter: ({ message }: Error) => message.includes('message') })
public filterErrorExample() {
throw new Error('error message');
}

}

const service = new Service();

(async () => {
const syncExample = service.syncExample();
console.log('Sync example:', syncExample);

const asyncExample = await service.asyncExample();
console.log('Async example:', asyncExample);

const filterExample = service.filterErrorExample();
console.log('Filter error example:', filterExample);
})();
69 changes: 66 additions & 3 deletions lib/fallback.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,70 @@ export type FallbackOptions = {
* Allows to provide a substitute value when method call fails or rejects.
* @param options optional fallback options
*/
export function fallback(valueProvider: (...args: any[]) => any, options?: FallbackOptions);
export function fallback(value: any, options?: FallbackOptions) {
throw new Error('Not implemented.');
export function fallback(valueProvider: (...args: any[]) => any, options?: FallbackOptions): any;
export function fallback(value: any, options?: FallbackOptions): any;
export function fallback(value: any | ((...args: any[]) => any), options?: FallbackOptions): any {

return function (target: any, propertyKey: any, descriptor: PropertyDescriptor) {
const method = descriptor.value;

descriptor.value = function () {
try {
const result = method.call(this, arguments);
const isPromiseLike = result && typeof result.then === 'function';

if (isPromiseLike) {
return fallbackPromise(options, result, value, this);
}

return result;

} catch (err) {
const isFiltered = filterError(err, options, this);

if (isFiltered) {
return getFallbackValue(value, this);
}

throw err;
}
};

};
}

function fallbackPromise(
options: FallbackOptions,
result: Promise<never>,
value: any,
instance: any,
): Promise<any> {

const reject = (err: any) => {
const isFiltered = filterError(err, options, instance);

return isFiltered
? Promise.resolve(getFallbackValue(value, instance))
: Promise.reject(err);
};

return result.catch(reject);
}

function filterError(error: Error, options: FallbackOptions, instance: any): boolean {
if (!options || !options.errorFilter) {
return true;
}

return options.errorFilter.bind(instance)(error);
}

function getFallbackValue(value: any | ((...args: any[]) => any), instance: any): any {
const isFunction = typeof value === 'function';

if (isFunction) {
return value.bind(instance)();
}

return value;
}
186 changes: 186 additions & 0 deletions test/fallback.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { fallback } from '../lib/fallback';
import { expect } from 'chai';

describe('@fallback', () => {

describe('synchronous method', () => {

it('should take the real value', () => {
class TestClass {
@fallback(42)
test() {
return 4242;
}
}

const target = new TestClass();
const result = target.test();

expect(result).to.equal(4242);
});

it('should take the fallback value', () => {
class TestClass {
@fallback(42)
test() {
throw new Error('4242');
}
}

const target = new TestClass();
const result = target.test();

expect(result).to.equal(42);
});

it('should take the fallback provided value', () => {
class TestClass {
value = 42;
@fallback(function () { return this.value; })
test() {
throw new Error('4242');
}
}

const target = new TestClass();
const result = target.test();

expect(result).to.equal(42);
});

it('should return fallback by filtered error', () => {
class TestClass {
@fallback(42, { errorFilter: ({ message }: Error) => message === '4242' })
test() {
throw new Error('4242');
}
}

const target = new TestClass();
const result = target.test();

expect(result).to.equal(42);
});

it('should return fallback by filtered error from class method', () => {
class TestClass {
filterError(err: Error) {
return err.message.includes('4242');
}
@fallback(42, { errorFilter(err) { return this.filterError(err); } })
test() {
throw new Error('4242');
}
}

const target = new TestClass();
const result = target.test();

expect(result).to.equal(42);
});

it('should throw error', () => {
class TestClass {
@fallback(42, { errorFilter: ({ message }: Error) => message === '3131' })
test() {
throw new Error('4242');
}
}

const target = new TestClass();
expect(target.test).to.throw(Error);
});

});

describe('asynchronous method', () => {

it('should take the real value', async () => {
class TestClass {
@fallback(42)
test() {
return Promise.resolve(4242);
}
}

const target = new TestClass();
const result = await target.test();

expect(result).to.equal(4242);
});

it('should take the fallback value', async () => {
class TestClass {
@fallback(42)
test() {
return Promise.reject(new Error('4242'));
}
}

const target = new TestClass();
const result = await target.test();

expect(result).to.equal(42);
});

it('should take the fallback provided value', async () => {
class TestClass {
value = 42;
@fallback(function () { return this.value; })
test() {
return Promise.reject(new Error('4242'));
}
}

const target = new TestClass();
const result = await target.test();

expect(result).to.equal(42);
});

it('should return fallback by filtered error', async () => {
class TestClass {
filterError(err: Error) {
return err.message.includes('4242');
}
@fallback(42, { errorFilter(err) { return this.filterError(err); } })
test() {
return Promise.reject(new Error('4242'));
}
}

const target = new TestClass();
const result = await target.test();

expect(result).to.equal(42);
});

it('should return fallback by filtered error', async () => {
class TestClass {
@fallback(42, { errorFilter: ({ message }: Error) => message === '4242' })
test() {
return Promise.reject(new Error('4242'));
}
}

const target = new TestClass();
const result = await target.test();

expect(result).to.equal(42);
});

it('should reject an error', async () => {
class TestClass {
@fallback(42, { errorFilter: ({ message }: Error) => message === '3131' })
test() {
return Promise.reject(new Error('4242'));
}
}

const target = new TestClass();
await expect(target.test()).to.be.rejectedWith('4242');
});

});

});

0 comments on commit f4f6dcb

Please sign in to comment.