Skip to content
This repository has been archived by the owner on Dec 24, 2024. It is now read-only.

Commit

Permalink
fix(node/errorhandler): use a provider creator
Browse files Browse the repository at this point in the history
Instead of having `errorHandler` and `errorHandlerWithOptions`, they have been merged into one
single provider creator.

BREAKING CHANGE: The provider `errorHandlerWithOptions` doesn't exist anymore.
  • Loading branch information
homer0 committed Jul 17, 2020
1 parent c6821f9 commit b53b71e
Show file tree
Hide file tree
Showing 2 changed files with 138 additions and 61 deletions.
112 changes: 81 additions & 31 deletions node/errorHandler.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
const { provider } = require('jimple');
const { providerCreator } = require('../shared/jimpleFns');
const { deepAssignWithShallowMerge } = require('../shared/deepAssign');
/**
* @module node/errorHandler
*/
Expand All @@ -7,6 +8,32 @@ const { provider } = require('jimple');
* @typedef {import('./logger').Logger} Logger
*/

/**
* @typedef {import('../shared/jimpleFns').ProviderCreatorWithOptions<O>}
* ProviderCreatorWithOptions
* @template O
*/

/**
* @typedef {Object} ErrorHandlerServiceMap
* @property {string[]|string|Logger} [logger]
* A list of loggers' service names from which the service will try to find the first available,
* a specific service name, or an instance of {@link Logger}.
* @parent module:node/errorHandler
*/

/**
* @typedef {Object} ErrorHandlerProviderOptions
* @property {string} serviceName
* The name that will be used to register an instance of {@link ErrorHandler}. Its default value
* is `errorHandler`.
* @property {boolean} exitOnError
* Whether or not to exit the process after receiving an error.
* @property {ErrorHandlerServiceMap} services
* A dictionary with the services that need to be injected on the class.
* @parent module:node/errorHandler
*/

