flipper-cassanity 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - ree
5
+ - 1.9.3
6
+ notifications:
7
+ email: false
8
+ bundler_args: --without guard
9
+ before_script: sudo service cassandra status
10
+ services:
11
+ - cassandra
data/Gemfile ADDED
@@ -0,0 +1,13 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
3
+
4
+ gem 'rake'
5
+ gem 'rspec'
6
+ gem 'activesupport', :require => false
7
+
8
+ group(:guard) do
9
+ gem 'guard'
10
+ gem 'guard-rspec'
11
+ gem 'guard-bundler'
12
+ gem 'rb-fsevent'
13
+ end
data/Guardfile ADDED
@@ -0,0 +1,20 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'bundler' do
5
+ watch('Gemfile')
6
+ watch(/^.+\.gemspec/)
7
+ end
8
+
9
+ rspec_options = {
10
+ :all_after_pass => false,
11
+ :all_on_start => false,
12
+ :keep_failed => false,
13
+ }
14
+
15
+ guard 'rspec', rspec_options do
16
+ watch(%r{^spec/.+_spec\.rb$}) { "spec" }
17
+ watch(%r{^lib/(.+)\.rb$}) { "spec" }
18
+ watch(%r{shared_adapter_specs\.rb$}) { "spec" }
19
+ watch('spec/helper.rb') { "spec" }
20
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 John Nunemaker
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Flipper::Cassanity
2
+
3
+ A [Cassanity](https://github.com/jnunemaker/cassanity) adapter for [Flipper](https://github.com/jnunemaker/flipper).
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'flipper-cassanity'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install flipper-cassanity
18
+
19
+ ## Usage
20
+
21
+ ```ruby
22
+ # Assumes keyspace created and column family exists with this schema:
23
+ # {
24
+ # primary_key: [:key, :field],
25
+ # columns: {
26
+ # key: :text,
27
+ # field: :text,
28
+ # value: :text,
29
+ # },
30
+ # }
31
+
32
+ require 'flipper/adapters/cassanity'
33
+ column_family = Cassanity::Client.new[:cassanity][:flipper]
34
+ adapter = Flipper::Adapters::Cassanity.new(column_family)
35
+ flipper = Flipper.new(adapter)
36
+ # profit...
37
+ ```
38
+
39
+ ## Internals
40
+
41
+ Each feature is stored in a single row, which means getting a feature is single query.
42
+
43
+ ```ruby
44
+ require 'flipper/adapters/cassanity'
45
+ column_family = Cassanity::Client.new[:cassanity][:flipper]
46
+ adapter = Flipper::Adapters::Cassanity.new(column_family)
47
+ flipper = Flipper.new(adapter)
48
+
49
+ # Register a few groups.
50
+ Flipper.register(:admins) { |thing| thing.admin? }
51
+ Flipper.register(:early_access) { |thing| thing.early_access? }
52
+
53
+ # Create a user class that has flipper_id instance method.
54
+ User = Struct.new(:flipper_id)
55
+
56
+ flipper[:stats].enable
57
+ flipper[:stats].enable flipper.group(:admins)
58
+ flipper[:stats].enable flipper.group(:early_access)
59
+ flipper[:stats].enable User.new('25')
60
+ flipper[:stats].enable User.new('90')
61
+ flipper[:stats].enable User.new('180')
62
+ flipper[:stats].enable flipper.random(15)
63
+ flipper[:stats].enable flipper.actors(45)
64
+
65
+ flipper[:search].enable
66
+
67
+ puts 'all docs in collection'
68
+ pp column_family.select
69
+ # all docs in collection
70
+ # [{"_id"=>"stats",
71
+ # "actors"=>["25", "90", "180"],
72
+ # "boolean"=>"true",
73
+ # "groups"=>["admins", "early_access"],
74
+ # "percentage_of_actors"=>"45",
75
+ # "percentage_of_random"=>"15"},
76
+ # {"_id"=>"flipper_features", "features"=>["stats", "search"]},
77
+ # {"_id"=>"search", "boolean"=>"true"}]
78
+ puts
79
+
80
+ puts 'flipper get of feature'
81
+ pp adapter.get(flipper[:stats])
82
+ # flipper get of feature
83
+ # {:boolean=>"true",
84
+ # :groups=>#<Set: {"admins", "early_access"}>,
85
+ # :actors=>#<Set: {"25", "90", "180"}>,
86
+ # :percentage_of_actors=>"45",
87
+ # :percentage_of_random=>"15"}
88
+ ```
89
+
90
+ ## Contributing
91
+
92
+ 1. Fork it
93
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
94
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
95
+ 4. Push to the branch (`git push origin my-new-feature`)
96
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+
4
+ require 'rspec/core/rake_task'
5
+ RSpec::Core::RakeTask.new
6
+
7
+ task :default => :spec
data/examples/basic.rb ADDED
@@ -0,0 +1,51 @@
1
+ # Nothing to see here... move along.
2
+ # Sets up load path for examples and requires some stuff
3
+ require 'pp'
4
+ require 'pathname'
5
+ require 'logger'
6
+
7
+ root_path = Pathname(__FILE__).dirname.join('..').expand_path
8
+ lib_path = root_path.join('lib')
9
+ $:.unshift(lib_path)
10
+
11
+ require 'flipper/adapters/cassanity'
12
+ require 'cassanity/instrumentation/log_subscriber'
13
+ Cassanity::Instrumentation::LogSubscriber.logger = Logger.new(STDOUT, Logger::DEBUG)
14
+
15
+ client = Cassanity::Client.new('127.0.0.1:9160', {
16
+ instrumenter: ActiveSupport::Notifications,
17
+ })
18
+ keyspace = client.keyspace(:cassanity)
19
+ column_family = keyspace.column_family({
20
+ name: :flipper,
21
+ schema: {
22
+ primary_key: [:key, :field],
23
+ columns: {
24
+ key: :text,
25
+ field: :text,
26
+ value: :text,
27
+ },
28
+ },
29
+ })
30
+
31
+ keyspace.recreate
32
+ column_family.create
33
+
34
+ adapter = Flipper::Adapters::Cassanity.new(column_family)
35
+ flipper = Flipper.new(adapter)
36
+
37
+ flipper[:stats].enable
38
+
39
+ if flipper[:stats].enabled?
40
+ puts "\n\nEnabled!\n\n\n"
41
+ else
42
+ puts "\n\nDisabled!\n\n\n"
43
+ end
44
+
45
+ flipper[:stats].disable
46
+
47
+ if flipper[:stats].enabled?
48
+ puts "\n\nEnabled!\n\n\n"
49
+ else
50
+ puts "\n\nDisabled!\n\n\n"
51
+ end
@@ -0,0 +1,75 @@
1
+ # Nothing to see here... move along.
2
+ # Sets up load path for examples and requires some stuff
3
+ require 'pp'
4
+ require 'pathname'
5
+ require 'logger'
6
+
7
+ root_path = Pathname(__FILE__).dirname.join('..').expand_path
8
+ lib_path = root_path.join('lib')
9
+ $:.unshift(lib_path)
10
+
11
+ require 'flipper/adapters/cassanity'
12
+ require 'cassanity/instrumentation/log_subscriber'
13
+ Cassanity::Instrumentation::LogSubscriber.logger = Logger.new(STDOUT, Logger::DEBUG)
14
+
15
+ client = Cassanity::Client.new('127.0.0.1:9160', {
16
+ instrumenter: ActiveSupport::Notifications,
17
+ })
18
+ keyspace = client.keyspace(:cassanity)
19
+ column_family = keyspace.column_family({
20
+ name: :flipper,
21
+ schema: {
22
+ primary_key: [:key, :field],
23
+ columns: {
24
+ key: :text,
25
+ field: :text,
26
+ value: :text,
27
+ },
28
+ },
29
+ })
30
+
31
+ keyspace.recreate
32
+ column_family.create
33
+
34
+ adapter = Flipper::Adapters::Cassanity.new(column_family)
35
+ flipper = Flipper.new(adapter)
36
+
37
+ # Register a few groups.
38
+ Flipper.register(:admins) { |thing| thing.admin? }
39
+ Flipper.register(:early_access) { |thing| thing.early_access? }
40
+
41
+ # Create a user class that has flipper_id instance method.
42
+ User = Struct.new(:flipper_id)
43
+
44
+ flipper[:stats].enable
45
+ flipper[:stats].enable flipper.group(:admins)
46
+ flipper[:stats].enable flipper.group(:early_access)
47
+ flipper[:stats].enable User.new('25')
48
+ flipper[:stats].enable User.new('90')
49
+ flipper[:stats].enable User.new('180')
50
+ flipper[:stats].enable flipper.random(15)
51
+ flipper[:stats].enable flipper.actors(45)
52
+
53
+ flipper[:search].enable
54
+
55
+ puts 'all docs in collection'
56
+ pp column_family.select
57
+ # all docs in collection
58
+ # [{"_id"=>"stats",
59
+ # "actors"=>["25", "90", "180"],
60
+ # "boolean"=>"true",
61
+ # "groups"=>["admins", "early_access"],
62
+ # "percentage_of_actors"=>"45",
63
+ # "percentage_of_random"=>"15"},
64
+ # {"_id"=>"flipper_features", "features"=>["stats", "search"]},
65
+ # {"_id"=>"search", "boolean"=>"true"}]
66
+ puts
67
+
68
+ puts 'flipper get of feature'
69
+ pp adapter.get(flipper[:stats])
70
+ # flipper get of feature
71
+ # {:boolean=>"true",
72
+ # :groups=>#<Set: {"admins", "early_access"}>,
73
+ # :actors=>#<Set: {"25", "90", "180"}>,
74
+ # :percentage_of_actors=>"45",
75
+ # :percentage_of_random=>"15"}
@@ -0,0 +1,20 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/flipper/adapters/cassanity/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "flipper-cassanity"
6
+ gem.version = Flipper::Adapters::Cassanity::VERSION
7
+ gem.authors = ["John Nunemaker"]
8
+ gem.email = ["nunemaker@gmail.com"]
9
+ gem.description = %q{Cassanity adapter for Flipper.}
10
+ gem.summary = %q{Cassanity adapter for Flipper.}
11
+ gem.homepage = "http://jnunemaker.github.com/flipper-cassanity"
12
+ gem.require_paths = ["lib"]
13
+
14
+ gem.files = `git ls-files`.split($/)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+
18
+ gem.add_dependency 'flipper', '~> 0.5.0'
19
+ gem.add_dependency 'cassanity', '~> 0.4.0'
20
+ end
@@ -0,0 +1,7 @@
1
+ module Flipper
2
+ module Adapters
3
+ class Cassanity
4
+ VERSION = "0.5.0"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,172 @@
1
+ require 'set'
2
+ require 'flipper'
3
+ require 'cassanity'
4
+
5
+ module Flipper
6
+ module Adapters
7
+ class Cassanity
8
+ include Flipper::Adapter
9
+
10
+ # Private: The key that stores the set of known features.
11
+ FeaturesKey = :flipper_features
12
+
13
+ # Public: The name of the adapter.
14
+ attr_reader :name
15
+
16
+ # Private: The column family where the data is stored.
17
+ attr_reader :column_family
18
+
19
+ # Public: Initializes a Cassanity flipper adapter.
20
+ #
21
+ # column_family - The Cassanity::ColumnFamily that should store the info.
22
+ def initialize(column_family)
23
+ @column_family = column_family
24
+ @name = :cassanity
25
+ end
26
+
27
+ # Public: Gets the values for all gates for a given feature.
28
+ #
29
+ # Returns a Hash of Flipper::Gate#key => value.
30
+ def get(feature)
31
+ result = {}
32
+ doc = doc_for(feature)
33
+ fields = doc.keys
34
+
35
+ feature.gates.each do |gate|
36
+ result[gate.key] = case gate.data_type
37
+ when :boolean, :integer
38
+ doc[gate.key.to_s]
39
+ when :set
40
+ fields_to_gate_value fields, gate
41
+ else
42
+ unsupported_data_type gate.data_type
43
+ end
44
+ end
45
+
46
+ result
47
+ end
48
+
49
+ # Public: Enables a gate for a given thing.
50
+ #
51
+ # feature - The Flipper::Feature for the gate.
52
+ # gate - The Flipper::Gate to disable.
53
+ # thing - The Flipper::Type being disabled for the gate.
54
+ #
55
+ # Returns true.
56
+ def enable(feature, gate, thing)
57
+ case gate.data_type
58
+ when :boolean, :integer
59
+ update feature.key, gate.key, thing.value.to_s
60
+ when :set
61
+ update feature.key, to_field(gate, thing), thing.value.to_s
62
+ else
63
+ unsupported_data_type gate.data_type
64
+ end
65
+
66
+ true
67
+ end
68
+
69
+ # Public: Disables a gate for a given thing.
70
+ #
71
+ # feature - The Flipper::Feature for the gate.
72
+ # gate - The Flipper::Gate to disable.
73
+ # thing - The Flipper::Type being disabled for the gate.
74
+ #
75
+ # Returns true.
76
+ def disable(feature, gate, thing)
77
+ case gate.data_type
78
+ when :boolean
79
+ delete feature.key
80
+ when :integer
81
+ update feature.key, gate.key, thing.value.to_s
82
+ when :set
83
+ delete feature.key, to_field(gate, thing)
84
+ else
85
+ unsupported_data_type gate.data_type
86
+ end
87
+
88
+ true
89
+ end
90
+
91
+ # Public: Adds a feature to the set of known features.
92
+ def add(feature)
93
+ update FeaturesKey, feature.name, 1
94
+ true
95
+ end
96
+
97
+ # Public: The set of known features.
98
+ def features
99
+ rows = select(FeaturesKey)
100
+ rows.map { |row| row['field'] }.to_set
101
+ end
102
+
103
+ # Private: Converts gate and thing to hash key.
104
+ def to_field(gate, thing)
105
+ "#{gate.key}/#{thing.value}"
106
+ end
107
+
108
+ # Private: Select rows matching key and optionally field.
109
+ #
110
+ # Returns an Array of Hashes.
111
+ def select(key, field = :skip)
112
+ @column_family.select({
113
+ select: [:field, :value],
114
+ where: where(key, field),
115
+ })
116
+ end
117
+
118
+ # Private: Update key/field combo to value.
119
+ #
120
+ # Returns nothing.
121
+ def update(key, field, value)
122
+ @column_family.update({
123
+ set: {value: value},
124
+ where: {key: key, field: field},
125
+ })
126
+ end
127
+
128
+ # Private: Delete rows matching key and optionally field as well.
129
+ #
130
+ # Returns nothing.
131
+ def delete(key, field = :skip)
132
+ @column_family.delete({
133
+ where: where(key, field),
134
+ })
135
+ end
136
+
137
+ # Private: Given a key and field it returns appropriate where hash for
138
+ # querying the column family.
139
+ #
140
+ # Returns a Hash to be used as criteria for a query.
141
+ def where(key, field)
142
+ where = {key: key}
143
+ where[:field] = field unless field == :skip
144
+ where
145
+ end
146
+
147
+ # Private: Gets a hash of fields => values for the given feature.
148
+ #
149
+ # Returns a Hash of fields => values.
150
+ def doc_for(feature)
151
+ field_value_pairs = select(feature.key).map { |row| row.values }
152
+ Hash[*field_value_pairs.flatten]
153
+ end
154
+
155
+ # Private: Returns a set of values given an array of fields and a gate.
156
+ #
157
+ # Returns a Set of the values enabled for the gate.
158
+ def fields_to_gate_value(fields, gate)
159
+ regex = /^#{Regexp.escape(gate.key)}\//
160
+ keys = fields.grep(regex)
161
+ values = keys.map { |key| key.split('/', 2).last }
162
+ values.to_set
163
+ end
164
+
165
+ # Private: Raises error letting user know that data type is not
166
+ # supported by this adapter.
167
+ def unsupported_data_type(data_type)
168
+ raise "#{data_type} is not supported by this adapter"
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1 @@
1
+ require 'flipper/adapters/cassanity'
@@ -0,0 +1,30 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/cassanity'
3
+ require 'flipper/spec/shared_adapter_specs'
4
+
5
+ describe Flipper::Adapters::Cassanity do
6
+ let(:client) { Cassanity::Client.new }
7
+ let(:keyspace) { client.keyspace(:cassanity) }
8
+ let(:column_family) {
9
+ keyspace.column_family({
10
+ name: :flipper,
11
+ schema: {
12
+ primary_key: [:key, :field],
13
+ columns: {
14
+ key: :text,
15
+ field: :text,
16
+ value: :text,
17
+ },
18
+ },
19
+ })
20
+ }
21
+
22
+ subject { described_class.new(column_family) }
23
+
24
+ before do
25
+ keyspace.recreate
26
+ column_family.create
27
+ end
28
+
29
+ it_should_behave_like 'a flipper adapter'
30
+ end
data/spec/helper.rb ADDED
@@ -0,0 +1,18 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+
3
+ require 'bundler'
4
+ Bundler.setup :default
5
+
6
+ require 'flipper-cassanity'
7
+
8
+ RSpec.configure do |config|
9
+ config.filter_run :focused => true
10
+ config.alias_example_to :fit, :focused => true
11
+ config.alias_example_to :xit, :pending => true
12
+ config.run_all_when_everything_filtered = true
13
+ config.fail_fast = true
14
+
15
+ config.backtrace_clean_patterns = [
16
+ /rspec-(core|expectations)/,
17
+ ]
18
+ end
metadata ADDED
@@ -0,0 +1,101 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flipper-cassanity
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - John Nunemaker
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-02-16 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: flipper
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.5.0
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 0.5.0
30
+ - !ruby/object:Gem::Dependency
31
+ name: cassanity
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ~>
36
+ - !ruby/object:Gem::Version
37
+ version: 0.4.0
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ version: 0.4.0
46
+ description: Cassanity adapter for Flipper.
47
+ email:
48
+ - nunemaker@gmail.com
49
+ executables: []
50
+ extensions: []
51
+ extra_rdoc_files: []
52
+ files:
53
+ - .gitignore
54
+ - .rspec
55
+ - .travis.yml
56
+ - Gemfile
57
+ - Guardfile
58
+ - LICENSE
59
+ - README.md
60
+ - Rakefile
61
+ - examples/basic.rb
62
+ - examples/internals.rb
63
+ - flipper-cassanity.gemspec
64
+ - lib/flipper-cassanity.rb
65
+ - lib/flipper/adapters/cassanity.rb
66
+ - lib/flipper/adapters/cassanity/version.rb
67
+ - spec/cassanity_spec.rb
68
+ - spec/helper.rb
69
+ homepage: http://jnunemaker.github.com/flipper-cassanity
70
+ licenses: []
71
+ post_install_message:
72
+ rdoc_options: []
73
+ require_paths:
74
+ - lib
75
+ required_ruby_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ segments:
82
+ - 0
83
+ hash: 216004828098320081
84
+ required_rubygems_version: !ruby/object:Gem::Requirement
85
+ none: false
86
+ requirements:
87
+ - - ! '>='
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ segments:
91
+ - 0
92
+ hash: 216004828098320081
93
+ requirements: []
94
+ rubyforge_project:
95
+ rubygems_version: 1.8.23
96
+ signing_key:
97
+ specification_version: 3
98
+ summary: Cassanity adapter for Flipper.
99
+ test_files:
100
+ - spec/cassanity_spec.rb
101
+ - spec/helper.rb