├── LICENSE.md
├── README.md
├── demo
├── Application.cfc
├── demo.css
└── index.cfm
├── jars
├── commons-pool2-2.0.jar
└── jedis-2.8.1.jar
├── lib
├── Rollout.cfc
└── storage
│ ├── InMemoryStorage.cfc
│ └── JedisStorage.cfc
└── tests
├── Application.cfc
├── LICENSE.md
├── README.md
├── index.cfm
├── specs
├── InMemoryStorageTest.cfc
├── JedisStorageTest.cfc
├── RolloutTest.cfc
└── TestCase.cfc
└── tinytest
├── Application.cfc
├── assets
├── app
│ ├── css
│ │ └── test-suite.css
│ ├── less
│ │ └── test-suite.less
│ └── main.js
└── jquery
│ └── jquery-1.9.1.min.js
├── design
├── design.png
└── design2.png
├── lib
├── Error.cfc
├── TestCase.cfc
├── TestResults.cfc
└── TestSuite.cfc
└── templates
└── test-suite.cfm
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | # The MIT License (MIT)
3 |
4 | Copyright (c) 2013 [Ben Nadel][1]
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy of
7 | this software and associated documentation files (the "Software"), to deal in
8 | the Software without restriction, including without limitation the rights to
9 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
10 | the Software, and to permit persons to whom the Software is furnished to do so,
11 | subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
18 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
19 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
20 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 |
23 | [1]: http://www.bennadel.com
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Rollout For ColdFusion
3 |
4 | by [Ben Nadel][bennadel] (on [Google+][googleplus])
5 |
6 | This is my ColdFusion "port" of the Ruby gem, [Rollout][rollout], developed by the team
7 | over at [FetLife][fetlife]. Rollout is a feature flag library that helps you gradually roll
8 | features out to your user base using percentages, groups, and explicit user identifiers.
9 | This is not an exact port of the code but, rather, a library that drew inspiration from
10 | the Rollout gem.
11 |
12 | Internally, my version of Rollout is optimized for bulk reads. All of the data is stored
13 | in a single key which contains serialized JSON (JavaScript Object Notation) data. I chose
14 | to use this internal architecture because I'd rather go over the wire fewer times and
15 | pull back more data each time. This also keeps the storage API extremely simple and easy
16 | to implement:
17 |
18 | ## Storage API
19 |
20 | Because the library stores all feature data a single JSON value, the storage mechanism
21 | doesn't have to deal with "keys" - it just deals with a single Struct value. The actual
22 | serialization of the data is deferred to the storage mechanism so that we can use storage
23 | that is not necessarily document oriented.
24 |
25 | * __delete__() - Deletes the persisted value.
26 | * __get__() - Returns the persisted value.
27 | * __set__( value ) - Persists the given value.
28 |
29 | If you are using the Jedis / Redis storage, the Redis key is defined as part of the storage
30 | instance, not the Rollout library.
31 |
32 | ## Instantiation
33 |
34 | To use Rollout, you have to instantiate it with a storage implementation. This library
35 | comes with an In-Memory implementation, which can be used for testing:
36 |
37 | ```cfc
38 | var storage = new lib.storage.InMemoryStorage();
39 |
40 | var rollout = new lib.Rollout( storage );
41 | ```
42 |
43 | But, you'll definitely want to use the Jedis / Redis implementation so your feature
44 | configurations actually persist across pages:
45 |
46 | ```cfc
47 | var jedisPoolConfig = createObject( "java", "redis.clients.jedis.JedisPoolConfig" ).init();
48 | var jedisPool = createObject( "java", "redis.clients.jedis.JedisPool" ).init( jedisPoolConfig, javaCast( "string", "localhost" ) );
49 | var storage = new lib.storage.JedisStorage( jedisPool, "rollout-features" );
50 |
51 | var rollout = new lib.Rollout( storage );
52 | ```
53 |
54 | ## Usage
55 |
56 | The primary gesture in a feature rollout is to check to see whether or not a given feature
57 | is activated. This can be done based on percentages, groups, and user identifiers. When
58 | checked on its own, a feature is only considered to be "active" if it is being rolled-out
59 | to 100% of the users. When checked in the context of a user (and optional groups), a feature
60 | can be partially active.
61 |
62 | The following list represents the Rollout API broken down by use-case.
63 |
64 | ### Activating Features
65 |
66 | When activating a feature, understand that percentage rollout acts _independently_ from the
67 | explicit user and group activation. Meaning, a feature can be rolled out to 0% of users but
68 | _still be active_ for explicit users and groups.
69 |
70 | * __activateFeature__( featureName )
71 | * __activateFeatureForGroup__( featureName, groupName )
72 | * __activateFeatureForPercentage__( featureName, percentage )
73 | * __activateFeatureForUser__( featureName, userIdentifier )
74 | * __activateFeatureForUsers__( featureName, userIdentifiers )
75 | * __ensureFeature__( featureName )
76 |
77 | ### Deactivating Features
78 |
79 | * __clearFeatures__()
80 | * __deactivateFeature__( featureName )
81 | * __deactivateFeatureForGroup__( featureName, groupName )
82 | * __deactivateFeatureForPercentage__( featureName )
83 | * __deactivateFeatureForUser__( featureName, userIdentifier )
84 | * __deleteFeature__( featureName )
85 |
86 | ### Getting Features And Feature States
87 |
88 | My implementation of Rollout is optimized for `getFeatureStatesForUser()`. This will pull
89 | back all of the feature configuration for the given user in a single internal request. When
90 | making this request, you have the _option_ to pass in a collection of groups. If the collection
91 | is an array, the values indicate the groups that the user is a members of:
92 |
93 | ```cfc
94 | var featureStates = rollout.getFeatureStatesForUser(
95 | user.id,
96 | [ "managers", "admins" ]
97 | );
98 | ```
99 |
100 | If the collection is a struct, the keys of the struct represent the group names and the
101 | struct values indicate whether or not the user is a member of that group. This approach allows
102 | the group membership to be calculated as part of the method invocation:
103 |
104 | ```cfc
105 | var featureStates = rollout.getFeatureStatesForUser(
106 | user.id,
107 | {
108 | managers: user.permissions.isManager,
109 | admins: user.permissions.isAdmin
110 | }
111 | );
112 | ```
113 |
114 | * __getFeature__( required string featureName )
115 | * __getFeatureNames__()
116 | * __getFeatureStates__()
117 | * __getFeatureStatesForGroup__( groupName )
118 | * __getFeatureStatesForUser__( userIdentifier [, groups ] )
119 | * __getFeatures__()
120 |
121 | ### Checking Single Feature State
122 |
123 | * __isFeatureActive__( featureName )
124 | * __isFeatureActiveForGroup__( featureName, groupName )
125 | * __isFeatureActiveForUser__( featureName, userIdentifier [, groups ] )
126 |
127 | ## Demo
128 |
129 | I have included a Jedis storage demo that helps to illustrates how several of the features
130 | in Rollout work. This demo assumes that Redis is running on `localhost`.
131 |
132 |
133 | [bennadel]: http://www.bennadel.com
134 | [googleplus]: https://plus.google.com/108976367067760160494?rel=author
135 | [rollout]: https://github.com/fetlife/rollout
136 | [fetlife]: https://github.com/fetlife
--------------------------------------------------------------------------------
/demo/Application.cfc:
--------------------------------------------------------------------------------
1 | component
2 | output = false
3 | hint = "I define the application settings and event handlers."
4 | {
5 |
6 | // Define the application settings.
7 | this.name = hash( getCurrentTemplatePath() );
8 | this.applicationTimeout = createTimeSpan( 0, 0, 10, 0 );
9 |
10 | // Calculate our directory paths.
11 | this.demoDirectory = getDirectoryFromPath( getCurrentTemplatePath() );
12 | this.projectDirectory = ( this.demoDirectory & "../" );
13 |
14 | // Setup our component mappings.
15 | this.mappings[ "/lib" ] = ( this.projectDirectory & "lib/" );
16 | this.mappings[ "/jars" ] = ( this.projectDirectory & "jars/" );
17 |
18 | // Load the Jedis JAR files so we can use the Jedis storage for the demo.
19 | this.javaSettings = {
20 | loadPaths: [
21 | ( this.mappings[ "/jars" ] & "commons-pool2-2.0.jar" ),
22 | ( this.mappings[ "/jars" ] & "jedis-2.8.1.jar" )
23 | ]
24 | };
25 |
26 |
27 | /**
28 | * I initialize the application.
29 | *
30 | * @output false
31 | */
32 | public boolean function onApplicationStart() {
33 |
34 | // For the demo, we're going to store the features in Redis so that we can see
35 | // the features persists across page refreshes.
36 | var jedisPoolConfig = createObject( "java", "redis.clients.jedis.JedisPoolConfig" ).init();
37 | var jedisPool = createObject( "java", "redis.clients.jedis.JedisPool" ).init( jedisPoolConfig, javaCast( "string", "localhost" ) );
38 | var storage = new lib.storage.JedisStorage( jedisPool, "demo:features" );
39 |
40 | // Setup our rollout service.
41 | application.rollout = new lib.Rollout( storage );
42 |
43 |
44 | // Setup our demo users. We're going to create a set that contains a set of
45 | // men and a set of women so that we can demonstrate targeting based on gender
46 | // groups - just makes the demo a little more feature-rich.
47 | application.users = [];
48 |
49 | var id = 0;
50 |
51 | for ( var gender in [ "M", "F" ] ) {
52 |
53 | for ( var i = 1 ; i <= 30 ; i++ ) {
54 |
55 | arrayAppend(
56 | application.users,
57 | {
58 | id: ++id,
59 | name: "User #gender#-#id#",
60 | gender: gender
61 | }
62 | );
63 |
64 | }
65 |
66 | }
67 |
68 | return( true );
69 |
70 | }
71 |
72 |
73 | /**
74 | * I initialize the request.
75 | *
76 | * @output false
77 | */
78 | public boolean function onRequestStart() {
79 |
80 | // If the INIT url variable is present, re-initialize the app.
81 | if ( structKeyExists( url, "init" ) ) {
82 |
83 | applicationStop();
84 | return( false );
85 |
86 | }
87 |
88 | return( true );
89 |
90 | }
91 |
92 | }
--------------------------------------------------------------------------------
/demo/demo.css:
--------------------------------------------------------------------------------
1 |
2 | body {
3 | font-family: "Open Sans", helvetica, arial, sans-serif ;
4 | font-weight: 400 ;
5 | }
6 |
7 | h1,
8 | h2,
9 | h3 {
10 | font-weight: 600 ;
11 | }
12 |
13 | a {
14 | color: #FF4D4D ;
15 | }
16 |
17 | strong {
18 | font-weight: 600 ;
19 | }
20 |
21 | form {
22 | border-bottom: 1px solid #CCCCCC ;
23 | margin-bottom: 30px ;
24 | padding-bottom: 20px ;
25 | }
26 |
27 | table a {
28 | color: #333333 ;
29 | }
30 |
31 | table td.active {
32 | background-color: #B5D954 ;
33 | color: #FFFFFF ;
34 | }
35 |
36 | table td.active a {
37 | color: #003300 ;
38 | }
--------------------------------------------------------------------------------
/demo/index.cfm:
--------------------------------------------------------------------------------
1 |
2 |
3 | rollout = application.rollout;
4 |
5 | // If there are no defined features, let's just set them up. This way, we don't
6 | // have to worry about "key existence" in the rest of the demo.
7 | if ( ! arrayLen( rollout.getFeatureNames() ) ) {
8 |
9 | rollout.ensureFeature( "featureA" );
10 | rollout.ensureFeature( "featureB" );
11 | rollout.ensureFeature( "featureC" );
12 |
13 | }
14 |
15 |
16 | // ------------------------------------------------------------------------------- //
17 | // ------------------------------------------------------------------------------- //
18 |
19 |
20 | // Param our action variables.
21 | param name="url.action" type="string" default="";
22 | param name="url.featureName" type="string" default="";
23 | param name="url.percentage" type="numeric" default="0";
24 | param name="url.groupName" type="string" default="";
25 | param name="url.userIdentifier" type="string" default="";
26 |
27 | if ( url.action == "activateFeatureForPercentage" ) {
28 |
29 | rollout.activateFeatureForPercentage( url.featureName, url.percentage );
30 |
31 | } else if ( url.action == "activateFeatureForGroup" ) {
32 |
33 | rollout.activateFeatureForGroup( url.featureName, url.groupName );
34 |
35 | } else if ( url.action == "deactivateFeatureForGroup" ) {
36 |
37 | rollout.deactivateFeatureForGroup( url.featureName, url.groupName );
38 |
39 | } else if ( url.action == "activateFeatureForUser" ) {
40 |
41 | rollout.activateFeatureForUser( url.featureName, url.userIdentifier );
42 |
43 | } else if ( url.action == "deactivateFeatureForUser" ) {
44 |
45 | rollout.deactivateFeatureForUser( url.featureName, url.userIdentifier );
46 |
47 | }
48 |
49 |
50 | // ------------------------------------------------------------------------------- //
51 | // ------------------------------------------------------------------------------- //
52 |
53 |
54 | // Get the collection of feature names for our various loops.
55 | featureNames = rollout.getFeatureNames();
56 |
57 | arraySort( featureNames, "text", "asc" );
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | ColdFusion Rollout
71 |
72 |
73 |
74 |
75 |
76 |
77 |
78 |