Skip to content

Commit

Permalink
feat(boot): improve service booter to load classes decorated with @Bind
Browse files Browse the repository at this point in the history
  • Loading branch information
raymondfeng committed Jul 29, 2019
1 parent 88eff77 commit 48e01f4
Show file tree
Hide file tree
Showing 5 changed files with 102 additions and 34 deletions.
35 changes: 35 additions & 0 deletions packages/boot/src/__tests__/fixtures/bindable-classes.artifact.ts
@@ -0,0 +1,35 @@
// Copyright IBM Corp. 2019. All Rights Reserved.
// Node module: @loopback/boot
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {bind, BindingScope, Provider} from '@loopback/core';

@bind({
tags: {serviceType: 'local'},
scope: BindingScope.SINGLETON,
})
export class BindableGreetingService {
greet(whom: string = 'world') {
return Promise.resolve(`Hello ${whom}`);
}
}

@bind({tags: {serviceType: 'local', name: 'CurrentDate'}})
export class DateProvider implements Provider<Date> {
value(): Promise<Date> {
return Promise.resolve(new Date());
}
}

export class NotBindableGreetingService {
greet(whom: string = 'world') {
return Promise.resolve(`Hello ${whom}`);
}
}

export class NotBindableDateProvider implements Provider<Date> {
value(): Promise<Date> {
return Promise.resolve(new Date());
}
}
Expand Up @@ -21,8 +21,10 @@ describe('service booter integration tests', () => {

it('boots services when app.boot() is called', async () => {
const expectedBindings = [
`${SERVICES_PREFIX}.GeocoderService`,
// greeting service is skipped - service classes are not supported (yet)
'services.BindableGreetingService',
'services.CurrentDate',
'services.GeocoderService',
'services.NotBindableDate',
];

await app.boot();
Expand All @@ -31,6 +33,18 @@ describe('service booter integration tests', () => {
expect(bindings.sort()).to.eql(expectedBindings.sort());
});

it('boots bindable classes when app.boot() is called', async () => {
const expectedBindings = [
`${SERVICES_PREFIX}.CurrentDate`,
`${SERVICES_PREFIX}.BindableGreetingService`,
];

await app.boot();

const bindings = app.findByTag({serviceType: 'local'}).map(b => b.key);
expect(bindings.sort()).to.eql(expectedBindings.sort());
});

async function getApp() {
await sandbox.copyFile(resolve(__dirname, '../fixtures/application.js'));
await sandbox.copyFile(
Expand All @@ -43,6 +57,11 @@ describe('service booter integration tests', () => {
'services/greeting.service.js',
);

await sandbox.copyFile(
resolve(__dirname, '../fixtures/bindable-classes.artifact.js'),
'services/bindable-classes.service.js',
);

const MyApp = require(resolve(SANDBOX_PATH, 'application.js')).BooterApp;
app = new MyApp();
}
Expand Down
Expand Up @@ -27,7 +27,7 @@ describe('service booter unit tests', () => {
beforeEach(createStub);
afterEach(restoreStub);

it('gives a warning if called on an app without RepositoryMixin', async () => {
it('does not require service mixin', async () => {
const normalApp = new Application();
await sandbox.copyFile(
resolve(__dirname, '../../fixtures/service-provider.artifact.js'),
Expand All @@ -43,12 +43,7 @@ describe('service booter unit tests', () => {
];
await booterInst.load();

sinon.assert.calledOnce(stub);
sinon.assert.calledWith(
stub,
'app.serviceProvider() function is needed for ServiceBooter. You can add ' +
'it to your Application using ServiceMixin from @loopback/service-proxy.',
);
sinon.assert.notCalled(stub);
});

it(`uses ServiceDefaults for 'options' if none are given`, () => {
Expand Down
52 changes: 28 additions & 24 deletions packages/boot/src/booters/service.booter.ts
Expand Up @@ -3,14 +3,20 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {
BINDING_METADATA_KEY,
Constructor,
inject,
MetadataInspector,
} from '@loopback/context';
import {CoreBindings} from '@loopback/core';
import {ApplicationWithServices} from '@loopback/service-proxy';
import {inject, Provider, Constructor} from '@loopback/context';
import * as debugFactory from 'debug';
import {ArtifactOptions} from '../interfaces';
import {BaseArtifactBooter} from './base-artifact.booter';
import {BootBindings} from '../keys';
import {BaseArtifactBooter} from './base-artifact.booter';

type ServiceProviderClass = Constructor<Provider<object>>;
const debug = debugFactory('loopback:boot:service-booter');

/**
* A class that extends BaseArtifactBooter to boot the 'Service' artifact type.
Expand All @@ -23,8 +29,6 @@ type ServiceProviderClass = Constructor<Provider<object>>;
* @param bootConfig - Service Artifact Options Object
*/
export class ServiceBooter extends BaseArtifactBooter {
serviceProviders: ServiceProviderClass[];

constructor(
@inject(CoreBindings.APPLICATION_INSTANCE)
public app: ApplicationWithServices,
Expand All @@ -46,24 +50,12 @@ export class ServiceBooter extends BaseArtifactBooter {
async load() {
await super.load();

this.serviceProviders = this.classes.filter(isServiceProvider);
for (const cls of this.classes) {
if (!isBindableClass(cls)) continue;

/**
* If Service providers were discovered, we need to make sure ServiceMixin
* was used (so we have `app.serviceProvider()`) to perform the binding of a
* Service provider class.
*/
if (this.serviceProviders.length > 0) {
if (!this.app.serviceProvider) {
console.warn(
'app.serviceProvider() function is needed for ServiceBooter. You can add ' +
'it to your Application using ServiceMixin from @loopback/service-proxy.',
);
} else {
this.serviceProviders.forEach(cls => {
this.app.serviceProvider(cls as Constructor<Provider<object>>);
});
}
debug('Bind class: %s', cls.name);
const binding = this.app.service(cls);
debug('Binding created for class: %j', binding);
}
}
}
Expand All @@ -77,8 +69,20 @@ export const ServiceDefaults: ArtifactOptions = {
nested: true,
};

function isServiceProvider(cls: Constructor<{}>): cls is ServiceProviderClass {
function isServiceProvider(cls: Constructor<unknown>) {
const hasSupportedName = cls.name.endsWith('Provider');
const hasValueMethod = 'value' in cls.prototype;
const hasValueMethod = typeof cls.prototype.value === 'function';
return hasSupportedName && hasValueMethod;
}

function isBindableClass(cls: Constructor<unknown>) {
if (MetadataInspector.getClassMetadata(BINDING_METADATA_KEY, cls)) {
return true;
}
if (isServiceProvider(cls)) {
debug('Provider class found: %s', cls.name);
return true;
}
debug('Skip class not decorated with @bind: %s', cls.name);
return false;
}
17 changes: 16 additions & 1 deletion packages/core/src/__tests__/unit/application.unit.ts
Expand Up @@ -270,11 +270,26 @@ describe('Application', () => {
const binding = app.service(MyServiceProvider);
expect(Array.from(binding.tagNames)).to.containEql(CoreTags.SERVICE);
expect(binding.tagMap.date).to.eql('now');
expect(binding.key).to.equal('localServices.MyServiceProvider');
expect(binding.key).to.equal('localServices.MyService');
expect(binding.scope).to.equal(BindingScope.TRANSIENT);
expect(findKeysByTag(app, 'service')).to.containEql(binding.key);
});

it('binds a service provider with name tag', () => {
@bind({tags: {date: 'now', name: 'my-service'}})
class MyServiceProvider implements Provider<Date> {
value() {
return new Date();
}
}

const binding = app.service(MyServiceProvider);
expect(Array.from(binding.tagNames)).to.containEql(CoreTags.SERVICE);
expect(binding.tagMap.date).to.eql('now');
expect(binding.key).to.equal('services.my-service');
expect(findKeysByTag(app, 'service')).to.containEql(binding.key);
});

function givenApp() {
app = new Application();
}
Expand Down

0 comments on commit 48e01f4

Please sign in to comment.