├── .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 ![Download count all time](https://img.shields.io/npm/dt/ember-api-feature-flags.svg) [![Build Status](https://travis-ci.org/poteto/ember-api-feature-flags.svg?branch=master)](https://travis-ci.org/poteto/ember-api-feature-flags) [![npm version](https://badge.fury.io/js/ember-api-feature-flags.svg)](https://badge.fury.io/js/ember-api-feature-flags) [![Ember Observer Score](http://emberobserver.com/badges/ember-api-feature-flags.svg)](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} data 344 | * @param {String} [featureKey=get(this, 'featureKey')] 345 | * @returns {Object} 346 | */ 347 | _normalizeData(data, featureKey = get(this, 'featureKey')) { 348 | return data.reduce((acc, d) => { 349 | let normalizedKey = this.normalizeKey(d[featureKey]); 350 | acc[normalizedKey] = pick(d, [get(this, 'enabledKey')]); 351 | return acc; 352 | }, {}); 353 | } 354 | }); 355 | -------------------------------------------------------------------------------- /addon/utils/pick.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { get, isNone } = Ember; 4 | 5 | /** 6 | * Pick specific key/value pairs off an object. Returns new array. 7 | * 8 | * @export 9 | * @param {Object} obj 10 | * @param {Array} [attributes=[]] 11 | * @returns {Object} 12 | */ 13 | export default function pick(obj, attributes = []) { 14 | return attributes.reduce((acc, attr) => { 15 | let value = get(obj, attr); 16 | if (!isNone(value)) { 17 | acc[attr] = value; 18 | } 19 | return acc; 20 | }, {}); 21 | } 22 | -------------------------------------------------------------------------------- /addon/utils/pure-assign.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { merge } = Ember; 4 | const assign = Ember.assign || Object.assign || _assign; 5 | 6 | function _assign(origin, ...sources) { 7 | return sources.reduce((acc, source) => merge(acc, source), merge({}, origin)); 8 | } 9 | 10 | export default function pureAssign() { 11 | return assign({}, ...arguments); 12 | } 13 | -------------------------------------------------------------------------------- /app/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/app/.gitkeep -------------------------------------------------------------------------------- /app/services/feature-flags.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-api-feature-flags/services/feature-flags'; 2 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-api-feature-flags", 3 | "dependencies": {} 4 | } 5 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | scenarios: [ 4 | { 5 | name: 'ember-lts-2.4', 6 | bower: { 7 | dependencies: { 8 | 'ember': 'components/ember#lts-2-4' 9 | }, 10 | resolutions: { 11 | 'ember': 'lts-2-4' 12 | } 13 | }, 14 | npm: { 15 | devDependencies: { 16 | 'ember-source': null 17 | } 18 | } 19 | }, 20 | { 21 | name: 'ember-lts-2.8', 22 | bower: { 23 | dependencies: { 24 | 'ember': 'components/ember#lts-2-8' 25 | }, 26 | resolutions: { 27 | 'ember': 'lts-2-8' 28 | } 29 | }, 30 | npm: { 31 | devDependencies: { 32 | 'ember-source': null 33 | } 34 | } 35 | }, 36 | { 37 | name: 'ember-release', 38 | bower: { 39 | dependencies: { 40 | 'ember': 'components/ember#release' 41 | }, 42 | resolutions: { 43 | 'ember': 'release' 44 | } 45 | }, 46 | npm: { 47 | devDependencies: { 48 | 'ember-source': null 49 | } 50 | } 51 | }, 52 | { 53 | name: 'ember-beta', 54 | bower: { 55 | dependencies: { 56 | 'ember': 'components/ember#beta' 57 | }, 58 | resolutions: { 59 | 'ember': 'beta' 60 | } 61 | }, 62 | npm: { 63 | devDependencies: { 64 | 'ember-source': null 65 | } 66 | } 67 | }, 68 | { 69 | name: 'ember-canary', 70 | bower: { 71 | dependencies: { 72 | 'ember': 'components/ember#canary' 73 | }, 74 | resolutions: { 75 | 'ember': 'canary' 76 | } 77 | }, 78 | npm: { 79 | devDependencies: { 80 | 'ember-source': null 81 | } 82 | } 83 | }, 84 | { 85 | name: 'ember-default', 86 | npm: { 87 | devDependencies: {} 88 | } 89 | } 90 | ] 91 | }; 92 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = function(/* environment, appConfig */) { 5 | return { }; 6 | }; 7 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 3 | 4 | module.exports = function(defaults) { 5 | var app = new EmberAddon(defaults, { 6 | // Add options here 7 | }); 8 | 9 | /* 10 | This build file specifies the options for the dummy test app of this 11 | addon, located in `/tests/dummy` 12 | This build file does *not* influence how the addon or the app using it 13 | behave. You most likely want to be modifying `./index.js` or app's build file 14 | */ 15 | 16 | return app.toTree(); 17 | }; 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-api-feature-flags' 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-api-feature-flags", 3 | "version": "0.3.0", 4 | "description": "API based, read-only feature flags for Ember", 5 | "keywords": [ 6 | "ember-addon", 7 | "feature flags", 8 | "api" 9 | ], 10 | "license": "MIT", 11 | "author": "Lauren Tan ", 12 | "directories": { 13 | "doc": "doc", 14 | "test": "tests" 15 | }, 16 | "repository": "https://github.com/poteto/ember-api-feature-flags", 17 | "bugs": "https://github.com/poteto/ember-api-feature-flags/issues", 18 | "homepage": "https://github.com/poteto/ember-api-feature-flags", 19 | "scripts": { 20 | "build": "ember build", 21 | "start": "ember server", 22 | "test": "ember try:each" 23 | }, 24 | "dependencies": { 25 | "ember-cli-babel": "^5.1.7" 26 | }, 27 | "devDependencies": { 28 | "broccoli-asset-rev": "^2.4.5", 29 | "ember-ajax": "^2.4.1", 30 | "ember-cli": "2.12.0", 31 | "ember-cli-dependency-checker": "^1.3.0", 32 | "ember-cli-eslint": "^3.0.0", 33 | "ember-cli-htmlbars": "^1.1.1", 34 | "ember-cli-htmlbars-inline-precompile": "^0.3.6", 35 | "ember-cli-inject-live-reload": "^1.4.1", 36 | "ember-cli-pretender": "^1.0.1", 37 | "ember-cli-qunit": "^3.1.0", 38 | "ember-cli-release": "^1.0.0-beta.2", 39 | "ember-cli-shims": "^1.0.2", 40 | "ember-cli-sri": "^2.1.0", 41 | "ember-cli-uglify": "^1.2.0", 42 | "ember-disable-prototype-extensions": "^1.1.0", 43 | "ember-export-application-global": "^1.0.5", 44 | "ember-get-config": "^0.2.3", 45 | "ember-load-initializers": "^0.6.0", 46 | "ember-resolver": "^2.0.3", 47 | "ember-source": "~2.12.0", 48 | "loader.js": "^4.2.3" 49 | }, 50 | "engines": { 51 | "node": ">= 4" 52 | }, 53 | "ember-addon": { 54 | "configPath": "tests/dummy/config" 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | module.exports = { 3 | "test_page": "tests/index.html?hidepassed", 4 | "disable_watching": true, 5 | "launch_in_ci": [ 6 | "Chrome" 7 | ], 8 | "launch_in_dev": [ 9 | "Chrome" 10 | ] 11 | }; 12 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | embertest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/acceptance/main-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { test } from 'qunit'; 3 | import moduleForAcceptance from '../../tests/helpers/module-for-acceptance'; 4 | 5 | const { $ } = Ember; 6 | 7 | moduleForAcceptance('Acceptance | main'); 8 | 9 | test('it works', function(assert) { 10 | visit('/'); 11 | 12 | andThen(() => assert.equal(currentURL(), '/')); 13 | andThen(() => assert.ok($('#secret-message').length), 'feature flag is enabled'); 14 | andThen(() => assert.notOk($('#normal-message').length), 'feature flag is enabled'); 15 | }); 16 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | let App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | const { Controller, inject: { service } } = Ember; 4 | 5 | export default Controller.extend({ 6 | featureFlags: service() 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | const Router = Ember.Router.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() { 10 | }); 11 | 12 | export default Router; 13 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/dummy/app/routes/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

My App

2 | 3 | {{#if featureFlags.secretMessage.isEnabled}} 4 |

This is a secret message.

5 | {{else}} 6 |

This is a normal message.

7 | {{/if}} 8 | 9 | {{outlet}} 10 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | 25 | 'ember-api-feature-flags': { 26 | featureUrl: 'https://www.example.com/api/v1/features', 27 | featureKey: 'key', 28 | enabledKey: 'value', 29 | shouldMemoize: true, 30 | defaultValue: false 31 | } 32 | }; 33 | 34 | if (environment === 'development') { 35 | // ENV.APP.LOG_RESOLVER = true; 36 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 37 | // ENV.APP.LOG_TRANSITIONS = true; 38 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 39 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 40 | } 41 | 42 | if (environment === 'test') { 43 | // Testem prefers this... 44 | ENV.locationType = 'none'; 45 | 46 | // keep test console output quieter 47 | ENV.APP.LOG_ACTIVE_GENERATION = false; 48 | ENV.APP.LOG_VIEW_LOOKUPS = false; 49 | 50 | ENV.APP.rootElement = '#ember-testing'; 51 | } 52 | 53 | if (environment === 'production') { 54 | 55 | } 56 | 57 | return ENV; 58 | }; 59 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/destroy-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default function destroyApp(application) { 4 | application.pretenderInstance.shutdown(); 5 | Ember.run(application, 'destroy'); 6 | } 7 | -------------------------------------------------------------------------------- /tests/helpers/module-for-acceptance.js: -------------------------------------------------------------------------------- 1 | import { module } from 'qunit'; 2 | import Ember from 'ember'; 3 | import startApp from '../helpers/start-app'; 4 | import destroyApp from '../helpers/destroy-app'; 5 | 6 | const { RSVP: { Promise } } = Ember; 7 | 8 | export default function(name, options = {}) { 9 | module(name, { 10 | beforeEach() { 11 | this.application = startApp(); 12 | 13 | if (options.beforeEach) { 14 | return options.beforeEach.apply(this, arguments); 15 | } 16 | }, 17 | 18 | afterEach() { 19 | let afterEach = options.afterEach && options.afterEach.apply(this, arguments); 20 | return Promise.resolve(afterEach).then(() => destroyApp(this.application)); 21 | } 22 | }); 23 | } 24 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from '../../resolver'; 2 | import config from '../../config/environment'; 3 | 4 | const resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/send-response.js: -------------------------------------------------------------------------------- 1 | const { stringify } = JSON; 2 | 3 | export default function sendResponse(data, statusCode = 200, headers = { 'Content-Type': 'application/json' }) { 4 | return [statusCode, headers, stringify(data)]; 5 | } 6 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Pretender from 'pretender'; 3 | import Application from '../../app'; 4 | import config from '../../config/environment'; 5 | import sendResponse from './send-response'; 6 | 7 | const { 'ember-api-feature-flags': options } = config; 8 | const { featureUrl } = options; 9 | 10 | export default function startApp(attrs) { 11 | let attributes = Ember.merge({}, config.APP); 12 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 13 | 14 | return Ember.run(() => { 15 | let application = Application.create(attributes); 16 | application.pretenderInstance = new Pretender(function() { 17 | this.get(featureUrl, function() { 18 | let response = [ 19 | { 20 | "id": 1, 21 | "key": "secret_message", 22 | "value": "true", 23 | "created_at": "2017-03-22T03:30:10.270Z", 24 | "updated_at": "2017-03-22T03:30:10.270Z" 25 | } 26 | ]; 27 | return sendResponse(response); 28 | }) 29 | }); 30 | application.setupForTesting(); 31 | application.injectTestHelpers(); 32 | 33 | return application; 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/integration/.gitkeep -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/feature-flag/index-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import FeatureFlag from 'ember-api-feature-flags/feature-flag'; 3 | import { module, test } from 'qunit'; 4 | 5 | const { Evented, Object: EmberObject } = Ember; 6 | 7 | module('Unit | Utility | feature flag'); 8 | 9 | test('computed - #isEnabled - when no data', function(assert) { 10 | let featureFlag = FeatureFlag.create({ data: undefined }); 11 | assert.notOk(featureFlag.get('isEnabled'), 'should be disabled'); 12 | }); 13 | 14 | test('computed - #isEnabled - when true', function(assert) { 15 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: true } }); 16 | assert.ok(featureFlag.get('isEnabled'), 'should be enabled'); 17 | }); 18 | 19 | test('computed - #isEnabled - when true as string', function(assert) { 20 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: 'true' } }); 21 | assert.ok(featureFlag.get('isEnabled'), 'should be enabled'); 22 | }); 23 | 24 | test('computed - #isEnabled - when false', function(assert) { 25 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: false } }); 26 | assert.notOk(featureFlag.get('isEnabled'), 'should be enabled'); 27 | }); 28 | 29 | test('computed - #isEnabled - when false as string', function(assert) { 30 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: 'false' } }); 31 | assert.notOk(featureFlag.get('isEnabled'), 'should be enabled'); 32 | }); 33 | 34 | test('computed - #isDisabled - when no data', function(assert) { 35 | let featureFlag = FeatureFlag.create({ data: undefined }); 36 | assert.ok(featureFlag.get('isDisabled'), 'should be disabled'); 37 | }); 38 | 39 | test('computed - #isDisabled - when true', function(assert) { 40 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: true } }); 41 | assert.notOk(featureFlag.get('isDisabled'), 'should be disabled'); 42 | }); 43 | 44 | test('computed - #isDisabled - when true as string', function(assert) { 45 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: 'true' } }); 46 | assert.notOk(featureFlag.get('isDisabled'), 'should be disabled'); 47 | }); 48 | 49 | test('computed - #isDisabled - when false', function(assert) { 50 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: false } }); 51 | assert.ok(featureFlag.get('isDisabled'), 'should be disabled'); 52 | }); 53 | 54 | test('computed - #isDisabled - when false as string', function(assert) { 55 | let featureFlag = FeatureFlag.create({ data: { key: 'boolean', value: 'false' } }); 56 | assert.ok(featureFlag.get('isDisabled'), 'should be disabled'); 57 | }); 58 | 59 | test('when deferred - should listen for `didFetchData`', function(assert) { 60 | let DummyService = EmberObject.extend(Evented, { 61 | data: { foo: { key: 'boolean', value: 'true' } }, 62 | do(eventName) { this.trigger(eventName); } 63 | }); 64 | let service = DummyService.create(); 65 | let featureFlag = FeatureFlag.create({ 66 | isDeferred: true, 67 | __service__: service, 68 | __key__: 'foo', 69 | data: { key: 'boolean', value: 'false' } 70 | }); 71 | assert.ok(featureFlag.get('isDisabled'), 'precondition - should be disabled'); 72 | service.do('didFetchData'); 73 | assert.ok(featureFlag.get('isEnabled'), 'should be enabled'); 74 | }); 75 | -------------------------------------------------------------------------------- /tests/unit/services/feature-flags-test.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import { moduleFor, test } from 'ember-qunit'; 3 | 4 | const { typeOf } = Ember; 5 | const { keys } = Object; 6 | const defaultOptions = { 7 | featureUrl: 'http://www.example.com/features', 8 | featureKey: 'feature_key', 9 | enabledKey: 'value', 10 | shouldMemoize: true, 11 | defaultValue: false 12 | }; 13 | function isPromise(p) { 14 | return typeOf(p.then) === 'function' 15 | && typeOf(p.catch) === 'function'; 16 | } 17 | 18 | moduleFor('service:feature-flags', 'Unit | Service | feature flags', { 19 | }); 20 | 21 | test('#configure should set options on service', function(assert) { 22 | let service = this.subject(); 23 | let options = { 24 | featureUrl: 'http://www.example.com/features', 25 | featureKey: 'key', 26 | enabledKey: 'value', 27 | shouldMemoize: true, 28 | defaultValue: false 29 | }; 30 | service.configure(options); 31 | keys(options).forEach((k) => { 32 | assert.equal(service.get(k), options[k], 'should set option'); 33 | }); 34 | }); 35 | 36 | test('#fetchFeatures', function(assert) { 37 | let service = this.subject(); 38 | service 39 | .configure(defaultOptions) 40 | .setupForTesting(); 41 | assert.ok(isPromise(service.fetchFeatures()), 'should return promise'); 42 | }); 43 | 44 | test('#receiveData - when valid', function(assert) { 45 | let service = this.subject(); 46 | service 47 | .configure(defaultOptions) 48 | .receiveData([ 49 | { 'feature_key': 'foo_bar', key: 'boolean', value: true } 50 | ]); 51 | assert.ok(service.get('didFetchData')); 52 | assert.ok(service.get('_data'), 'should set `_data`'); 53 | }); 54 | 55 | test('#receiveData - when invalid', function(assert) { 56 | let service = this.subject(); 57 | service 58 | .configure(defaultOptions) 59 | .receiveData([]); 60 | assert.notOk(service.get('didFetchData')); 61 | assert.equal(service.get('error'), 'Empty data received'); 62 | }); 63 | 64 | test('#receiveError', function(assert) { 65 | let service = this.subject(); 66 | service 67 | .configure(defaultOptions) 68 | .receiveError('it failed'); 69 | assert.notOk(service.get('didFetchData'), 'should update `didFetchData`'); 70 | assert.equal(service.get('error'), 'it failed', 'should set `error`'); 71 | }); 72 | 73 | test('computed - #data', function(assert) { 74 | let service = this.subject(); 75 | service 76 | .configure(defaultOptions) 77 | .receiveData([ 78 | { 'feature_key': 'foo_bar', key: 'boolean', value: true } 79 | ]); 80 | assert.deepEqual(service.get('data'), { fooBar: { value: true } }, 'should normalize data'); 81 | }); 82 | 83 | test('#normalizeKey', function(assert) { 84 | let service = this.subject(); 85 | service.configure(defaultOptions); 86 | assert.equal(service.normalizeKey('foo_bar'), 'fooBar', 'should be camel case'); 87 | }); 88 | 89 | test('#unknownProperty - when `didFetchData` is true', function(assert) { 90 | let service = this.subject(); 91 | service 92 | .configure(defaultOptions) 93 | .receiveData([ 94 | { 'feature_key': 'foo_bar', key: 'boolean', value: true } 95 | ]); 96 | assert.ok(service.get('fooBar.isEnabled'), 'should proxy `get` correctly'); 97 | }); 98 | 99 | test('#unknownProperty - when `didFetchData` is false', function(assert) { 100 | let service = this.subject(); 101 | service.__testing__ = false; 102 | service 103 | .configure(defaultOptions) 104 | .receiveError('it failed'); 105 | assert.notOk(service.get('meow.isEnabled'), 'should proxy `get` correctly'); 106 | }); 107 | 108 | test('#unknownProperty - when `isTesting` is true', function(assert) { 109 | let service = this.subject(); 110 | service 111 | .configure(defaultOptions) 112 | .setupForTesting(); 113 | assert.ok(service.get('anything.isEnabled'), 'should proxy `get` correctly'); 114 | }); 115 | -------------------------------------------------------------------------------- /tests/unit/utils/pick-test.js: -------------------------------------------------------------------------------- 1 | import pick from 'ember-api-feature-flags/utils/pick'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Utility | pick'); 5 | 6 | test('it picks key/value pairs off an object', function(assert) { 7 | let obj = { foo: '123', bar: '456', baz: '789' }; 8 | assert.deepEqual(pick(obj, ['foo', 'baz']), { foo: '123', baz: '789' }); 9 | assert.deepEqual(pick(obj, ['bar', 'baz']), { bar: '456', baz: '789' }); 10 | assert.deepEqual(obj, { foo: '123', bar: '456', baz: '789' }, 'does not mutate object'); 11 | }); 12 | -------------------------------------------------------------------------------- /tests/unit/utils/pure-assign-test.js: -------------------------------------------------------------------------------- 1 | import pureAssign from 'ember-api-feature-flags/utils/pure-assign'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Utility | pure assign'); 5 | 6 | test(`it does not mutate destination or source objects`, function(assert) { 7 | let foo = { name: 'foo' }; 8 | let bar = { name: 'bar' }; 9 | let result = pureAssign(foo, bar, { test: 1 }); 10 | 11 | assert.deepEqual(result, { name: 'bar', test: 1 }, 'should assign object'); 12 | assert.deepEqual(foo, { name: 'foo' }, 'should not mutate destination'); 13 | assert.deepEqual(bar, { name: 'bar' }, 'should not mutate source'); 14 | }); 15 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/poteto/ember-api-feature-flags/113f519111611e5bf5aafeba93e17f28f182a0a1/vendor/.gitkeep --------------------------------------------------------------------------------