├── .bowerrc
├── .editorconfig
├── .ember-cli
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .travis.yml
├── .watchmanconfig
├── LICENSE.md
├── README.md
├── addon
├── .gitkeep
├── feature-flag
│ └── index.js
├── services
│ └── feature-flags.js
└── utils
│ ├── pick.js
│ └── pure-assign.js
├── app
├── .gitkeep
└── services
│ └── feature-flags.js
├── bower.json
├── config
├── ember-try.js
└── environment.js
├── ember-cli-build.js
├── index.js
├── package.json
├── testem.js
├── tests
├── .eslintrc.js
├── acceptance
│ └── main-test.js
├── dummy
│ ├── app
│ │ ├── app.js
│ │ ├── components
│ │ │ └── .gitkeep
│ │ ├── controllers
│ │ │ ├── .gitkeep
│ │ │ └── application.js
│ │ ├── helpers
│ │ │ └── .gitkeep
│ │ ├── index.html
│ │ ├── models
│ │ │ └── .gitkeep
│ │ ├── resolver.js
│ │ ├── router.js
│ │ ├── routes
│ │ │ └── .gitkeep
│ │ ├── styles
│ │ │ └── app.css
│ │ └── templates
│ │ │ ├── application.hbs
│ │ │ └── components
│ │ │ └── .gitkeep
│ ├── config
│ │ └── environment.js
│ └── public
│ │ ├── crossdomain.xml
│ │ └── robots.txt
├── helpers
│ ├── destroy-app.js
│ ├── module-for-acceptance.js
│ ├── resolver.js
│ ├── send-response.js
│ └── start-app.js
├── index.html
├── integration
│ └── .gitkeep
├── test-helper.js
└── unit
│ ├── .gitkeep
│ ├── feature-flag
│ └── index-test.js
│ ├── services
│ └── feature-flags-test.js
│ └── utils
│ ├── pick-test.js
│ └── pure-assign-test.js
├── vendor
└── .gitkeep
└── yarn.lock
/.bowerrc:
--------------------------------------------------------------------------------
1 | {
2 | "directory": "bower_components",
3 | "analytics": false
4 | }
5 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # EditorConfig helps developers define and maintain consistent
2 | # coding styles between different editors and IDEs
3 | # editorconfig.org
4 |
5 | root = true
6 |
7 |
8 | [*]
9 | end_of_line = lf
10 | charset = utf-8
11 | trim_trailing_whitespace = true
12 | insert_final_newline = true
13 | indent_style = space
14 | indent_size = 2
15 |
16 | [*.hbs]
17 | insert_final_newline = false
18 |
19 | [*.{diff,md}]
20 | trim_trailing_whitespace = false
21 |
--------------------------------------------------------------------------------
/.ember-cli:
--------------------------------------------------------------------------------
1 | {
2 | /**
3 | Ember CLI sends analytics information by default. The data is completely
4 | anonymous, but there are times when you might want to disable this behavior.
5 |
6 | Setting `disableAnalytics` to true will prevent any data from being sent.
7 | */
8 | "disableAnalytics": false
9 | }
10 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | ecmaVersion: 6,
5 | sourceType: 'module'
6 | },
7 | extends: 'eslint:recommended',
8 | env: {
9 | browser: true
10 | },
11 | rules: {
12 | }
13 | };
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # compiled output
4 | /dist
5 | /tmp
6 |
7 | # dependencies
8 | /node_modules
9 | /bower_components
10 |
11 | # misc
12 | /.sass-cache
13 | /connect.lock
14 | /coverage/*
15 | /libpeerconnection.log
16 | npm-debug.log*
17 | testem.log
18 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /bower_components
2 | /config/ember-try.js
3 | /dist
4 | /tests
5 | /tmp
6 | **/.gitkeep
7 | .bowerrc
8 | .editorconfig
9 | .ember-cli
10 | .gitignore
11 | .eslintrc.js
12 | .watchmanconfig
13 | .travis.yml
14 | bower.json
15 | ember-cli-build.js
16 | testem.js
17 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | ---
2 | language: node_js
3 | node_js:
4 | - "6"
5 |
6 | sudo: required
7 | dist: trusty
8 |
9 | cache:
10 | yarn: true
11 |
12 | addons:
13 | apt:
14 | sources:
15 | - google-chrome
16 | packages:
17 | - google-chrome-stable
18 |
19 | env:
20 | # we recommend testing LTS's and latest stable release (bonus points to beta/canary)
21 | - EMBER_TRY_SCENARIO=ember-lts-2.4
22 | - EMBER_TRY_SCENARIO=ember-lts-2.8
23 | - EMBER_TRY_SCENARIO=ember-release
24 | - EMBER_TRY_SCENARIO=ember-beta
25 | - EMBER_TRY_SCENARIO=ember-canary
26 | - EMBER_TRY_SCENARIO=ember-default
27 |
28 | matrix:
29 | fast_finish: true
30 | allow_failures:
31 | - env: EMBER_TRY_SCENARIO=ember-canary
32 |
33 | before_install:
34 | - export DISPLAY=:99.0
35 | - sh -e /etc/init.d/xvfb start
36 | - curl -o- -L https://yarnpkg.com/install.sh | bash
37 | - export PATH=$HOME/.yarn/bin:$PATH
38 |
39 | install:
40 | - yarn install --no-lockfile
41 |
42 | script:
43 | # Usually, it's ok to finish the test scenario without reverting
44 | # to the addon's original dependency state, skipping "cleanup".
45 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO test --skip-cleanup
46 |
--------------------------------------------------------------------------------
/.watchmanconfig:
--------------------------------------------------------------------------------
1 | {
2 | "ignore_dirs": ["tmp", "dist"]
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Lauren Elizabeth Tan
4 |
5 | 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:
6 |
7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8 |
9 | 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.
10 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ember-api-feature-flags  [](https://travis-ci.org/poteto/ember-api-feature-flags) [](https://badge.fury.io/js/ember-api-feature-flags) [](http://emberobserver.com/addons/ember-api-feature-flags)
2 |
3 | API based, read-only feature flags for Ember. To install:
4 |
5 | ```
6 | ember install ember-api-feature-flags
7 | ```
8 |
9 | ## How it works
10 |
11 | `ember-api-feature-flags` installs a service into your app. This service will let you [fetch your feature flag data](#fetching-feature-flags) from a specified URL.
12 |
13 | For example, call `fetchFeatures` in your application route:
14 |
15 | ```js
16 | // application/route.js
17 | import Ember from 'ember';
18 |
19 | const { inject: { Service }, Route } = Ember;
20 |
21 | export default Route.extend({
22 | featureFlags: service(),
23 |
24 | beforeModel() {
25 | this.get('featureFlags')
26 | .fetchFeatures()
27 | .then((data) => featureFlags.receiveData(data))
28 | .catch((reason) => featureFlags.receiveError(reason));
29 | }
30 | });
31 | ```
32 |
33 | Once fetched, you can then easily check if a given feature is enabled/disabled in both Handlebars:
34 |
35 | ```hbs
36 | {{#if featureFlags.myFeature.isEnabled}}
37 |
Do it
38 | {{/if}}
39 |
40 | {{#if featureFlags.anotherFeature.isDisabled}}
41 | Do something else
42 | {{/if}}
43 | ```
44 |
45 | ...and JavaScript:
46 |
47 | ```js
48 | import Ember from 'ember';
49 |
50 | const {
51 | Component,
52 | inject: { service },
53 | get
54 | } = Ember;
55 |
56 | export default Component.extend({
57 | featureFlags: service(),
58 |
59 | actions: {
60 | save() {
61 | let isDisabled = get(this, 'featureFlags.myFeature.isDisabled');
62 |
63 | if (isDisabled) {
64 | return;
65 | }
66 |
67 | // stuff
68 | }
69 | }
70 | });
71 | ```
72 |
73 | ## API fetch failure
74 |
75 | When the fetch fails, the service enters "error" mode. In this mode, feature flag lookups via HBS or JS will still function as normal. However, they will always return the default value set in the config (which defaults to `false`). You can set this to `true` if you want all features to be enabled in event of fetch failure.
76 |
77 | ## Configuration
78 |
79 | To configure, add to your `config/environment.js`:
80 |
81 | ```js
82 | /* eslint-env node */
83 | module.exports = function(environment) {
84 | var ENV = {
85 | 'ember-api-feature-flags': {
86 | featureUrl: 'https://www.example.com/api/v1/features',
87 | featureKey: 'feature_key',
88 | enabledKey: 'value',
89 | shouldMemoize: true,
90 | defaultValue: false
91 | }
92 | }
93 | return ENV;
94 | ```
95 |
96 | `featureUrl` **must** be defined, or `ember-api-feature-flags` will not be able to fetch feature flag data from your API.
97 |
98 | ## Fetching feature flags
99 |
100 | ### Unauthenticated
101 |
102 | For example, call `fetchFeatures` in your application route:
103 |
104 | ```js
105 | // application/route.js
106 | import Ember from 'ember';
107 |
108 | const { inject: { Service }, Route } = Ember;
109 |
110 | export default Route.extend({
111 | featureFlags: service(),
112 |
113 | beforeModel() {
114 | this.get('featureFlags')
115 | .fetchFeatures()
116 | .then((data) => featureFlags.receiveData(data))
117 | .catch((reason) => featureFlags.receiveError(reason));
118 | }
119 | });
120 | ```
121 |
122 | ### Authenticated
123 | In the following example, the application uses `ember-simple-auth`, and the `authenticated` data includes the user's `email` and `token`:
124 |
125 | ```js
126 | import Ember from 'ember';
127 | import Session from 'ember-simple-auth/services/session';
128 |
129 | const {
130 | inject: { service },
131 | get
132 | } = Ember;
133 |
134 | export default Session.extend({
135 | featureFlags: service(),
136 |
137 | // call this function after the session is authenticated
138 | fetchFeatureFlags() {
139 | let featureFlags = get(this, 'featureFlags');
140 | let { authenticated: { email, token } } = get(this, 'data');
141 | let headers = { Authorization: `Token token=${token}, email=${email}`};
142 | featureFlags
143 | .fetchFeatures({ headers })
144 | .then((data) => featureFlags.receiveData(data))
145 | .catch((reason) => featureFlags.receiveError(reason));
146 | }
147 | ```
148 |
149 | ### `featureUrl* {String}`
150 |
151 | Required. The URL where your API returns feature flag data. You can change this per environment in `config/environment.js`:
152 |
153 | ```js
154 | if (environment === 'canary') {
155 | ENV['ember-api-feature-flags'].featureUrl = 'https://www.example.com/api/v1/features';
156 | }
157 | ```
158 |
159 | ### `featureKey {String} = 'feature_key'`
160 |
161 | This key is the key on your feature flag data object yielding the feature's name. In other words, this key's value determines what you will use to access your feature flag (e.g. `this.get('featureFlags.newProfilePage.isEnabled')`):
162 |
163 | ```js
164 | // example feature flag data object
165 | {
166 | "id": 26,
167 | "feature_key": "new_profile_page", // <-
168 | "key": "boolean",
169 | "value": "true",
170 | "created_at": "2017-03-22T03:30:10.270Z",
171 | "updated_at": "2017-03-22T03:30:10.270Z"
172 | }
173 | ```
174 |
175 | The value on this key will be normalized by the [`normalizeKey`](#normalizeKey) method.
176 |
177 | ### `enabledKey {String} = 'value'`
178 |
179 | This determines which key to pick off of the feature flag data object. This value is then used by the `FeatureFlag` object (a wrapper around the single feature flag) when determining if a feature flag is enabled.
180 |
181 | ```js
182 | // example feature flag data object
183 | {
184 | "id": 26,
185 | "feature_key": "new_profile_page",
186 | "key": "boolean",
187 | "value": "true", // <-
188 | "created_at": "2017-03-22T03:30:10.270Z",
189 | "updated_at": "2017-03-22T03:30:10.270Z"
190 | }
191 | ```
192 |
193 | ### `shouldMemoize {Boolean} = true`
194 |
195 | By default, the service will instantiate and cache `FeatureFlag` objects. Set this to `false` to disable.
196 |
197 | ### `defaultValue {Boolean} = false`
198 |
199 | If the service is in error mode, all feature flag lookups will return this value as their `isEnabled` value.
200 |
201 | ## API
202 |
203 | * Properties
204 | + [`didFetchData`](#didfetchdata)
205 | + [`data`](#data)
206 | * Methods
207 | + [`configure`](#configure)
208 | + [`fetchFeatures`](#fetchfeatures)
209 | + [`receiveData`](#receivedata)
210 | + [`receiveError`](#receiveerror)
211 | + [`normalizeKey`](#normalizekey)
212 | + [`get`](#get)
213 | + [`setupForTesting`](#setupfortesting)
214 |
215 | #### `didFetchData`
216 |
217 | Returns a boolean value that represents the success state of fetching data from your API. If the GET request fails, this will be `false` and the service will be set to "error" mode. In error mode, all feature flags will return the default value as the value for `isEnabled`.
218 |
219 | **[⬆️ back to top](#api)**
220 |
221 | #### `data`
222 |
223 | A computed property that represents the normalized feature flag data.
224 |
225 | ```js
226 | let data = service.get('data');
227 |
228 | /**
229 | {
230 | "newProfilePage": { value: "true" },
231 | "newFriendList": { value: "true" }
232 | }
233 | **/
234 | ```
235 |
236 | **[⬆️ back to top](#api)**
237 |
238 | #### `configure {Object}`
239 |
240 | Configure the service. You can use this method to change service options at runtime. Acceptable options are the same as in the [configuration](#configuration) section.
241 |
242 | ```js
243 | service.configure({
244 | featureUrl: 'http://www.example.com/features',
245 | featureKey: 'feature_key',
246 | enabledKey: 'value',
247 | shouldMemoize: true,
248 | defaultValue: false
249 | });
250 | ```
251 |
252 | **[⬆️ back to top](#api)**
253 |
254 | #### `fetchFeatures {Object} = options`
255 |
256 | Performs the GET request to the specified URL, with optional headers to be passed to `ember-ajax`. Returns a Promise.
257 |
258 | ```js
259 | service.fetchFeatures().then((data) => doStuff(data));
260 | service.fetchFeatures({ headers: /* ... */}).then((data) => doStuff(data));
261 | ```
262 |
263 | **[⬆️ back to top](#api)**
264 |
265 | #### `receiveData {Object}`
266 |
267 | Receive data from API and set internal properties. If data is blank, we set the service in error mode.
268 |
269 | ```js
270 | service.receiveData([
271 | {
272 | "id": 26,
273 | "feature_key": "new_profile_page",
274 | "key": "boolean",
275 | "value": "true",
276 | "created_at": "2017-03-22T03:30:10.270Z",
277 | "updated_at": "2017-03-22T03:30:10.270Z"
278 | },
279 | {
280 | "id": 27,
281 | "feature_key": "new_friend_list",
282 | "key": "boolean",
283 | "value": "true",
284 | "created_at": "2017-03-22T03:30:10.287Z",
285 | "updated_at": "2017-03-22T03:30:10.287Z"
286 | }
287 | ]);
288 | service.get('data') // normalized data
289 | ```
290 |
291 | **[⬆️ back to top](#api)**
292 |
293 | #### `receiveError {Object}`
294 |
295 | Set service in errored state. Records failure reason as a side effect.
296 |
297 | ```js
298 | service.receiveError('Something went wrong');
299 | service.get('didFetchData', false);
300 | service.get('error', 'Something went wrong');
301 | ```
302 |
303 | **[⬆️ back to top](#api)**
304 |
305 | #### `normalizeKey {String}`
306 |
307 | Normalizes keys. Defaults to camelCase.
308 |
309 | ```js
310 | service.normalizeKey('new_profile_page'); // "newProfilePage"
311 | ```
312 |
313 | **[⬆️ back to top](#api)**
314 |
315 | #### `get {String}`
316 |
317 | Fetches the feature flag. Use in conjunction with `isEnabled` or `isDisabled` on the feature flag.
318 |
319 | ```js
320 | service.get('newProfilePage.isEnabled'); // true
321 | service.get('newFriendList.isEnabled'); // true
322 | service.get('oldProfilePage.isDisabled'); // true
323 | ```
324 |
325 | **[⬆️ back to top](#api)**
326 |
327 | #### `setupForTesting`
328 |
329 | Sets the service in testing mode. This is useful when writing acceptance/integration tests in your application as you don't need to intercept the request to your API. When the service is in testing mode, all features are enabled.
330 |
331 | ```js
332 | service.setupForTesting();
333 | service.get('newFriendList.isEnabled'); // true
334 | ```
335 |
336 | **[⬆️ back to top](#api)**
337 |
338 | ## Installation
339 |
340 | * `git clone ` this repository
341 | * `cd ember-api-feature-flags`
342 | * `npm install`
343 | * `bower install`
344 |
345 | ## Running
346 |
347 | * `ember serve`
348 | * Visit your app at [http://localhost:4200](http://localhost:4200).
349 |
350 | ## Running Tests
351 |
352 | * `npm test` (Runs `ember try:each` to test your addon against multiple Ember versions)
353 | * `ember test`
354 | * `ember test --server`
355 |
356 | ## Building
357 |
358 | * `ember build`
359 |
360 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/).
361 |
--------------------------------------------------------------------------------
/addon/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/addon/.gitkeep
--------------------------------------------------------------------------------
/addon/feature-flag/index.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 |
3 | const {
4 | Object: EmberObject,
5 | computed: { readOnly, not, bool },
6 | computed,
7 | get,
8 | setProperties,
9 | isPresent,
10 | typeOf
11 | } = Ember;
12 |
13 | /**
14 | * A small object that represents a feature flag and its `type` and `value`.
15 | *
16 | * @public
17 | * @export
18 | */
19 | export default EmberObject.extend({
20 | /**
21 | * A `FeatureFlag` that is a "relay" means that the object contains no data,
22 | * it is only a lightweight object that returns `isEnabled` with the default
23 | * value.
24 | *
25 | * @public
26 | * @property {Boolean}
27 | */
28 | isRelay: false,
29 |
30 | /**
31 | * Default value to return in `isEnabled` if the `FeatureFlag` is a relay.
32 | *
33 | * @public
34 | * @property {Any}
35 | */
36 | defaultValue: false,
37 |
38 | hasData: bool('data'),
39 | value: readOnly('data.value'),
40 |
41 | init() {
42 | this._super(...arguments);
43 | this.__listenForData__();
44 | },
45 |
46 | /**
47 | * When service is deferred, listen for the trigger so that we can get the
48 | * updated data from the service.
49 | *
50 | * @private
51 | * @returns {Void}
52 | */
53 | __listenForData__() {
54 | let service = get(this, '__service__');
55 | if (service === undefined) {
56 | return;
57 | }
58 | let featureFlag = this;
59 | service.on('didFetchData', function() {
60 | let service = this;
61 | let key = get(featureFlag, '__key__');
62 | let data = get(service, `data.${key}`);
63 | setProperties(featureFlag, {
64 | data,
65 | isRelay: false
66 | });
67 | });
68 | },
69 |
70 | /**
71 | * Is the `FeatureFlag` enabled?
72 | *
73 | * @public
74 | * @readonly
75 | * @returns {Boolean}
76 | */
77 | isEnabled: computed('isRelay', 'hasData', 'value', 'defaultValue', function() {
78 | let isRelay = get(this, 'isRelay');
79 | let hasData = get(this, 'hasData');
80 | let defaultValue = get(this, 'defaultValue');
81 | if (isRelay || !hasData) {
82 | return defaultValue;
83 | }
84 | let value = this._normalize(get(this, 'value'));
85 | return isPresent(value) ? value : defaultValue;
86 | }).readOnly(),
87 |
88 | /**
89 | * Is the `FeatureFlag` disabled?
90 | *
91 | * @public
92 | * @readonly
93 | * @returns {Boolean}
94 | */
95 | isDisabled: not('isEnabled').readOnly(),
96 |
97 | _normalize(value) {
98 | if (typeOf(value) === 'string') {
99 | return value === 'true';
100 | }
101 | return !!value;
102 | }
103 | });
104 |
--------------------------------------------------------------------------------
/addon/services/feature-flags.js:
--------------------------------------------------------------------------------
1 | import Ember from 'ember';
2 | import request from 'ember-ajax/request';
3 | import FeatureFlag from 'ember-api-feature-flags/feature-flag';
4 | import pick from 'ember-api-feature-flags/utils/pick';
5 | import pureAssign from 'ember-api-feature-flags/utils/pure-assign';
6 | import config from 'ember-get-config';
7 |
8 | const {
9 | String: { camelize },
10 | RSVP: { resolve },
11 | Evented,
12 | Service,
13 | computed,
14 | assert,
15 | get,
16 | set,
17 | isPresent,
18 | setProperties,
19 | typeOf
20 | } = Ember;
21 | const { 'ember-api-feature-flags': featureFlagsConfig, environment } = config;
22 | const SERVICE_OPTIONS = [
23 | 'featureUrl',
24 | 'featureKey',
25 | 'enabledKey',
26 | 'shouldMemoize',
27 | 'defaultValue'
28 | ];
29 | const FEATURE_FLAG_DEFAULTS = {
30 | /**
31 | * Feature API endpoint.
32 | *
33 | * @public
34 | * @property {String}
35 | */
36 | featureUrl: undefined,
37 | /**
38 | * Feature key name on the response object.
39 | *
40 | * @public
41 | * @property {String}
42 | */
43 | featureKey: 'feature_key',
44 |
45 | /**
46 | * Feature value key on the response object.
47 | *
48 | * @public
49 | * @property {String}
50 | */
51 | enabledKey: 'value',
52 |
53 | /**
54 | * If true, will cache FeatureFlag objects.
55 | *
56 | * @public
57 | * @property {Boolean}
58 | */
59 | shouldMemoize: true
60 | };
61 |
62 | export default Service.extend(Evented, {
63 | /**
64 | * Boolean status reflecting success or failure of fetching data.
65 | *
66 | * @public
67 | * @property {Boolean}
68 | */
69 | didFetchData: false,
70 |
71 | /**
72 | * Raw response from API.
73 | *
74 | * @private
75 | * @property {Object|Null}
76 | */
77 | _data: null,
78 |
79 | /**
80 | * Memoized cache of FeatureFlag objects.
81 | *
82 | * @private
83 | * @property {Object}
84 | */
85 | _cache: {},
86 |
87 | /**
88 | * Test mode status.
89 | *
90 | * @private
91 | * @property {Boolean}
92 | */
93 | __testing__: false,
94 |
95 | init() {
96 | this._super(...arguments);
97 | let options = pureAssign(FEATURE_FLAG_DEFAULTS, featureFlagsConfig);
98 | assert(`[ember-api-feature-flags] No feature URL found, please set one`, isPresent(options.featureUrl));
99 | this.configure(options);
100 | if (environment === 'test') {
101 | this.setupForTesting();
102 | }
103 | },
104 |
105 | /**
106 | * Set options on the FeatureFlags service.
107 | *
108 | * @public
109 | * @chainable
110 | * @param {Object} options
111 | * @returns {this}
112 | */
113 | configure(options) {
114 | assert(`[ember-api-feature-flags] Cannot configure FeatureFlags service without options`, isPresent(options));
115 | setProperties(this, pick(options, SERVICE_OPTIONS));
116 | return this;
117 | },
118 |
119 | /**
120 | * Normalized data from API.
121 | *
122 | * @public
123 | * @returns {Object|Boolean}
124 | */
125 | data: computed('didFetchData', '_data', function() {
126 | return get(this, 'didFetchData') && this._normalizeData(get(this, '_data'));
127 | }).readOnly(),
128 | /**
129 |
130 | /**
131 | * Fetch features from endpoint specified in config/environment.
132 | *
133 | * @public
134 | * @async
135 | * @param {Object} [options={}] Options to pass to ember-ajax
136 | * @returns {Promise}
137 | */
138 | fetchFeatures(options = {}) {
139 | let url = get(this, 'featureUrl');
140 | if (get(this, '__testing__')) {
141 | return resolve(true);
142 | }
143 | return request(url, options);
144 | },
145 |
146 | /**
147 | * Receive data from API and set internal properties. If data is blank, we
148 | * set the service in error mode.
149 | *
150 | * @public
151 | * @param {Any} data
152 | * @returns {Any}
153 | */
154 | receiveData(data) {
155 | let isValid = this._validateData(data);
156 | if (!isValid) {
157 | return this.receiveError('Empty data received');
158 | }
159 | let values = setProperties(this, { _data: data, didFetchData: true });
160 | this.trigger('didFetchData');
161 | return values;
162 | },
163 |
164 | /**
165 | * Set service in errored state. Records failure reason as a side effect.
166 | *
167 | * @public
168 | * @param {Any} reason
169 | * @returns {Boolean}
170 | */
171 | receiveError(reason) {
172 | set(this, 'error', reason);
173 | return set(this, 'didFetchData', false);
174 | },
175 |
176 | /**
177 | * Normalizes a key with a function. Defaults to camelCase.
178 | *
179 | * @public
180 | * @param {String} [key='']
181 | * @returns {String}
182 | */
183 | normalizeKey(key = '') {
184 | return camelize(key);
185 | },
186 |
187 | /**
188 | * Allows proxying `get` to FeatureFlag objects. For example:
189 | *
190 | * ```js
191 | * let service = get(this, 'featureFlags);
192 | * service.get('myFeatureName.isEnabled);
193 | * ```
194 | *
195 | * @public
196 | * @param {String} key
197 | * @returns {FeatureFlag}
198 | */
199 | unknownProperty(key) {
200 | if (SERVICE_OPTIONS.includes(key)) {
201 | return this[key];
202 | }
203 | let keyForFeature = this.normalizeKey(key);
204 | let didFetchData = get(this, 'didFetchData');
205 | let isTesting = get(this, '__testing__');
206 | if (isTesting) {
207 | return this._handleTest(keyForFeature);
208 | }
209 | if (didFetchData) {
210 | return this._handleSuccess(keyForFeature);
211 | }
212 | return this._handleFailed(keyForFeature);
213 | },
214 |
215 | /**
216 | * Stop holding references to cached FeatureFlags.
217 | *
218 | * @public
219 | * @returns {Void}
220 | */
221 | willDestroy() {
222 | this._super(...arguments);
223 | delete this._cache;
224 | },
225 |
226 | /**
227 | * Sets the service in testing mode.
228 | *
229 | * @public
230 | * @returns {Void}
231 | */
232 | setupForTesting() {
233 | setProperties(this, {
234 | __testing__: true,
235 | didFetchData: true,
236 | shouldMemoize: false
237 | });
238 | },
239 |
240 | /**
241 | * Validates data received from API.
242 | *
243 | * @private
244 | * @param {Array} data
245 | * @returns {Boolean}
246 | */
247 | _validateData(data) {
248 | return typeOf(data) === 'array' && get(data, 'length') > 0;
249 | },
250 |
251 | /**
252 | * When in testing mode, we set all features to be enabled by default.
253 | *
254 | * @public
255 | * @returns {FeatureFlag}
256 | */
257 | _handleTest(key) {
258 | return this._createFeatureFlag(key, {
259 | isRelay: true,
260 | defaultValue: true
261 | });
262 | },
263 |
264 | /**
265 | * When data is present, return a FeatureFlag object for `key`. Also memoizes
266 | * by default.
267 | *
268 | * @private
269 | * @param {Any} key
270 | * @param {Boolean} [shouldMemoize=true]
271 | * @returns {FeatureFlag}
272 | */
273 | _handleSuccess(key, shouldMemoize = get(this, 'shouldMemoize')) {
274 | let data = get(this, 'data');
275 | let defaultValue = get(this, 'defaultValue');
276 | let featureFlag = this._createFeatureFlag(key, {
277 | defaultValue,
278 | data: get(data, key)
279 | });
280 | return shouldMemoize ? this._memoize(key, featureFlag) : featureFlag;
281 | },
282 |
283 | /**
284 | * When no data is present, return a "relay" FeatureFlag object for `key`. A
285 | * relay is simply a proxy FeatureFlag object that holds no data.
286 | *
287 | * @private
288 | * @param {Any} key
289 | * @param {Boolean} [shouldMemoize=get(this, 'shouldMemoize')]
290 | * @returns {FeatureFlag}
291 | */
292 | _handleFailed(key, shouldMemoize = get(this, 'shouldMemoize')) {
293 | let defaultValue = get(this, 'defaultValue');
294 | let featureFlag = this._createFeatureFlag(key, {
295 | defaultValue,
296 | isRelay: true
297 | });
298 | return shouldMemoize ? this._memoize(key, featureFlag) : featureFlag;
299 | },
300 |
301 | /**
302 | * Creates a new FeatureFlag instance with default options.
303 | *
304 | * @param {String} key
305 | * @param {Object} [opts={}]
306 | * @returns
307 | */
308 | _createFeatureFlag(key, opts = {}) {
309 | let defaultOpts = {
310 | __key__: key,
311 | __service__: this
312 | };
313 | return FeatureFlag.create(pureAssign(defaultOpts, opts));
314 | },
315 |
316 | /**
317 | * Memoizes a feature flag lookup into the service's internal cache.
318 | *
319 | * @private
320 | * @param {String} key
321 | * @param {FeatureFlag} featureFlag
322 | * @param {Boolean} [shouldInvalidate=false]
323 | * @returns {FeatureFlag}
324 | */
325 | _memoize(key, featureFlag, shouldInvalidate = false) {
326 | let cache = get(this, '_cache');
327 | if (shouldInvalidate) {
328 | delete cache[key];
329 | }
330 | let found = cache[key];
331 | if (isPresent(found)) {
332 | return found;
333 | }
334 | cache[key] = featureFlag;
335 | return featureFlag;
336 | },
337 |
338 | /**
339 | * For a given data array, returns an object where the keys are the `featureKey`
340 | * values.
341 | *
342 | * @private
343 | * @param {Array