Skip to content

Commit

Permalink
feat(packages/sui-decorators): Create AsyncInlineError decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
oriolpuig authored and Marc Benito committed May 29, 2024
1 parent dd1a6a9 commit dc9b3c3
Show file tree
Hide file tree
Showing 4 changed files with 246 additions and 1 deletion.
60 changes: 60 additions & 0 deletions packages/sui-decorators/src/decorators/AsyncInlineError/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import isPromise from '../../helpers/isPromise.js'

const GENERIC_ERROR_MESSAGE = 'You might decorate an async function with use @AsyncInlineError'

const _runner = ({instance, original} = {}) => {
return function (...args) {
const response = []
Object.defineProperty(response, '__INLINE_ERROR__', {
enumerable: false,
writable: true,
value: true
})

let promise = null

try {
promise = original.apply(instance, args)
} catch (error) {
throw new Error(GENERIC_ERROR_MESSAGE, {cause: error})
}

if (!isPromise(promise)) {
throw new Error(GENERIC_ERROR_MESSAGE)
}

return promise
.then(r => {
response[0] = null
response[1] = r
return response
})
.catch(e => {
response[0] = e
response[1] = null
return response
})
}
}

export function AsyncInlineError(config = {}) {
return function (target, fnName, descriptor) {
const {value: fn, configurable, enumerable} = descriptor

return Object.assign(
{},
{
configurable,
enumerable,
value(...args) {
const _fnRunner = _runner({
instance: this,
original: fn
})

return _fnRunner.apply(this, args)
}
}
)
}
}
3 changes: 2 additions & 1 deletion packages/sui-decorators/src/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {AsyncInlineError} from './decorators/AsyncInlineError/index.js'
import {cache, invalidateCache} from './decorators/cache/index.js'
import inlineError from './decorators/error.js'
import streamify from './decorators/streamify.js'
import tracer from './decorators/tracer/index.js'

export {cache, invalidateCache, streamify, inlineError, tracer}
export {AsyncInlineError, cache, invalidateCache, streamify, inlineError, tracer}
92 changes: 92 additions & 0 deletions packages/sui-decorators/test/browser/asyncInlineErrorSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {expect} from 'chai'

import {AsyncInlineError} from '../../src/index.js'

describe('AsyncInlineError decorator', () => {
it('should exist', () => {
expect(AsyncInlineError).to.exist
expect(AsyncInlineError).to.be.a('function')
})

it('should return an array [null, resp] when the promise is resolved', async () => {
class Buzz {
@AsyncInlineError()
returnASuccessPromise() {
return Promise.resolve(true)
}
}
const buzz = new Buzz()
expect(await buzz.returnASuccessPromise()).to.be.eql([null, true])
})

it('should return an array [Error, null] when the promise is rejected', async () => {
class Buzz {
@AsyncInlineError()
returnAFailedPromise() {
return Promise.reject(new Error('Error Rejected'))
}
}
const buzz = new Buzz()

const [err, resp] = await buzz.returnAFailedPromise()
expect(resp).to.be.eql(null)
expect(err).to.be.an.instanceof(Error)
expect(err.message).to.be.eql('Error Rejected')
})

it('should preserve the context', async () => {
class Buzz {
name = 'Carlos'

@AsyncInlineError()
returnASuccessPromise() {
return Promise.resolve(this.name)
}
}
const buzz = new Buzz()
expect(await buzz.returnASuccessPromise()).to.be.eql([null, 'Carlos'])
})

it('should works with an Error subclass', async () => {
class CustomError extends Error {}
class Buzz {
@AsyncInlineError()
returnAFailedPromise() {
return Promise.reject(new CustomError('Error Rejected'))
}
}
const buzz = new Buzz()

const [err, resp] = await buzz.returnAFailedPromise()
expect(resp).to.be.eql(null)
expect(err).to.be.an.instanceof(CustomError)
expect(err.message).to.be.eql('Error Rejected')
})

it('should fail when the decorator is used in a non-async function', () => {
expect(() => {
class Buzz {
@AsyncInlineError()
execute() {
return true
}
}
const buzz = new Buzz()
buzz.execute()
}).to.throw('You might decorate an async function with use @AsyncInlineError')
})

it('should fail when the decorated method throws an error', async () => {
class Buzz {
@AsyncInlineError()
throwAnException() {
throw new Error('Error exception')
}
}
const buzz = new Buzz()

expect(() => {
buzz.throwAnException()
}).to.throw('You might decorate an async function with use @AsyncInlineError')
})
})
92 changes: 92 additions & 0 deletions packages/sui-decorators/test/server/asyncInlineErrorSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import {expect} from 'chai'

import {AsyncInlineError} from '../../src/index.js'

describe('AsyncInlineError decorator', () => {
it('should exist', () => {
expect(AsyncInlineError).to.exist
expect(AsyncInlineError).to.be.a('function')
})

it('should return an array [null, resp] when the promise is resolved', async () => {
class Buzz {
@AsyncInlineError()
returnASuccessPromise() {
return Promise.resolve(true)
}
}
const buzz = new Buzz()
expect(await buzz.returnASuccessPromise()).to.be.eql([null, true])
})

it('should return an array [Error, null] when the promise is rejected', async () => {
class Buzz {
@AsyncInlineError()
returnAFailedPromise() {
return Promise.reject(new Error('Error Rejected'))
}
}
const buzz = new Buzz()

const [err, resp] = await buzz.returnAFailedPromise()
expect(resp).to.be.eql(null)
expect(err).to.be.an.instanceof(Error)
expect(err.message).to.be.eql('Error Rejected')
})

it('should preserve the context', async () => {
class Buzz {
name = 'Carlos'

@AsyncInlineError()
returnASuccessPromise() {
return Promise.resolve(this.name)
}
}
const buzz = new Buzz()
expect(await buzz.returnASuccessPromise()).to.be.eql([null, 'Carlos'])
})

it('should works with an Error subclass', async () => {
class CustomError extends Error {}
class Buzz {
@AsyncInlineError()
returnAFailedPromise() {
return Promise.reject(new CustomError('Error Rejected'))
}
}
const buzz = new Buzz()

const [err, resp] = await buzz.returnAFailedPromise()
expect(resp).to.be.eql(null)
expect(err).to.be.an.instanceof(CustomError)
expect(err.message).to.be.eql('Error Rejected')
})

it('should fail when the decorator is used in a non-async function', () => {
expect(() => {
class Buzz {
@AsyncInlineError()
execute() {
return true
}
}
const buzz = new Buzz()
buzz.execute()
}).to.throw('You might decorate an async function with use @AsyncInlineError')
})

it('should fail when the decorated method throws an error', async () => {
class Buzz {
@AsyncInlineError()
throwAnException() {
throw new Error('Error exception')
}
}
const buzz = new Buzz()

expect(() => {
buzz.throwAnException()
}).to.throw('You might decorate an async function with use @AsyncInlineError')
})
})

0 comments on commit dc9b3c3

Please sign in to comment.