/**
* An error handler that captures uncaught exceptions and unhandled rejections in order to log
* them with detail.
Expand Down Expand Up @@ -96,44 +123,67 @@ class ErrorHandler {
});
}
}

/**
* Generates a `Provider` with an already defined flag to exit or not the process when after
* handling an error.
* The service provider to register an instance of {@link ErrorHandler} on the container.
*
* @param {boolean} [exitOnError] Whether or not to exit the process after receiving an error.
* @returns {Provider}
* @throws {Error} If `services.logger` specifies a service that doesn't exist or if it's a falsy
* value.
* @type {ProviderCreatorWithOptions<ErrorHandlerProviderOptions>}
* @tutorial errorHandler
*/
const errorHandlerWithOptions = (exitOnError) => provider((app) => {
app.set('errorHandler', () => {
let logger = null;
try {
logger = app.get('logger');
} catch (ignore) {
logger = app.get('appLogger');
const errorHandler = providerCreator((options = {}) => (app) => {
app.set(options.serviceName || 'errorHandler', () => {
/**
* @type {ErrorHandlerProviderOptions}
* @ignore
*/
const useOptions = deepAssignWithShallowMerge(
{
services: {
logger: ['logger', 'appLogger'],
},
},
options,
);

const { logger } = useOptions.services;
/**
* @type {?Logger}
* @ignore
*/
let useLogger;
if (Array.isArray(logger)) {
useLogger = logger.reduce(
(acc, name) => {
let nextAcc;
if (acc) {
nextAcc = acc;
} else {
try {
nextAcc = app.get(name);
} catch (ignore) {
nextAcc = null;
}
}

return nextAcc;
},
null,
);
} else if (typeof logger === 'string') {
useLogger = app.get(logger);
} else {
useLogger = logger;
}

return new ErrorHandler(
logger,
exitOnError,
);
if (!useLogger) {
throw new Error('No logger service was found');
}

return new ErrorHandler(useLogger, useOptions.exitOnError);
});
});
/**
* The service provider that once registered on the app container will set an instance of
* `ErrorHandler` as the `errorHandler` service.
*
* @example
* // Register it on the container
* container.register(errorHandler);
* // Getting access to the service instance
* const instance = container.get('errorHandler');
*
* @type {Provider}
* @tutorial errorHandler
*/
const errorHandler = errorHandlerWithOptions();

module.exports.ErrorHandler = ErrorHandler;
module.exports.errorHandlerWithOptions = errorHandlerWithOptions;
module.exports.errorHandler = errorHandler;
87 changes: 57 additions & 30 deletions tests/node/errorHandler.test.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
jest.mock('jimple', () => ({ provider: jest.fn(() => 'provider') }));
jest.unmock('../../node/errorHandler.js');
jest.unmock('../../shared/deepAssign.js');
jest.unmock('../../shared/jimpleFns.js');

const { provider } = require('jimple');
const {
ErrorHandler,
errorHandlerWithOptions,
errorHandler,
} = require('../../node/errorHandler');

Expand Down Expand Up @@ -136,24 +135,20 @@ describe('ErrorHandler', () => {
expect(exitMock).toHaveBeenCalledTimes(0);
});

it('should have a Jimple provider to register the service', () => {
it('should include a provider for the DIC', () => {
// Given
const container = {
set: jest.fn(),
get: jest.fn((dependency) => dependency),
};
let sut = null;
let serviceProvider = null;
let serviceName = null;
let serviceFn = null;
// When
[[serviceProvider]] = provider.mock.calls;
serviceProvider(container);
errorHandler.register(container);
[[serviceName, serviceFn]] = container.set.mock.calls;
sut = serviceFn();
// Then
expect(errorHandler).toBe('provider');
expect(provider).toHaveBeenCalledTimes(1);
expect(serviceName).toBe('errorHandler');
expect(sut).toBeInstanceOf(ErrorHandler);
expect(container.get).toHaveBeenCalledTimes(1);
Expand All @@ -173,51 +168,83 @@ describe('ErrorHandler', () => {
}),
};
let sut = null;
let serviceProvider = null;
let serviceName = null;
let serviceFn = null;
// When
[[serviceProvider]] = provider.mock.calls;
serviceProvider(container);
errorHandler.register(container);
[[serviceName, serviceFn]] = container.set.mock.calls;
sut = serviceFn();
// Then
expect(errorHandler).toBe('provider');
expect(provider).toHaveBeenCalledTimes(1);
expect(serviceName).toBe('errorHandler');
expect(sut).toBeInstanceOf(ErrorHandler);
expect(container.get).toHaveBeenCalledTimes(['logger', 'appLogger'].length);
expect(container.get).toHaveBeenCalledWith('appLogger');
expect(container.get).toHaveBeenNthCalledWith(1, 'logger');
expect(container.get).toHaveBeenNthCalledWith(2, 'appLogger');
});

it('should have a customizable Jimple provider to disable the `process.exit`', () => {
it('should allow custom options on its service provider', () => {
// Given
const container = {
set: jest.fn(),
get: jest.fn((dependency) => dependency),
};
const exitOnError = false;
const options = {
serviceName: 'myErrorHandler',
services: {
logger: 'MyLogger',
},
exitOnError: false,
};
let sut = null;
let registerResult = null;
let serviceProvider = null;
let serviceName = null;
let serviceFn = null;
// When
registerResult = errorHandlerWithOptions(exitOnError);
/**
* The first one is the one for the default provider, the second one is
* the one generated by this test.
*/
[, [serviceProvider]] = provider.mock.calls;
serviceProvider(container);
errorHandler(options).register(container);
[[serviceName, serviceFn]] = container.set.mock.calls;
sut = serviceFn();
// Then
expect(registerResult).toBe('provider');
expect(serviceName).toBe('errorHandler');
expect(serviceName).toBe(options.serviceName);
expect(sut).toBeInstanceOf(ErrorHandler);
expect(sut.exitOnError).toBe(exitOnError);
expect(sut.exitOnError).toBe(options.exitOnError);
expect(container.get).toHaveBeenCalledTimes(1);
expect(container.get).toHaveBeenCalledWith('logger');
expect(container.get).toHaveBeenCalledWith(options.services.logger);
});

it('should allow a custom logger on its service provider', () => {
// Given
const container = {
set: jest.fn(),
get: jest.fn(),
};
const options = {
services: {
logger: { hello: 'world' },
},
};
let serviceFn = null;
// When
errorHandler(options).register(container);
[[, serviceFn]] = container.set.mock.calls;
serviceFn();
// Then
expect(container.get).toHaveBeenCalledTimes(0);
});

it('should throw an error if an invalid logger is provided', () => {
// Given
const container = {
set: jest.fn(),
get: jest.fn(),
};
const options = {
services: {
logger: null,
},
};
let serviceFn = null;
// When/Then
errorHandler(options).register(container);
[[, serviceFn]] = container.set.mock.calls;
expect(() => serviceFn()).toThrow(/No logger service was found/i);
});
});

0 comments on commit b53b71e

Please sign in to comment.