Skip to content

Commit

Permalink
feat: rewrite factory to be Higher order function
Browse files Browse the repository at this point in the history
split spec files
  • Loading branch information
adriengibrat committed Mar 12, 2018
1 parent dd7086b commit 720940c
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 107 deletions.
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,24 @@ class HttpError extends CustomError {
super(message)
}
}

...
new HttpError(404, 'Not found')
```

### Using the factory
```ts
import { factory } from 'ts-custom-error'

const MyError = factory('MyError', { message: 'foo', code: 42 })
const HttpError = factory(function (code, message= '') {
this.code = code
this.message = message
})

...
new HttpError(404, 'Not found')
// or
HttpError(404, 'Not found')
```

## Similar packages
Expand Down
79 changes: 16 additions & 63 deletions src/custom-error.spec.ts
Original file line number Diff line number Diff line change
@@ -1,72 +1,25 @@
import { checkProtoChain, checkProperties } from './spec.utils'
import { CustomError } from './custom-error'
import { factory } from './factory'

const checkProtoChain = (contructor, ...chain) => () => {
const error = new contructor()
expect(error).toBeInstanceOf(contructor)
chain.forEach(c => expect(error).toBeInstanceOf(c))
}
test('Instance', () => checkProtoChain(CustomError, Error))

const checkProperties = (contructor, message = 'foo') => () => {
const error = new contructor(message)
expect(error.message).toBe(message)
expect(error).toHaveProperty('stack')
}

test('Instance', checkProtoChain(CustomError, Error))

test(
'Extended',
checkProtoChain(class SubError extends CustomError {}, CustomError, Error),
)

test('Basic properties', checkProperties(CustomError))

test('Factory instance', checkProtoChain(factory('TestError'), Error))

const TestError = factory('TestError')

test(
'Factory extended',
checkProtoChain(factory('SubError', {}, TestError), TestError, Error),
)

test(
'Factory extended by class',
checkProtoChain(class SubError extends TestError {}, TestError, Error),
)

test(
'Factory properties',
checkProperties(factory('TestError', { message: 'default', code: 1 })),
)
test('Instance pre ES6 environment', () => {
const O = <any>Object
const E = <any>Error
const setPrototypeOf = O.setPrototypeOf
const captureStackTrace = E.captureStackTrace
delete O.setPrototypeOf
delete E.captureStackTrace

test('Factory exended properties', () => {
const TestError = factory('TestError', { message: 'foo', code: 1 })
const message = 'my message'
const code = 42
const ArgsError = factory<{
name: 'ArgsError'
message: string
code: number
}>('ArgsError', { message: 'bar', code: 2 }, TestError)
const argsError = new ArgsError(message, code)
expect(argsError.message).toBe(message)
expect(argsError.code).toBe(code)
checkProtoChain(CustomError, Error)

const defaultArgsError = new ArgsError()
expect(defaultArgsError.message).toBe('bar')
expect(defaultArgsError.code).toBe(2)
O.setPrototypeOf = setPrototypeOf
E.captureStackTrace = captureStackTrace
})

test('Instance pre ES6 environment', () => {
delete (<any>Object).setPrototypeOf
delete (<any>Error).captureStackTrace
checkProtoChain(CustomError, Error)()
test('Extended', () => {
class SubError extends CustomError {}
checkProtoChain(SubError, CustomError, Error)
})

// call toString & inspect methods to display examples and for coverage
console.log('%o\n%o', TestError, Error)
console.log('%o\n%o', TestError.toString(), Error.toString())
console.log('%o\n%o', TestError('test'), Error('test'))
console.log('%o\n%o', TestError('msg').toString(), Error('msg').toString())
test('Basic properties', () => checkProperties(CustomError))
45 changes: 45 additions & 0 deletions src/factory.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { checkProtoChain, checkProperties } from './spec.utils'
import { factory } from './factory'

const TestError = factory(function TestError() {})

test('Factory instance', () => checkProtoChain(TestError, Error))

test('Factory extended', () => {
const SubError = factory(function SubError() {}, TestError)
checkProtoChain(SubError, TestError, Error)
checkProtoChain(factory(SubError, RangeError), RangeError, Error)
})

test('Factory extended by class', () => {
const TestError = <ErrorConstructor>factory(function MyError() {},
RangeError)
class SubError extends TestError {}
checkProtoChain(SubError, TestError, RangeError, Error)
})

test('Factory properties', () => {
function TestError(code = 1, message = 'foo') {
this.code = code
this.message = message
}
checkProperties(factory(TestError), 'foo')

function AnotherError(code = 2, message = 'bar') {
this.code = code
this.message = message
}
checkProperties(factory(AnotherError), 'bar')

const ArgsError = factory<{ code: number }>(
AnotherError,
factory(TestError),
)
const argsError = ArgsError(3, 'baz')
expect(argsError.message).toBe('baz')
expect(argsError.code).toBe(3)

const defaultArgsError = ArgsError()
expect(defaultArgsError.message).toBe('bar')
expect(defaultArgsError.code).toBe(2)
})
64 changes: 21 additions & 43 deletions src/factory.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,33 @@
import { fixStack } from './utils'

export interface CustomErrorConstructor<CustomError extends Error>
extends ErrorConstructor {
new (...args): CustomError
(...args): CustomError
readonly prototype: CustomError
interface CustomError extends Error {}

interface CustomProperties {
[property: string]: any
}

export const factory = <CustomError extends Error = Error>(
name: string,
props: { [prop: string]: any } = { message: undefined },
parent: CustomErrorConstructor<CustomError> | ErrorConstructor = Error,
): CustomErrorConstructor<CustomError> => {
const properties = Object.keys(props)
interface CustomConstructor<Properties> extends ErrorConstructor {
new (...args): CustomError & Properties
(...args): CustomError & Properties
readonly prototype: CustomError
}

const constructor = function(this: Error, ...args: any[]): void {
export function factory<Properties = CustomError>(
fn: (...Arguments) => void,
parent: ErrorConstructor = Error,
): CustomConstructor<Properties> {
function CustomError(this: CustomError, ...args: any[]): void {
// allow simple function call
if (!(this instanceof constructor)) return new constructor(...args)
// call super
if (!(this instanceof CustomError)) return new CustomError(...args)
// apply super
parent.apply(this, args)
// set properties from arguments or default values
properties.forEach((key, index) =>
Object.defineProperty(this, key, {
value: index in args ? args[index] : props[key],
writable: true,
configurable: true,
}),
)
// apply custom fn
fn.apply(this, args)
// try to remove contructor from stack trace
fixStack(this, constructor)
fixStack(this, CustomError)
}

return Object.defineProperties(constructor, {
prototype: {
value: Object.create(parent.prototype, {
name: { value: name },
toString: {
value: function() {
return `${this.name}: ${this.message}`
},
},
inspect: {
value: function() {
return this.stack
},
},
}),
},
toString: {
value: () => `function ${name}(${properties}) { [custom code] }`,
},
inspect: { value: () => `{ [Function: ${name}] }` },
return Object.defineProperties(CustomError, {
prototype: { value: Object.create(parent.prototype) },
})
}
11 changes: 11 additions & 0 deletions src/spec.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const checkProtoChain = (contructor, ...chain) => {
const error = new contructor()
expect(error).toBeInstanceOf(contructor)
chain.forEach(c => expect(error).toBeInstanceOf(c))
}

export const checkProperties = (contructor, message = 'foo') => {
const error = new contructor(message)
expect(error.message).toBe(message)
expect(error).toHaveProperty('stack')
}

0 comments on commit 720940c

Please sign in to comment.