-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Hydrate CustomError and Result. (#344)
* feat: Implement custom JSON serialization for CustomErrors. * feat: Implement a error hydration function. * feat: Fix toJSON method on CustomError. * feat: Implement hydrateResult function. * feat: Make some changes to README according to feedback. * chore: Add note about serializing cause and data to readme. * feat: Allow specification of potential custom errors in hydration. * chore: Remove hydration of CustomError.cause. * feat: Add error case to result hydration. * feat: Add optional hydration of error in hydrateResult. * feat: Omit data or cause when serializing CustomError if they are not serializable.
- Loading branch information
Hannes Leutloff
authored
Feb 22, 2022
1 parent
0549e6b
commit 3f4343c
Showing
8 changed files
with
395 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { CustomError } from './CustomError'; | ||
import { CustomErrorConstructor } from './CustomErrorConstructor'; | ||
import { defekt } from './defekt'; | ||
import { error, Result, value } from './Result'; | ||
|
||
class HydratingErrorFailed extends defekt({ code: 'HydratingErrorFailed' }) {} | ||
|
||
const hydrateCustomError = function<TPotentialCustomErrorNames extends string = string> ({ | ||
rawEx, potentialErrorConstructors | ||
}: { | ||
rawEx: any; | ||
potentialErrorConstructors: CustomErrorConstructor<TPotentialCustomErrorNames>[]; | ||
}): Result<CustomError<TPotentialCustomErrorNames>, HydratingErrorFailed> { | ||
if (typeof rawEx !== 'object' || rawEx === null) { | ||
return error(new HydratingErrorFailed('The given error is not an object.')); | ||
} | ||
|
||
if (!('name' in rawEx)) { | ||
return error(new HydratingErrorFailed('The given error is missing a name.')); | ||
} | ||
if (!('code' in rawEx)) { | ||
return error(new HydratingErrorFailed('The given error is missing a code.')); | ||
} | ||
if (!('message' in rawEx)) { | ||
return error(new HydratingErrorFailed('The given error is missing a message.')); | ||
} | ||
|
||
const ActualErrorConstructor = potentialErrorConstructors.find( | ||
(errorContructor): boolean => errorContructor.code === rawEx.code | ||
); | ||
|
||
if (ActualErrorConstructor === undefined) { | ||
return error(new HydratingErrorFailed({ | ||
message: 'Could not find an appropriate ErrorConstructor to hydrate the given error.', | ||
data: { code: rawEx.code } | ||
})); | ||
} | ||
|
||
const stack = rawEx?.stack; | ||
const data = rawEx?.data; | ||
const cause = rawEx?.cause; | ||
|
||
const hydratedError = new ActualErrorConstructor({ message: rawEx.message, cause, data }); | ||
|
||
hydratedError.name = rawEx.name; | ||
hydratedError.stack = stack; | ||
|
||
return value(hydratedError); | ||
}; | ||
|
||
export { | ||
HydratingErrorFailed, | ||
|
||
hydrateCustomError | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
import { CustomError } from './CustomError'; | ||
import { CustomErrorConstructor } from './CustomErrorConstructor'; | ||
import { defekt } from './defekt'; | ||
import { hydrateCustomError } from './hydrateCustomError'; | ||
import { error, Result, value } from './Result'; | ||
|
||
class HydratingResultFailed extends defekt({ code: 'HydratingResultFailed' }) {} | ||
|
||
const hydrateResult = function<TValue, TError extends Error, TPotentialCustomErrorNames extends string = string> ({ rawResult, potentialErrorConstructors }: { | ||
rawResult: { value: TValue } | { error: TError }; | ||
potentialErrorConstructors?: CustomErrorConstructor<TPotentialCustomErrorNames>[]; | ||
}): Result<Result<TValue, TError | CustomError<TPotentialCustomErrorNames>>, HydratingResultFailed> { | ||
if ('value' in rawResult) { | ||
return value(value(rawResult.value) as Result<TValue, any>); | ||
} | ||
if ('error' in rawResult) { | ||
let processedError: Error = rawResult.error; | ||
|
||
if (potentialErrorConstructors) { | ||
const hydrateErrorResult = hydrateCustomError({ rawEx: rawResult.error, potentialErrorConstructors }); | ||
|
||
if (hydrateErrorResult.hasError()) { | ||
return error(new HydratingResultFailed({ cause: hydrateErrorResult.error })); | ||
} | ||
|
||
processedError = hydrateErrorResult.value; | ||
} | ||
|
||
return value(error(processedError) as Result<TValue, any>); | ||
} | ||
|
||
return error(new HydratingResultFailed()); | ||
}; | ||
|
||
export { | ||
hydrateResult | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,100 @@ | ||
import { assert } from 'assertthat'; | ||
import { defekt, hydrateCustomError, isCustomError } from '../../lib'; | ||
|
||
suite('hydrateCustomError', (): void => { | ||
test('creates an error instance from raw data.', async (): Promise<void> => { | ||
class TokenInvalid extends defekt({ code: 'TokenInvalid' }) {} | ||
class TokenExpired extends defekt({ code: 'TokenExpired' }) {} | ||
|
||
const rawEx = { | ||
name: 'TokenInvalid', | ||
code: 'TokenInvalid', | ||
message: 'Foo', | ||
data: { foo: 'bar' }, | ||
cause: { | ||
name: 'TokenExpired', | ||
code: 'TokenExpired', | ||
message: 'Token expired.' | ||
} | ||
}; | ||
|
||
const ex = hydrateCustomError({ | ||
rawEx, | ||
potentialErrorConstructors: [ TokenInvalid, TokenExpired ] | ||
}).unwrapOrThrow(); | ||
|
||
assert.that(isCustomError(ex, TokenInvalid)).is.true(); | ||
assert.that(ex.cause).is.equalTo(rawEx.cause); | ||
}); | ||
|
||
test('fails to hydrate an error that is missing a name.', async (): Promise<void> => { | ||
const rawEx = { | ||
code: 'TokenInvalid', | ||
message: 'Foo' | ||
}; | ||
|
||
const hydrateResult = hydrateCustomError({ | ||
rawEx, | ||
potentialErrorConstructors: [] | ||
}); | ||
|
||
assert.that(hydrateResult).is.anErrorWithMessage('The given error is missing a name.'); | ||
}); | ||
|
||
test('fails to hydrate an error that is missing a code.', async (): Promise<void> => { | ||
const rawEx = { | ||
name: 'TokenInvalid', | ||
message: 'Foo' | ||
}; | ||
|
||
const hydrateResult = hydrateCustomError({ | ||
rawEx, | ||
potentialErrorConstructors: [] | ||
}); | ||
|
||
assert.that(hydrateResult).is.anErrorWithMessage('The given error is missing a code.'); | ||
}); | ||
|
||
test('fails to hydrate an error that is missing a message.', async (): Promise<void> => { | ||
const rawEx = { | ||
name: 'TokenInvalid', | ||
code: 'TokenInvalid' | ||
}; | ||
|
||
const hydrateResult = hydrateCustomError({ | ||
rawEx, | ||
potentialErrorConstructors: [] | ||
}); | ||
|
||
assert.that(hydrateResult).is.anErrorWithMessage('The given error is missing a message.'); | ||
}); | ||
|
||
test('fails to hydrate an error for which no appropriate error constructer is given.', async (): Promise<void> => { | ||
class TokenExpired extends defekt({ code: 'TokenExpired' }) {} | ||
|
||
const rawEx = { | ||
name: 'TokenInvalid', | ||
code: 'TokenInvalid', | ||
message: 'Foo' | ||
}; | ||
|
||
const hydrateResult = hydrateCustomError({ | ||
rawEx, | ||
potentialErrorConstructors: [ TokenExpired ] | ||
}); | ||
|
||
assert.that(hydrateResult).is.anErrorWithMessage('Could not find an appropriate ErrorConstructor to hydrate the given error.'); | ||
}); | ||
|
||
test('can hydrate serialized and deserialized errors.', async (): Promise<void> => { | ||
class TokenInvalid extends defekt({ code: 'TokenInvalid' }) {} | ||
|
||
const ex = new TokenInvalid(); | ||
const hydratedEx = hydrateCustomError({ | ||
rawEx: JSON.parse(JSON.stringify(ex)), | ||
potentialErrorConstructors: [ TokenInvalid ] | ||
}).unwrapOrThrow(); | ||
|
||
assert.that(isCustomError(hydratedEx, TokenInvalid)).is.true(); | ||
}); | ||
}); |
Oops, something went wrong.