Skip to content

Commit

Permalink
feat: add greeter-extension example
Browse files Browse the repository at this point in the history
The example illustrates how to implement extension point/extenion
pattern in LoopBack 4 to provide great extensibility.
  • Loading branch information
raymondfeng committed Jan 15, 2019
1 parent b25d990 commit 25ad6d9
Show file tree
Hide file tree
Showing 23 changed files with 598 additions and 2 deletions.
1 change: 1 addition & 0 deletions examples/greeter-extension/.npmrc
@@ -0,0 +1 @@
package-lock=false
2 changes: 2 additions & 0 deletions examples/greeter-extension/.prettierignore
@@ -0,0 +1,2 @@
dist
*.json
6 changes: 6 additions & 0 deletions examples/greeter-extension/.prettierrc
@@ -0,0 +1,6 @@
{
"bracketSpacing": false,
"singleQuote": true,
"printWidth": 80,
"trailingComma": "all"
}
21 changes: 21 additions & 0 deletions examples/greeter-extension/.vscode/settings.json
@@ -0,0 +1,21 @@
{
"editor.rulers": [80],
"editor.tabCompletion": "on",
"editor.tabSize": 2,
"editor.trimAutoWhitespace": true,
"editor.formatOnSave": true,

"files.exclude": {
"**/.DS_Store": true,
"**/.git": true,
"**/.hg": true,
"**/.svn": true,
"**/CVS": true,
"dist": true,
},
"files.insertFinalNewline": true,
"files.trimTrailingWhitespace": true,

"tslint.ignoreDefinitionFiles": true,
"typescript.tsdk": "./node_modules/typescript/lib"
}
29 changes: 29 additions & 0 deletions examples/greeter-extension/.vscode/tasks.json
@@ -0,0 +1,29 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Watch and Compile Project",
"type": "shell",
"command": "npm",
"args": ["--silent", "run", "build:watch"],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": "$tsc-watch"
},
{
"label": "Build, Test and Lint",
"type": "shell",
"command": "npm",
"args": ["--silent", "run", "test:dev"],
"group": {
"kind": "test",
"isDefault": true
},
"problemMatcher": ["$tsc", "$tslint5"]
}
]
}
25 changes: 25 additions & 0 deletions examples/greeter-extension/LICENSE
@@ -0,0 +1,25 @@
Copyright (c) Author 2018. All Rights Reserved.
Node module: @loopback/example-greeter-extension
This project is licensed under the MIT License, full text below.

--------

MIT license

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
160 changes: 160 additions & 0 deletions examples/greeter-extension/README.md
@@ -0,0 +1,160 @@
# @loopback/example-greeter-extension

