Skip to content

Commit e235bc1

Browse files
committed
Add mixin that implements a common adapter pattern
1 parent d3fe2fc commit e235bc1

File tree

16 files changed

+762
-12
lines changed

16 files changed

+762
-12
lines changed

README.md

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,159 @@
1-
# Ember-cli-adapter-pattern
1+
# Ember-cli-adapter-pattern [![Build Status](https://travis-ci.org/tomasbasham/ember-cli-adapter-pattern.svg?branch=master)](https://travis-ci.org/tomasbasham/ember-cli-adapter-pattern)
22

3-
This README outlines the details of collaborating on this Ember addon.
3+
An [Ember CLI](http://www.ember-cli.com/) addon to standardise a common adapter pattern.
4+
5+
The adapter pattern helps to provide a common interface from which two incompatible interface may work together. For example you may wish to include in your applications the ability to authenticate users through a variety of social platforms (i.e. Facebook and Twitter). Each platform defines its own API which to interface with it's servers. Using the adapter pattern you can separate the logic of both APIs into their own adapter objects, using a common interface to work with both.
6+
7+
This addon implements a common adapter pattern that can be included in any ember object allowing it to act as a proxy between the application and any external interface.
48

59
## Installation
610

11+
From within your Ember CLI project directory run:
12+
```
13+
ember install ember-cli-adapter-pattern
14+
```
15+
16+
## Usage
17+
18+
This addon implements a mixin that should be included in any object you wish to act as the interface between your application and any external platform or API.
19+
20+
### Adaptable Mixin
21+
22+
In order to implement the adapter pattern, it is recommended you include the `Adaptable` mixin into an ember object that will act as a singleton, i.e. a service.
23+
24+
##### <a name="adaptable-example"></a>Example:
25+
26+
```JavaScript
27+
// app/services/social.js
28+
import Ember from 'ember';
29+
import Adaptable from 'ember-cli-adapter-pattern/mixins/adaptable';
30+
import proxyToAdapter from 'ember-cli-adapter-pattern/utils/proxy-to-adapter';
31+
32+
export default Ember.Service.extend(Adaptable, {
33+
login: proxyToAdapter('login'), // Provides a safe method to proxy your API to each adapter.
34+
35+
/*
36+
* There are potentially many ways
37+
* to activate adapters, but it is
38+
* imperative that somewhere the
39+
* `activateAdapters` method is
40+
* called.
41+
*/
42+
createAdapters: Ember.on('init', function() {
43+
const adapters = Ember.getWithDefault(this, 'property.with.adapter.configurations', Ember.A());
44+
45+
this.set('_adapters', {});
46+
this.set('context', {});
47+
48+
this.activateAdapters(adapters); // This is important and activates configured adapters.
49+
}),
50+
51+
/*
52+
* This is the only method that you
53+
* are required to write and defines
54+
* how you look up your adapters. It
55+
* is not limited to using the
56+
* container.
57+
*/
58+
_lookupAdapter(adapterName) {
59+
Ember.assert('Could not find adapter without a name', adapterName);
60+
61+
const container = this.get('container');
62+
const dasherizedAdapterName = Ember.String.dasherize(adapterName);
63+
const adapter = container.lookup(`container:${dasherizedAdapterName}`);
64+
65+
return adapter;
66+
}
67+
});
68+
```
69+
70+
This creates a service that will act as the common API for each of your adapters. Here you can see I have defined a single common API method named `login`.
71+
72+
### Making API Calls
73+
74+
To make calls to your API you must inject the `Adaptable` object into another ember object (i.e. a controller) and invoke it as normal.
75+
76+
##### <a name="all-adapters-example"></a>Example:
77+
78+
```JavaScript
79+
// app/controller/application.js
80+
import Ember from 'ember';
81+
82+
export default Ember.Controller.extend({
83+
social: Ember.Service.inject(),
84+
85+
actions: {
86+
loginWithService() {
87+
this.get('social').login({ username: 'Jean-Luc Picard', password: 'Enterprise-D' });
88+
}
89+
}
90+
});
91+
```
92+
93+
The action defined in the controller will call the `login` method on the `social` service. This will in turn forward the invocation on to each of the adapters. Of course in this example it would make little sense to login with more than one platform at the same time. If you wish to call only one adapter then you must pass in it's name to the API call.
94+
95+
##### <a name="single-adapter-example"></a>Example:
96+
97+
```JavaScript
98+
// app/controller/application.js
99+
import Ember from 'ember';
100+
101+
export default Ember.Controller.extend({
102+
social: Ember.Service.inject(),
103+
104+
actions: {
105+
loginWithService() {
106+
this.get('social').login('Facebook', { username: 'Jean-Luc Picard', password: 'Enterprise-D' });
107+
}
108+
}
109+
});
110+
```
111+
112+
This will only make the API call to the 'Facebook' adapter.
113+
114+
Each API call will be wrapped within a promise that resolves with the returned result of the adapter mapped to the adapter name.
115+
116+
### ProxyToAdapter Utility
117+
118+
The `proxyToAdapter` utility method simply forwards calls made to each of your API methods to all defined adapters, or just a single adapter if specified. It is recommended you use `proxyToAdapter` because it implements guard statements to prevent the application from throwing errors.
119+
120+
If however you need to extend the functionality of your API methods then somewhere in its implementation it needs to call the `invoke` method defined within the `Adaptable` mixin.
121+
122+
```JavaScript
123+
// app/services/social.js
124+
import Ember from 'ember';
125+
import Adaptable from 'ember-cli-adapter-pattern/mixins/adaptable';
126+
127+
export default Ember.Service.extend(Adaptable, {
128+
login(...args) {
129+
// Extended operations here.
130+
this.invoke('login', ...args);
131+
}
132+
});
133+
```
134+
135+
Here we are passing the name of the API call as the first argument to `invoke` followed by the arguments that were passed into the API call.
136+
137+
## Development
138+
139+
### Installation
140+
7141
* `git clone` this repository
8142
* `npm install`
9143
* `bower install`
10144

11-
## Running
145+
### Running
12146

13147
* `ember server`
14148
* Visit your app at http://localhost:4200.
15149

16-
## Running Tests
150+
### Running Tests
17151

18152
* `npm test` (Runs `ember try:testall` to test your addon against multiple Ember versions)
19153
* `ember test`
20154
* `ember test --server`
21155

22-
## Building
156+
### Building
23157

24158
* `ember build`
25159

addon/mixins/adaptable.js

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import Ember from 'ember';
2+
import requiredMethod from 'ember-cli-adapter-pattern/utils/required-method';
3+
4+
const {
5+
assert,
6+
copy,
7+
get,
8+
merge,
9+
on,
10+
set
11+
} = Ember;
12+
13+
const {
14+
resolve
15+
} = Ember.RSVP;
16+
17+
const {
18+
keys
19+
} = Object;
20+
21+
export default Ember.Mixin.create({
22+
23+
/*
24+
* A cache of active adapters to save
25+
* time on expensive container lookups.
26+
*
27+
* @type {Object}
28+
*/
29+
_adapters: null,
30+
31+
/*
32+
* Extra information you can attach to
33+
* every adapter call. This can be handy
34+
* when there is a value that needs to
35+
* be present with every aapter call,
36+
* reducing the need to pass the value
37+
* each time.
38+
*
39+
* @type {Object}
40+
*/
41+
context: null,
42+
43+
/*
44+
* Instantiates a series of adapters as
45+
* defined in the application config and
46+
* caches them to save on expensive future
47+
* lookups.
48+
*
49+
* @method activateAdapters
50+
*
51+
* @param {Array} adapterOptions
52+
* Adapter configuration options.
53+
*/
54+
activateAdapters(adapterOptions) {
55+
const cachedAdapters = get(this, '_adapters');
56+
const activatedAdapters = {};
57+
58+
adapterOptions.forEach((adapterOption) => {
59+
const { name } = adapterOption;
60+
const adapter = cachedAdapters[name] ? cachedAdapters[name] : this.activateAdapter(adapterOption);
61+
62+
set(activatedAdapters, name, adapter);
63+
});
64+
65+
set(this, '_adapters', activatedAdapters);
66+
},
67+
68+
/*
69+
* Instantiates a single adapter from a
70+
* configuration object.
71+
*
72+
* @method activateAdapter
73+
*
74+
* @params {Object} adapterOptions
75+
* Adapter configuration options. Must have a name property, and optioanlly a config property.
76+
*
77+
* @return {Object}
78+
* An instantiated adpater.
79+
*/
80+
activateAdapter({ name, config } = {}) {
81+
const adapter = this._lookupAdapter(name);
82+
assert(`Could not find adapter ${name}`, adapter);
83+
84+
return adapter.create({ this, config });
85+
},
86+
87+
/*
88+
* Invoke a method on a registered
89+
* adpater. If a specific adapter
90+
* name is supplied then the method
91+
* will only be invoked on that
92+
* adapter, providing it exists.
93+
*
94+
* @method invoke
95+
*
96+
* @param {String} methodName
97+
* The name of the method to invoke.
98+
*
99+
* @param {Rest} args
100+
* Any other supplied arguments.
101+
*
102+
* @return {Ember.RSVP}
103+
* A hash of promise objects.
104+
*/
105+
invoke(methodName, ...args) {
106+
const cachedAdapters = get(this, '_adapters');
107+
const adapterNames = keys(cachedAdapters);
108+
const [selectedAdapterNames, options] = args.length > 1 ? [[args[0]], args[1]] : [adapterNames, args[0]];
109+
const context = copy(get(this, 'context'));
110+
const mergedOptions = merge(context, options);
111+
112+
// Store a promise for each adapter response.
113+
const promises = {};
114+
115+
selectedAdapterNames.map((adapterName) => {
116+
const adapter = get(cachedAdapters, adapterName);
117+
promises[adapterName] = resolve(adapter[methodName].call(adapter, mergedOptions));
118+
});
119+
120+
return Ember.RSVP.hash(promises);
121+
},
122+
123+
/*
124+
* Ensure that we have a clean cache
125+
* of adapters. It may be beneficial
126+
* to override this method in a
127+
* consuming application or addon so
128+
* the adapters can be activated
129+
* here also.
130+
*
131+
* @method createAdapters
132+
* @on init
133+
*/
134+
createAdapters: on('init', function() {
135+
set(this, '_adapters', {});
136+
set(this, 'context', {});
137+
}),
138+
139+
/*
140+
* Tear down any cached adapters.
141+
*
142+
* @method destroyAdapters
143+
* @on willDestroy
144+
*/
145+
destroyAdapters: on('willDestroy', function() {
146+
const cachedAdapters = get(this, '_adapters');
147+
148+
for(let adapterName in cachedAdapters) {
149+
get(cachedAdapters, adapterName).destroy();
150+
}
151+
}),
152+
153+
/*
154+
* An abstract method that needs to
155+
* be defined on the consuming
156+
* application or addon responsible
157+
* for the lookup of adapter objects
158+
* from the container.
159+
*
160+
* @method lookupAdapter
161+
* @private
162+
*/
163+
_lookupAdapter: requiredMethod('_lookupAdapter')
164+
});

addon/utils/proxy-to-adapter.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import Ember from 'ember';
2+
3+
const {
4+
assert
5+
} = Ember;
6+
7+
/*
8+
* Utility method, returning a proxy function
9+
* that looks up a function on the intended
10+
* adapter. The proxy funtion will return a
11+
* resolved promise with a value.
12+
*
13+
* @method proxyToAdapter
14+
*
15+
* @param {String} methodName
16+
* Name of the method to proxy.
17+
*
18+
* @return {Function}
19+
* A proxy function returning a resolved promise.
20+
*/
21+
export default function proxyToAdapter(methodName) {
22+
assert('Method name is required for proxyToAdapter.', methodName);
23+
24+
return function(...args) {
25+
if (!this.invoke && typeof this.invoke !== 'function') {
26+
throw new Ember.Error('No invoke method. Have you forgotten to include the Adaptable mixin?');
27+
}
28+
29+
return this.invoke(methodName, ...args);
30+
};
31+
}

addon/utils/required-method.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import Ember from 'ember';
2+
3+
const {
4+
assert
5+
} = Ember;
6+
7+
/*
8+
* Utility method, returning a function that
9+
* throws an error unless defined. This is
10+
* like implementing abstract methods.
11+
*
12+
* @method requiredMethod
13+
*
14+
* @param {String} methodName
15+
* Name of the required method.
16+
*
17+
* @return {Function}
18+
* An 'abstract' method implementation.
19+
*/
20+
export default function requiredMethod(methodName) {
21+
assert('Method name is required for requiredMethod.', methodName);
22+
23+
return function() {
24+
throw new Ember.Error(`Definition of method ${methodName} is required.`);
25+
};
26+
}

0 commit comments

Comments
 (0)