项目作者: cypress-io

项目描述 :
Functional feature toggles on top of any object
高级语言: JavaScript
项目地址: git://github.com/cypress-io/feature-maybe.git
创建时间: 2017-12-11T18:33:39Z
项目社区:https://github.com/cypress-io/feature-maybe

开源协议:

下载


feature-maybe Build Status

Functional feature toggles on top of any object

  1. npm i -D feature-maybe

To start, wrap any object using the exported function.

  1. const featureMaybe = require('feature-maybe')
  2. // actual feature flags and values
  3. const features = {
  4. wizard: true,
  5. mode: 'beast'
  6. }
  7. const feature = featureMaybe(features)
  8. // feature: string -> Maybe(value)
  9. feature('wizard') // Maybe {...}
  10. // returns Maybe
  11. // http://folktale.origamitower.com/api/v2.0.0/en/folktale.maybe.html

Use

Get the feature value

Common use 1: get the value of a feature, for example the value of the mode feature is “beast”. Typically we just get it from the features object

  1. console.log(features.mode) // "beast"

But what happens when the feature mode is disabled? All of the sudden we get

  1. const features = {
  2. wizard: true,
  3. mode: false
  4. }
  5. console.log(features.mode) // false

Ok, let us delete mode property completely from the object (if it is JSON) or comment it out (if it is JavaScript)

  1. const features = {
  2. wizard: true
  3. // mode: 'beast'
  4. }
  5. console.log(features.mode) // undefined

Hmm, we cannot just use a feature, because the features.mode might be invalid. This mens we always have to think about the default value whenever we use features.mode.

  1. console.log(features.mode || 'normal') // "normal"

Even this is tricky because of JavaScript castings.

  1. const features = {
  2. wizard: true,
  3. limit: 0 // zero is valid number!
  4. }
  5. console.log(features.mode || 'no limit') // "no limit"

How does Maybe help here? If we want to get the actual value we need to use method .getOrElse(<default>) which does not suffer from || type casting.

  1. const features = {
  2. wizard: true,
  3. mode: false,
  4. limit: 0 // zero is valid number!
  5. }
  6. feature = featureMaybe(features)
  7. console.log(feature('limit').getOrElse('no limit')) // 0
  8. console.log(feature('mode').getOrElse('normal')) // "normal"

Pass feature around

If we just pass object property, we must remember to always check the value the same way. Otherwise the outside code will do things differently.

  1. // returns "limit" feature value
  2. function init () {
  3. const features = {
  4. limit: 0
  5. }
  6. console.log(limit in features ? features.limit : 'no limit')
  7. return features.limit
  8. }
  9. const limit = init()
  10. console.log(limit || 'no limit here')
  11. // 0
  12. // "no limit here"

When we pass wrapped Maybe value around, the checking logic is already encapsulated inside, leading to consistency.

  1. // returns "limit" feature value
  2. function init () {
  3. const features = {
  4. limit: 0
  5. }
  6. const feature = featureMaybe(features)
  7. const limit = feature('limit')
  8. console.log(limit.getOrElse('no limit'))
  9. return limit
  10. }
  11. const limit = init()
  12. console.log(limit.getOrElse('no limit here'))
  13. // 0
  14. // 0

The behavior is consistent.

Conditional code

Often we are not just interested in printing the value of a feature, but in running some code depending on the feature value. Usually this is simple if statement.

Let’s say you want to do something if wizard is enabled

  1. if (features.wizard) {
  2. console.log('you are a wizard')
  3. }

Nice, except if (predicate) suffers from the same shortcuts as getting the value of a feature, while avoiding typecasting obstacles. Let us refactor the above code to be a little bit clearer. We are going to move “if” branch into its own function.

  1. const greetWizard = () =>
  2. console.log('you are a wizard')
  3. if (features.wizard) {
  4. greetWizard()
  5. }

If we use Maybe then we can call greetWizard - just pass whatever function you want to the .map method.

  1. const greetWizard = () =>
  2. console.log('you are a wizard')
  3. feature('wizard')
  4. .map(greetWizard)

If you want to do something if wizard is NOT enabled

  1. if (!features.wizard) {
  2. noWizard()
  3. }
  4. // equivalent
  5. feature('wizard')
  6. .orElse(noWizard)

We can even model if / else syntax by using both callbacks

  1. feature('wizard')
  2. .map(greetWizard)
  3. .orElse(noWizard)

There is one difference between if and .map code. Actual value of the feature stored inside the Maybe instance is passed into the call back

  1. const features = {
  2. wizard: true,
  3. mode: 'beast'
  4. }
  5. const feature = featureMaybe(features)
  6. const printMode = (mode) =>
  7. console.log('mode is:', mode)
  8. // with "if" we need to remember to pass mode
  9. if (features.mode) {
  10. printMode(features.mode)
  11. }
  12. // with Maybe it happens automatically
  13. feature('mode')
  14. .map(printMode)
  15. // "mode is: beast"

Refining feature value

Imagine our feature is a temperature limit. Can we treat positive temperature limit differently from negative temperature or zero? First, we might map degrees from F to C using .map

  1. const t = feature('temperature') // Maybe(number)
  2. console.log('Temp is', t.getOrElse('too cold'))
  3. if (t.map(FtoC).getOrElse(0) > 0) {
  4. console.log('warm!')
  5. }

But it is easy to get rid of conditional here. All we need is derive a new Maybe from our existing Maybe t. Just return new Maybe from the a callback named chain

  1. const t = feature('temperature')
  2. const warmTemp = t.map(FtoC)
  3. .chain(degrees =>
  4. degrees > 0 ? Maybe.Just(degrees) : Maybe.Nothing()
  5. )

Note that warmTemp is a Maybe itself - if the original temperature value inside t was positive, then the warmTemp will have that positive temperature (in Celsius).

Other notes

All callbacks are synchronous

Non-existing features

For example, if we ask for non-existent feature “foo”

  1. feature('foo')
  2. .map(doSomethingForFoo) // NOT called
  3. .map(doElseForFoo) // NOT called
  4. .orElse(nopeNoFoo) // called

Turned off features

  1. const features = {
  2. wizard: true,
  3. mode: 'beast',
  4. admin: false
  5. }
  6. const feature = featureMaybe(features)
  7. feature('admin')
  8. .map(...) // NOT called

For more info see spec.js

Small print

Author: Gleb Bahmutov gleb@cypress.io © Cypress.io 2017

License: MIT - do anything with the code, but don’t blame us if it does not work.

Support: if you find any problems with this module, email / tweet /
open issue on Github

MIT License

Copyright (c) 2017 Cypress.io gleb@cypress.io

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

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

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.