-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): Add support for extension point/extension
Extension point/extension is a pattern for extensibility. This PR illustrates how we can support it using the Context apis and naming conventions. It also serves as an invitation to discuss if we should support such entities out of box.
- Loading branch information
Raymond Feng
committed
Oct 19, 2017
1 parent
d52f637
commit 6ba46f5
Showing
5 changed files
with
473 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
// Copyright IBM Corp. 2017. All Rights Reserved. | ||
// Node module: @loopback/core | ||
// This file is licensed under the MIT License. | ||
// License text available at https://opensource.org/licenses/MIT | ||
|
||
import {Context, Binding} from '@loopback/context'; | ||
|
||
import {Application, ApplicationConfig} from './application'; | ||
|
||
// tslint:disable:no-any | ||
/** | ||
* Interface for the extension point configuration | ||
*/ | ||
export interface ExtensionPointConfig { | ||
// Configuration properties for extensions keyed by the extension name | ||
extensions: { | ||
[extensionName: string]: any; | ||
}; | ||
// Configuration properties for the extension point itself | ||
[property: string]: any; | ||
} | ||
|
||
/** | ||
* Base class for extension points | ||
*/ | ||
export abstract class ExtensionPoint<EXT extends object> { | ||
/** | ||
* Configuration (typically to be injected) | ||
*/ | ||
protected configuration: ExtensionPointConfig; | ||
|
||
constructor( | ||
/** | ||
* The unique name of this extension point. It also serves as the binding | ||
* key prefix for bound extensions | ||
*/ | ||
protected name: string, | ||
/** | ||
* The Context (typically to be injected) | ||
*/ | ||
protected context: Context, | ||
/** | ||
* Configuration (typically to be injected) | ||
*/ | ||
configuration?: ExtensionPointConfig, | ||
) { | ||
this.configuration = configuration || {extensionPoint: {}, extensions: {}}; | ||
} | ||
|
||
/** | ||
* Find an array of bindings for extensions | ||
*/ | ||
getAllExtensionBindings(): Binding[] { | ||
return this.context.findByTag(`extensionPoint:${this.name}`); | ||
} | ||
|
||
/** | ||
* Get a map of extension bindings by the keys | ||
*/ | ||
getExtensionBindingMap(): {[name: string]: Binding} { | ||
const extensions: {[name: string]: Binding} = {}; | ||
const bindings = this.getAllExtensionBindings(); | ||
bindings.forEach(binding => { | ||
extensions[binding.key] = binding; | ||
}); | ||
return extensions; | ||
} | ||
|
||
/** | ||
* Look up an extension binding by name | ||
* @param extensionName Name of the extension | ||
*/ | ||
getExtensionBinding(extensionName: string): Binding { | ||
const bindings = this.getAllExtensionBindings(); | ||
const binding = bindings.find(b => b.tags.has(`name:${extensionName}`)); | ||
if (binding == null) | ||
throw new Error( | ||
`Extension ${extensionName} does not exist for extension point ${this | ||
.name}`, | ||
); | ||
return binding; | ||
} | ||
|
||
/** | ||
* Get an instance of an extension by name | ||
* @param extensionName Name of the extension | ||
*/ | ||
async getExtension(extensionName: string): Promise<EXT> { | ||
const binding = this.getExtensionBinding(extensionName); | ||
// Create a child context to bind `config` | ||
const extensionContext = new Context(this.context); | ||
extensionContext | ||
.bind('config') | ||
.to(this.configuration.extensions[extensionName]); | ||
return await binding.getValue(extensionContext); | ||
} | ||
} |
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,78 @@ | ||
# Feature: Context bindings - organizing services as extension point/extensions by naming convention | ||
|
||
- In order to manage services that follow the extension point/extension pattern | ||
- As a developer | ||
- I would like to bind a extension point and its extensions to the context following certain conventions | ||
- So that an extension point can find all of its extensions or one of them by name | ||
|
||
See https://wiki.eclipse.org/FAQ_What_are_extensions_and_extension_points%3F for the pattern. | ||
|
||
# Scenario - register an extension to a given extension point | ||
|
||
- Given a context | ||
- Given a class `AuthenticationManager` as the extension point for extensible authentication strategies | ||
- Given a class `LocalStrategy` that implements the `local` authentication strategy | ||
- Given a class `LDAPStrategy` that implements the `ldap` authentication strategy | ||
- Given a class `OAuth2Strategy` that implements the `oauth2` authentication strategy | ||
- We should be able to add `LocalStrategy` by binding it to `authentication.strategies.local` | ||
- We should be able to add `LDAPStrategy` by binding it to `authentication.strategies.ldap` | ||
- We should be able to add `OAuth2Strategy` by binding it to `authentication.strategies.oauth2` | ||
|
||
# Scenario - find all of its extensions for a given extension point | ||
|
||
- Given a context | ||
- Given a class `AuthenticationManager` as the extension point for extensible authentication strategies | ||
- Given a class `LocalStrategy` that implements the `local` authentication strategy | ||
- Given a class `LDAPStrategy` that implements the `ldap` authentication strategy | ||
- Given a class `OAuth2Strategy` that implements the `oauth2` authentication strategy | ||
- When LocalStrategy is bound to `authentication.strategies.local` | ||
- When LDAPStrategy is bound to `authentication.strategies.ldap` | ||
- When OAuth2Strategy is bound to `authentication.strategies.oauth2` | ||
- AuthenticationManager should be able to list all extensions bound to `authentication.strategies.*` | ||
|
||
# Scenario - find one of its extensions by name for a given extension point | ||
|
||
- Given a context | ||
- Given a class `AuthenticationManager` as the extension point for extensible authentication strategies | ||
- Given a class `LocalStrategy` that implements the `local` authentication strategy | ||
- Given a class `LDAPStrategy` that implements the `ldap` authentication strategy | ||
- Given a class `OAuth2Strategy` that implements the `oauth2` authentication strategy | ||
- When LocalStrategy is bound to `authentication.strategies.local` | ||
- When LDAPStrategy is bound to `authentication.strategies.ldap` | ||
- When OAuth2Strategy is bound to `authentication.strategies.oauth2` | ||
- AuthenticationManager should be able to find/get LocalStrategy by `local` from the context | ||
- AuthenticationManager should be able to find/get LDAPStrategy by `ldap` from the context | ||
- AuthenticationManager should be able to find/get OAuth2Strategy by `oauth2` from the context | ||
|
||
# Scenario - populate configuration for extension points and extension | ||
|
||
- Given a context | ||
- Given a class `AuthenticationManager` as the extension point for extensible authentication strategies | ||
- Given a class `LocalStrategy` that implements the `local` authentication strategy | ||
- Given a class `LDAPStrategy` that implements the `ldap` authentication strategy | ||
- Given a class `OAuth2Strategy` that implements the `oauth2` authentication strategy | ||
- When LocalStrategy is bound to `authentication.strategies.local` | ||
- When LDAPStrategy is bound to `authentication.strategies.ldap` | ||
- When OAuth2Strategy is bound to `authentication.strategies.oauth2` | ||
- Given a configuration object | ||
```js | ||
{ | ||
'authentication.strategies': { | ||
local: { /* config for local strategy */}, | ||
ldap: { /* config for ldap strategy */} | ||
oauth2: { /* config for oauth2 strategy */} | ||
} | ||
} | ||
``` | ||
- Each of the strategy class should be able to receive its configuration via dependency injection. For example, | ||
|
||
```ts | ||
class LocalStrategy implements AuthenticationStrategy { | ||
constructor(@inject('config') private config: LocalConfig) {} | ||
} | ||
``` | ||
|
||
# TBD | ||
|
||
- Should we go beyond naming conventions to make ExtensionPoint/Extension first class entity of the context? | ||
- Should we introduce decorators such as `@extensionPoint` and `@extension`? |
Oops, something went wrong.