This example project illustrates how to implement the
[Extension Point/extension pattern](https://wiki.eclipse.org/FAQ_What_are_extensions_and_extension_points%3F),
which promotes loose coupling and offers great extensibility. There are many use
cases in LoopBack 4 that fit into design pattern. For example:

- `@loopback/boot` uses `BootStrapper` that delegates to `Booters` to handle
different types of artifacts
- `@loopback/rest` uses `RequestBodyParser` that finds the corresponding
`BodyParsers` to parse request body encoded in different media types

## Overview

We'll use the following scenario to walk through important steps to organize the
`greet` service that allows extensible languages - each of them being supported
by a `Greeter` extension.

![greeters](greeters.png)

Various constructs from LoopBack 4, such as `Context`, `@inject.*`, and
`Component` are used to build the service in an extensible fashion.

## Define an extension point

In our scenario, we want to allow other modules to extend or customize how
people are greeted in different languages. To achieve that, we declare the
`greeter` extension point, which declares a contract as a TypeScript interface
that extensions must conform to.

### Define interface for extensions

```ts
/**
* Typically an extension point defines an interface as the contract for
* extensions to implement
*/
export interface Greeter {
language: string;
greet(name: string): string;
}
```

### Define class for the extension point

```ts
/**
* An extension point for greeters that can greet in different languages
*/
export class GreeterExtensionPoint {
constructor(
/**
* Inject a getter function to fetch greeters (bindings tagged with
* 'greeter')
*/
@inject.getter(bindingTagFilter({extensionPoint: 'greeter'}))
private greeters: Getter<Greeter[]>,
) {}
```
#### Access extensions for a given extension point
To simplify access to extensions for a given extension point, we use dependency
injection to receive a `getter` function that gives us a list of greeters.
#### Implement the delegation logic
Typically, the extension point implementation will get a list of registered
extensions. For example, when a person needs to be greeted in a specific
language, the code iterates through all greeters to find an instance that
matches the language.
## Implement an extension
Modules that want to connect to `greeter` extension point must implement
`Greeter` interface in their extension. The key attribute is that the
`GreeterExtensionPoint` being extended knows nothing about the module that is
connecting to it beyond the scope of that contract. This allows `greeters` built
by different individuals or companies to interact seamlessly, even without their
knowing much about one another.
## Register an extension point
To register an extension point, we simply bind the implementation class to a
`Context`. For example:
```ts
app
.bind('greeter-extension-point')
.toClass(GreeterExtensionPoint)
.inScope(BindingScope.SINGLETON);
```
The process can be automated with a component too:
```ts
import {createBindingFromClass} from '@loopback/context';
import {Component} from '@loopback/core';
import {GreeterExtensionPoint} from './greeter-extension-point';

export class GreeterComponent implements Component {
bindings = [
createBindingFromClass(GreeterExtensionPoint, {
key: 'greeter-extension-point',
}),
];
}
```
## Register extensions
To connect an extension to an extension point, we just have to bind the
extension to the `Context` and tag the binding with
`{extensionPoint: 'greeter'}`.
```ts
app
.bind('greeters.FrenchGreeter')
.toClass(FrenchGreeter)
.apply(asGreeter);
```
Please note `asGreeter` is a binding template function, which is equivalent as
configuring a binding with `{extensionPoint: 'greeter'}` tag and in the
`SINGLETON` scope.
```ts
/**
* A binding template for greeter extensions
* @param binding
*/
export const asGreeter: BindingTemplate = binding =>
binding.inScope(BindingScope.SINGLETON).tag({extensionPoint: 'greeter'});
```
## Configure an extension point
Sometimes it's desirable to make the extension point configurable.
## Configure an extension
Some extensions also support customization.
## Contributions
- [Guidelines](https://github.com/strongloop/loopback-next/blob/master/docs/CONTRIBUTING.md)
- [Join the team](https://github.com/strongloop/loopback-next/issues/110)
## Tests
Run `npm test` from the root folder.
## Contributors
See
[all contributors](https://github.com/strongloop/loopback-next/graphs/contributors).
## License
MIT
Binary file added examples/greeter-extension/greeters.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions examples/greeter-extension/index.d.ts
@@ -0,0 +1,6 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/example-greeter-extension
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './dist';
6 changes: 6 additions & 0 deletions examples/greeter-extension/index.js
@@ -0,0 +1,6 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/example-greeter-extension
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

module.exports = require('./dist');
6 changes: 6 additions & 0 deletions examples/greeter-extension/index.ts
@@ -0,0 +1,6 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/example-log-extension
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

export * from './src';
60 changes: 60 additions & 0 deletions examples/greeter-extension/package.json
@@ -0,0 +1,60 @@
{
"name": "@loopback/example-greeter-extension",
"version": "1.0.0-1",
"description": "An example extension point/extensions for LoopBack 4",
"main": "index.js",
"engines": {
"node": ">=8.9"
},
"scripts": {
"build:apidocs": "lb-apidocs",
"build": "lb-tsc es2017 --outDir dist",
"build:watch": "lb-tsc es2017 --outDir dist --watch",
"clean": "lb-clean *example-greeter-extension-*.tgz dist package api-docs",
"lint": "npm run prettier:check && npm run tslint",
"lint:fix": "npm run tslint:fix && npm run prettier:fix",
"prettier:cli": "lb-prettier \"**/*.ts\" \"**/*.js\"",
"prettier:check": "npm run prettier:cli -- -l",
"prettier:fix": "npm run prettier:cli -- --write",
"tslint": "lb-tslint",
"tslint:fix": "npm run tslint -- --fix",
"pretest": "npm run clean && npm run build",
"test": "lb-mocha \"dist/test/unit/**/*.js\" \"dist/test/acceptance/**/*.js\"",
"posttest": "npm run lint",
"test:dev": "lb-mocha --allow-console-logs dist/test/**/*.js && npm run posttest",
"verify": "npm pack && tar xf *example-greeter-extension*.tgz && tree package && npm run clean"
},
"repository": {
"type": "git",
"url": "git+https://github.com/strongloop/loopback-next.git"
},
"publishConfig": {
"access": "public"
},
"keywords": [
"loopback",
"loopback-extension"
],
"license": "MIT",
"bugs": {
"url": "https://github.com/strongloop/loopback-next/issues"
},
"homepage": "https://github.com/strongloop/loopback-next/tree/master/examples/greeter-extension",
"devDependencies": {
"@loopback/build": "^1.1.0",
"@loopback/testlab": "^1.0.3",
"@loopback/tslint-config": "^1.0.0",
"@types/debug": "0.0.30",
"@types/node": "^10.11.2",
"tslint": "^5.12.0",
"typescript": "^3.2.2"
},
"dependencies": {
"@loopback/context": "^1.4.0",
"@loopback/core": "^1.1.3",
"@loopback/openapi-v3": "^1.1.5",
"@loopback/rest": "^1.5.1",
"chalk": "^2.4.2",
"debug": "^4.0.1"
}
}
20 changes: 20 additions & 0 deletions examples/greeter-extension/src/component.ts
@@ -0,0 +1,20 @@
// Copyright IBM Corp. 2018. All Rights Reserved.
// Node module: @loopback/example-greeter-extension
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

import {createBindingFromClass} from '@loopback/context';
import {Component} from '@loopback/core';
import {ChineseGreeter} from './greeters/greeter-cn';
import {EnglishGreeter} from './greeters/greeter-en';
import {GreeterExtensionPoint} from './greeter-extension-point';

export class GreeterComponent implements Component {
bindings = [
createBindingFromClass(GreeterExtensionPoint, {
key: 'greeter-extension-point',
}),
createBindingFromClass(EnglishGreeter, {namespace: 'greeters'}),
createBindingFromClass(ChineseGreeter, {namespace: 'greeters'}),
];
}

0 comments on commit 25ad6d9

Please sign in to comment.