Skip to content

Commit

Permalink
Implement application components
Browse files Browse the repository at this point in the history
A component can be registered with an application either at
construction time via AppOptions, or by calling `app.component()` later.

This first version allows components to contribute controllers
and arbitrary provider bindings.
  • Loading branch information
bajtos authored and Raymond Feng committed Jun 9, 2017
1 parent 25b95f9 commit 1ca0e8e
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 31 deletions.
38 changes: 29 additions & 9 deletions packages/core/src/application.ts
Expand Up @@ -3,9 +3,10 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Binding, Context, Constructor} from '@loopback/context';
import {Component, OpenApiSpec} from '.';
import {Binding, Context, Constructor, Provider} from '@loopback/context';
import {OpenApiSpec} from '.';
import {ServerRequest, ServerResponse} from 'http';
import {Component, mountComponent} from './component';
import {getApiSpec} from './router/metadata';
import {HttpHandler} from './http-handler';
import {Sequence} from './sequence';
Expand Down Expand Up @@ -39,11 +40,7 @@ export class Application extends Context {

if (appConfig && appConfig.components) {
for (const component of appConfig.components) {
// TODO(superkhau): Need to figure a way around this hack,
// `componentClassName.constructor.name` + `componentClassName.name`
// doesn't work
const componentClassName = component.toString().split(' ')[1];
this.bind(`component.${componentClassName}`).toClass(component);
this.component(component);
}
}

Expand Down Expand Up @@ -107,13 +104,36 @@ export class Application extends Context {
return this.bind('controllers.' + controllerCtor.name).toClass(controllerCtor);
}

/**
* Add a component to this application.
*
* @param component The component to add.
*
* ```ts
*
* export const ProductComponent = {
* controllers = [ProductController];
* repositories = [ProductRepo, UserRepo];
* providers = {
* [AUTHENTICATION_STRATEGY]: AuthStrategy,
* [AUTHORIZATION_ROLE]: Role,
* },
* };
*
* app.component(ProductComponent);
* ```
*/
public component(component: Component) {
mountComponent(this, component);
}

protected _logError(err: Error, statusCode: number, req: ServerRequest): void {
console.error('Unhandled error in %s %s: %s %s',
req.method, req.url, statusCode, err.stack || err);
}
}

export interface AppConfig {
components: Array<Constructor<Component>>;
sequences: Array<Constructor<Sequence>>;
components?: Array<Component>;
sequences?: Array<Constructor<Sequence>>;
}
22 changes: 22 additions & 0 deletions packages/core/src/component.ts
Expand Up @@ -3,6 +3,28 @@
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {Constructor, Provider} from '@loopback/context';
import {Application} from '.';

// tslint:disable:no-any

export interface Component {
controllers?: Constructor<any>[];
providers?: {
[key: string]: Constructor<Provider<any>>;
};
}

export function mountComponent(app: Application, component: Component) {
if (component.controllers) {
for (const controllerCtor of component.controllers) {
app.controller(controllerCtor);
}
}

if (component.providers) {
for (const key in component.providers) {
app.bind(key).toProvider(component.providers[key]);
}
}
}
Expand Up @@ -4,35 +4,51 @@
// License text available at https://opensource.org/licenses/MIT

import {expect} from '@loopback/testlab';
import {Application, Sequence} from '../../..';
import {Application, Sequence, Component} from '../../..';

describe('Bootstrapping - Application', () => {
context('with user-defined configurations', () => {
let app: Application;
before(givenAppWithConfigs);

it('registers the given components', () => {
expect(app.find('component.*'))
.to.be.instanceOf(Array)
.with.lengthOf(4);
});

it('registers the given sequence', () => {
const app = new Application({sequences: [Sequence]});
expect(app.find('sequence.*'))
.to.be.instanceOf(Array)
.with.lengthOf(1);
});

function givenAppWithConfigs() {
class Todo { }
class Authentication { }
class Authorization { }
class Rejection { }
class TodoSequence extends Sequence { }
app = new Application({
components: [Todo, Authentication, Authorization, Rejection],
sequences: [TodoSequence],
});
}
it('registers all providers from components', async () => {
const component: Component = {
providers: {
'foo': class FooProvider {
value() { return 'bar'; }
},
},
};

const app = new Application({components: [component]});

// FIXME(bajtos) await should not be needed once this patch is landed:
// https://github.com/strongloop/loopback-next/pull/357
// As part of this fix, remove "async" keyword from the test function too
const value = await app.get('foo');
expect(value).to.equal('bar');
});

it('registers all controllers from components', () => {
// TODO(bajtos) Beef up this test. Create a real controller with
// a public API endpoint and verify that this endpoint can be invoked
// via HTTP/REST API.

class MyController {
}

const component: Component = {
controllers: [MyController],
};

const app = new Application({components: [component]});

expect(app.find('controllers.*').map(b => b.key))
.to.eql(['controllers.MyController']);
});
});
});
Expand Up @@ -23,6 +23,5 @@ const app = new Application({
});

// get metadata about the registered components
console.log(app.find('component.*')); // [Bindings] should match the 4 components registered above
console.log(app.find('sequence.*')); // [Bindings] should match the 1 sequence registered above
```

0 comments on commit 1ca0e8e

Please sign in to comment.