From 4e3c896e36a07c99327896f17b57eff780b18427 Mon Sep 17 00:00:00 2001 From: Mathis Wiehl Date: Fri, 11 Jan 2019 11:38:59 +0100 Subject: [PATCH] feat(core): add support for optional dependencies (#244) --- .../feature-service-registry.test.ts | 189 +++++++++++++++++- packages/core/src/feature-service-registry.ts | 151 ++++++++------ .../src/internal/toposort-dependencies.ts | 11 +- 3 files changed, 286 insertions(+), 65 deletions(-) diff --git a/packages/core/src/__tests__/feature-service-registry.test.ts b/packages/core/src/__tests__/feature-service-registry.test.ts index 0c73e770..e5c0a51d 100644 --- a/packages/core/src/__tests__/feature-service-registry.test.ts +++ b/packages/core/src/__tests__/feature-service-registry.test.ts @@ -55,7 +55,7 @@ describe('FeatureServiceRegistry', () => { providerDefinitionB = { id: 'b', - dependencies: {a: '^1.0'}, + optionalDependencies: {a: '^1.0'}, create: jest.fn(() => ({'1.0': binderB})) }; @@ -194,19 +194,43 @@ describe('FeatureServiceRegistry', () => { ]); }); - it('fails to register the Feature Service "b" due to the lack of dependency "a"', () => { + it('fails to register the Feature Service "c" due to the lack of dependency "a"', () => { expect(() => featureServiceRegistry.registerFeatureServices( - [providerDefinitionB], + [providerDefinitionC], 'test' ) ).toThrowError( new Error( - 'The required Feature Service "a" is not registered and therefore could not be bound to consumer "b".' + 'The required Feature Service "a" is not registered and therefore could not be bound to consumer "c".' ) ); }); + it('doesnt fail to register the Feature Service "b" due to the lack of optional dependency "a"', () => { + providerDefinitionB = { + id: 'b', + optionalDependencies: {a: '^1.0'}, + create: jest.fn(() => ({'1.0': jest.fn()})) + }; + + expect(() => + featureServiceRegistry.registerFeatureServices( + [providerDefinitionB], + 'test' + ) + ).not.toThrow(); + + expect(spyConsoleInfo.mock.calls).toEqual([ + [ + 'The optional Feature Service "a" is not registered and therefore could not be bound to consumer "b".' + ], + [ + 'The Feature Service "b" has been successfully registered by consumer "test".' + ] + ]); + }); + it('fails to register the Feature Service "d" due to an unsupported dependency version', () => { const stateProviderD = { id: 'd', @@ -226,6 +250,33 @@ describe('FeatureServiceRegistry', () => { ); }); + it('does not fail to register the Feature Service "d" due to an unsupported optional dependency version', () => { + const stateProviderD = { + id: 'd', + optionalDependencies: {a: '1.0'}, + create: jest.fn() + }; + + expect(() => + featureServiceRegistry.registerFeatureServices( + [providerDefinitionA, stateProviderD], + 'test' + ) + ).not.toThrow(); + + expect(spyConsoleInfo.mock.calls).toEqual([ + [ + 'The Feature Service "a" has been successfully registered by consumer "test".' + ], + [ + 'The optional Feature Service "a" in the unsupported version "1.0" could not be bound to consumer "d". The supported versions are ["1.1"].' + ], + [ + 'The Feature Service "d" has been successfully registered by consumer "test".' + ] + ]); + }); + it('fails to register the Feature Service "d" due to an invalid dependency version', () => { const stateProviderDefinitionD = { id: 'd', @@ -245,6 +296,33 @@ describe('FeatureServiceRegistry', () => { ); }); + it('does not fail to register the Feature Service "d" due to an invalid optional dependency version', () => { + const stateProviderDefinitionD = { + id: 'd', + optionalDependencies: {a: ''}, + create: jest.fn() + }; + + expect(() => + featureServiceRegistry.registerFeatureServices( + [providerDefinitionA, stateProviderDefinitionD], + 'test' + ) + ).not.toThrow(); + + expect(spyConsoleInfo.mock.calls).toEqual([ + [ + 'The Feature Service "a" has been successfully registered by consumer "test".' + ], + [ + 'The optional Feature Service "a" in an invalid version could not be bound to consumer "d".' + ], + [ + 'The Feature Service "d" has been successfully registered by consumer "test".' + ] + ]); + }); + it('fails to register the Feature Service "e" due to a dependency with an invalid version', () => { const stateProviderDefinitionD = { id: 'd', @@ -308,6 +386,109 @@ describe('FeatureServiceRegistry', () => { }); }); + describe('for a Feature Service consumer without an id specifier and dependencies', () => { + it('creates a bindings object with Feature Services', () => { + featureServiceRegistry = new FeatureServiceRegistry(); + + featureServiceRegistry.registerFeatureServices( + [providerDefinitionA], + 'test' + ); + + expect(binderA.mock.calls).toEqual([]); + + expect( + featureServiceRegistry.bindFeatureServices({ + id: 'foo', + dependencies: {a: '1.1'} + }) + ).toEqual({ + featureServices: {a: featureServiceA}, + unbind: expect.any(Function) + }); + + expect(binderA.mock.calls).toEqual([['foo']]); + }); + }); + + describe('for a Feature Service consumer and two optional dependencies', () => { + describe('with the first dependency missing', () => { + it('creates a bindings object with Feature Services', () => { + featureServiceRegistry = new FeatureServiceRegistry(); + + featureServiceRegistry.registerFeatureServices( + [providerDefinitionA], + 'test' + ); + + expect(binderA.mock.calls).toEqual([]); + + expect( + featureServiceRegistry.bindFeatureServices({ + id: 'foo', + optionalDependencies: {b: '1.0', a: '1.1'} + }) + ).toEqual({ + featureServices: {a: featureServiceA}, + unbind: expect.any(Function) + }); + + expect(binderA.mock.calls).toEqual([['foo']]); + }); + }); + + describe('with the second dependency missing', () => { + it('creates a bindings object with Feature Services', () => { + featureServiceRegistry = new FeatureServiceRegistry(); + + featureServiceRegistry.registerFeatureServices( + [providerDefinitionA], + 'test' + ); + + expect(binderA.mock.calls).toEqual([]); + + expect( + featureServiceRegistry.bindFeatureServices({ + id: 'foo', + optionalDependencies: {a: '1.1', b: '1.0'} + }) + ).toEqual({ + featureServices: {a: featureServiceA}, + unbind: expect.any(Function) + }); + + expect(binderA.mock.calls).toEqual([['foo']]); + }); + }); + + describe('with no dependency missing', () => { + it('creates a bindings object with Feature Services', () => { + featureServiceRegistry = new FeatureServiceRegistry(); + + featureServiceRegistry.registerFeatureServices( + [providerDefinitionA, providerDefinitionB], + 'test' + ); + + expect(binderA.mock.calls).toEqual([['b']]); + + expect( + featureServiceRegistry.bindFeatureServices({ + id: 'foo', + optionalDependencies: {a: '1.1', b: '^1.0'} + }) + ).toEqual({ + featureServices: {a: featureServiceA, b: featureServiceB}, + unbind: expect.any(Function) + }); + + expect(binderA.mock.calls).toEqual([['b'], ['foo']]); + expect(binderB.mock.calls).toEqual([['foo']]); + }); + }); + }); + it('fails to create a bindings object for an consumer which is already bound', () => { featureServiceRegistry.bindFeatureServices({id: 'foo'}); featureServiceRegistry.bindFeatureServices({id: 'foo'}, 'bar'); diff --git a/packages/core/src/feature-service-registry.ts b/packages/core/src/feature-service-registry.ts index 67042ee0..1797f82e 100644 --- a/packages/core/src/feature-service-registry.ts +++ b/packages/core/src/feature-service-registry.ts @@ -9,6 +9,7 @@ export interface FeatureServiceConsumerDependencies { export interface FeatureServiceConsumerDefinition { readonly id: string; readonly dependencies?: FeatureServiceConsumerDependencies; + readonly optionalDependencies?: FeatureServiceConsumerDependencies; } export interface FeatureServices { @@ -82,21 +83,22 @@ export interface FeatureServiceRegistryOptions { type ProviderId = string; -function createUnsupportedFeatureServiceError( +function createUnsupportedFeatureServiceMessage( + optional: boolean, providerId: string, consumerUid: string, requiredVersion: string, supportedVersions: string[] -): Error { - return new Error( - `The required Feature Service ${JSON.stringify( - providerId - )} in the unsupported version ${JSON.stringify( - requiredVersion - )} could not be bound to consumer ${JSON.stringify( - consumerUid - )}. The supported versions are ${JSON.stringify(supportedVersions)}.` - ); +): string { + return `The ${ + optional ? 'optional' : 'required' + } Feature Service ${JSON.stringify( + providerId + )} in the unsupported version ${JSON.stringify( + requiredVersion + )} could not be bound to consumer ${JSON.stringify( + consumerUid + )}. The supported versions are ${JSON.stringify(supportedVersions)}.`; } export class FeatureServiceRegistry implements FeatureServiceRegistryLike { @@ -115,17 +117,20 @@ export class FeatureServiceRegistry implements FeatureServiceRegistryLike { providerDefinitions: FeatureServiceProviderDefinition[], consumerId: string ): void { - const dependencyGraph = new Map(); + const providerDefinitionsById = new Map< + string, + FeatureServiceProviderDefinition + >(); for (const providerDefinition of providerDefinitions) { - dependencyGraph.set(providerDefinition.id, providerDefinition); + providerDefinitionsById.set(providerDefinition.id, providerDefinition); } - for (const providerId of toposortDependencies(dependencyGraph)) { - const providerDefinition = dependencyGraph.get(providerId); + for (const providerId of toposortDependencies(providerDefinitionsById)) { + const providerDefinition = providerDefinitionsById.get(providerId); if (this.sharedFeatureServices.has(providerId)) { - if (dependencyGraph.has(providerId)) { + if (providerDefinitionsById.has(providerId)) { console.warn( `The already registered Feature Service ${JSON.stringify( providerId @@ -161,7 +166,8 @@ export class FeatureServiceRegistry implements FeatureServiceRegistryLike { ): FeatureServicesBinding { const { id: consumerId, - dependencies: consumerDependencies + dependencies = {}, + optionalDependencies = {} } = consumerDefinition; const consumerUid = createUid(consumerId, consumerIdSpecifier); @@ -176,27 +182,31 @@ export class FeatureServiceRegistry implements FeatureServiceRegistryLike { const bindings = new Map>(); const featureServices: FeatureServices = Object.create(null); + const allDependencies = {...optionalDependencies, ...dependencies}; - if (consumerDependencies) { - for (const providerId of Object.keys(consumerDependencies)) { - const binding = this.bindFeatureService( - providerId, - consumerUid, - consumerDependencies[providerId] - ); + for (const providerId of Object.keys(allDependencies)) { + const binding = this.bindFeatureService( + providerId, + consumerUid, + allDependencies[providerId], + {optional: !dependencies.hasOwnProperty(providerId)} + ); - console.info( - `The required Feature Service ${JSON.stringify( - providerId - )} has been successfully bound to consumer ${JSON.stringify( - consumerUid - )}.` - ); + if (!binding) { + continue; + } + + console.info( + `The required Feature Service ${JSON.stringify( + providerId + )} has been successfully bound to consumer ${JSON.stringify( + consumerUid + )}.` + ); - bindings.set(providerId, binding); + bindings.set(providerId, binding); - featureServices[providerId] = binding.featureService; - } + featureServices[providerId] = binding.featureService; } this.consumerUids.add(consumerUid); @@ -248,28 +258,45 @@ export class FeatureServiceRegistry implements FeatureServiceRegistryLike { private bindFeatureService( providerId: string, consumerUid: string, - requiredVersion: string | undefined - ): FeatureServiceBinding { + requiredVersion: string | undefined, + {optional}: {optional: boolean} + ): FeatureServiceBinding | undefined { if (!requiredVersion) { - throw new Error( - `The required Feature Service ${JSON.stringify( - providerId - )} in an invalid version could not be bound to consumer ${JSON.stringify( - consumerUid - )}.` - ); + const message = `The ${ + optional ? 'optional' : 'required' + } Feature Service ${JSON.stringify( + providerId + )} in an invalid version could not be bound to consumer ${JSON.stringify( + consumerUid + )}.`; + + if (optional) { + console.info(message); + + return; + } + + throw new Error(message); } const sharedFeatureService = this.sharedFeatureServices.get(providerId); if (!sharedFeatureService) { - throw new Error( - `The required Feature Service ${JSON.stringify( - providerId - )} is not registered and therefore could not be bound to consumer ${JSON.stringify( - consumerUid - )}.` - ); + const message = `The ${ + optional ? 'optional' : 'required' + } Feature Service ${JSON.stringify( + providerId + )} is not registered and therefore could not be bound to consumer ${JSON.stringify( + consumerUid + )}.`; + + if (optional) { + console.info(message); + + return; + } + + throw new Error(message); } const supportedVersions = Object.keys(sharedFeatureService); @@ -278,11 +305,14 @@ export class FeatureServiceRegistry implements FeatureServiceRegistryLike { const actualVersion = coerce(supportedVersion); if (!actualVersion) { - throw createUnsupportedFeatureServiceError( - providerId, - consumerUid, - requiredVersion, - supportedVersions + throw new Error( + createUnsupportedFeatureServiceMessage( + optional, + providerId, + consumerUid, + requiredVersion, + supportedVersions + ) ); } @@ -292,12 +322,21 @@ export class FeatureServiceRegistry implements FeatureServiceRegistryLike { const bindFeatureService = version && sharedFeatureService[version]; if (!bindFeatureService) { - throw createUnsupportedFeatureServiceError( + const message = createUnsupportedFeatureServiceMessage( + optional, providerId, consumerUid, requiredVersion, supportedVersions ); + + if (optional) { + console.info(message); + + return; + } + + throw new Error(message); } return bindFeatureService(consumerUid); diff --git a/packages/core/src/internal/toposort-dependencies.ts b/packages/core/src/internal/toposort-dependencies.ts index 72967085..0699e32e 100644 --- a/packages/core/src/internal/toposort-dependencies.ts +++ b/packages/core/src/internal/toposort-dependencies.ts @@ -6,6 +6,7 @@ export interface Dependencies { export interface Dependant { readonly dependencies?: Dependencies; + readonly optionalDependencies?: Dependencies; } export type DependencyGraph = Map< @@ -23,7 +24,7 @@ function createTuple( function createDependencyEdges( dependentName: string, - dependencies: Dependencies = Object.create(null) + dependencies: Dependencies ): DependencyEdges { return Object.keys(dependencies).map(createTuple(dependentName)); } @@ -40,10 +41,10 @@ function createAllDependencyEdges( return allDependencyEdges; } - const dependencyEdges = createDependencyEdges( - dependencyName, - dependant.dependencies - ); + const dependencyEdges = createDependencyEdges(dependencyName, { + ...dependant.dependencies, + ...dependant.optionalDependencies + }); return [...allDependencyEdges, ...dependencyEdges]; },