flipper-mongo 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,5 +14,5 @@ Gem::Specification.new do |gem|
14
14
  gem.name = "flipper-mongo"
15
15
  gem.require_paths = ["lib"]
16
16
  gem.version = Flipper::Adapters::Mongo::VERSION
17
- gem.add_dependency 'flipper', '~> 0.1.1'
17
+ gem.add_dependency 'flipper', '~> 0.2'
18
18
  end
@@ -1,64 +1,55 @@
1
1
  require 'set'
2
+ require 'forwardable'
2
3
  require 'mongo'
3
4
 
4
5
  module Flipper
5
6
  module Adapters
6
7
  class Mongo
7
- DefaultId = 'flipper'
8
+ extend Forwardable
8
9
 
9
- def initialize(collection, options = {})
10
+ def initialize(collection)
10
11
  @collection = collection
11
- @options = options
12
- @mongo_criteria = {:_id => id}
13
- @mongo_options = {:upsert => true, :safe => true}
12
+ @update_options = {:safe => true, :upsert => true}
14
13
  end
15
14
 
16
15
  def read(key)
17
- read_key(key)
16
+ find_one key
18
17
  end
19
18
 
20
19
  def write(key, value)
21
- update '$set' => {key => value}
20
+ update key, {'$set' => {'v' => value}}
22
21
  end
23
22
 
24
23
  def delete(key)
25
- update '$unset' => {key => 1}
24
+ @collection.remove criteria(key)
26
25
  end
27
26
 
28
- def set_add(key, value)
29
- update '$addToSet' => {key => value}
27
+ def set_members(key)
28
+ (find_one(key) || Set.new).to_set
30
29
  end
31
30
 
32
- def set_delete(key, value)
33
- update '$pull' => {key => value}
31
+ def set_add(key, value)
32
+ update key, {'$addToSet' => {'v' => value}}
34
33
  end
35
34
 
36
- def set_members(key)
37
- read_key(key) { Set.new }.to_set
35
+ def set_delete(key, value)
36
+ update key, {'$pull' => {'v' => value}}
38
37
  end
39
38
 
40
39
  private
41
40
 
42
- def id
43
- @id ||= @options.fetch(:id) { DefaultId }
44
- end
45
-
46
- def update(updates)
47
- @collection.update(@mongo_criteria, updates, @mongo_options)
41
+ def find_one(key)
42
+ if (doc = @collection.find_one(criteria(key)))
43
+ doc['v']
44
+ end
48
45
  end
49
46
 
50
- def read_key(key, &block)
51
- load
52
-
53
- if block_given?
54
- @document.fetch(key, &block)
55
- else
56
- @document[key]
57
- end
47
+ def update(key, updates)
48
+ @collection.update criteria(key), updates, @update_options
58
49
  end
59
50
 
60
- def load
61
- @document = @collection.find_one(@mongo_criteria) || {}
51
+ def criteria(key)
52
+ {:_id => key}
62
53
  end
63
54
  end
64
55
  end
@@ -0,0 +1,77 @@
1
+ require 'set'
2
+ require 'mongo'
3
+
4
+ module Flipper
5
+ module Adapters
6
+ class MongoSingleDocument
7
+ class Document
8
+ DefaultId = 'flipper'
9
+
10
+ def initialize(collection, options = {})
11
+ @collection = collection
12
+ @options = options
13
+ @id = @options.fetch(:id) { DefaultId }
14
+ @source = @options.fetch(:source) { {} }
15
+ @criteria = {:_id => @id}
16
+ @mongo_options = {:safe => true, :upsert => true}
17
+ end
18
+
19
+ def read(key)
20
+ source[key]
21
+ end
22
+
23
+ def write(key, value)
24
+ @collection.update @criteria, {'$set' => {key => value}}, @mongo_options
25
+ @source[key] = value
26
+ end
27
+
28
+ def delete(key)
29
+ @collection.update @criteria, {'$unset' => {key => 1}}, @mongo_options
30
+ @source.delete key
31
+ end
32
+
33
+ def set_members(key)
34
+ members = source.fetch(key) { @source[key] = Set.new }
35
+
36
+ if members.is_a?(Array)
37
+ @source[key] = members.to_set
38
+ else
39
+ members
40
+ end
41
+ end
42
+
43
+ def set_add(key, value)
44
+ @collection.update @criteria, {'$addToSet' => {key => value}}, @mongo_options
45
+ set_members(key).add(value)
46
+ end
47
+
48
+ def set_delete(key, value)
49
+ @collection.update @criteria, {'$pull' => {key => value}}, @mongo_options
50
+ set_members(key).delete(value)
51
+ end
52
+
53
+ def clear
54
+ @loaded = nil
55
+ @source.clear
56
+ end
57
+
58
+ def loaded?
59
+ @loaded == true
60
+ end
61
+
62
+ private
63
+
64
+ def source
65
+ load unless loaded?
66
+ @source
67
+ end
68
+
69
+ def load
70
+ @loaded = true
71
+ @source.clear
72
+ @source.update @collection.find_one(@criteria) || {}
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -1,7 +1,7 @@
1
1
  module Flipper
2
2
  module Adapters
3
3
  class Mongo
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
6
6
  end
7
7
  end
@@ -0,0 +1,25 @@
1
+ require 'set'
2
+ require 'forwardable'
3
+ require 'mongo'
4
+ require 'flipper/adapters/mongo/document'
5
+
6
+ module Flipper
7
+ module Adapters
8
+ class MongoSingleDocument
9
+ extend Forwardable
10
+
11
+ def initialize(collection, options = {})
12
+ @collection = collection
13
+ @options = options
14
+ end
15
+
16
+ def_delegators :document, :read, :write, :delete, :set_members, :set_add, :set_delete
17
+
18
+ private
19
+
20
+ def document
21
+ Document.new(@collection, :id => @options[:id])
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,275 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/mongo/document'
3
+
4
+ describe Flipper::Adapters::MongoSingleDocument::Document do
5
+ subject { described_class.new(collection, :id => id, :source => source) }
6
+ let(:collection) { Mongo::Connection.new.db('testing')['testing'] }
7
+ let(:id) { described_class::DefaultId }
8
+ let(:source) { {} }
9
+ let(:criteria) { {:_id => id} }
10
+ let(:options) { {:safe => true, :upsert => true} }
11
+
12
+ def document
13
+ collection.find_one(criteria)
14
+ end
15
+
16
+ before do
17
+ collection.remove(criteria)
18
+ end
19
+
20
+ it "defaults id to flipper" do
21
+ described_class.new(collection).instance_variable_get("@id").should eq('flipper')
22
+ end
23
+
24
+ describe "loading document" do
25
+ before do
26
+ collection.update(criteria, {'$set' => {'foo' => 'bar', 'people' => [1, 2, 3]}}, options)
27
+ end
28
+
29
+ it "only happens once" do
30
+ collection.should_receive(:find_one).with(criteria).once.and_return({})
31
+ subject.read('foo')
32
+ subject.set_members('people')
33
+ end
34
+
35
+ it "happens again if document is cleared" do
36
+ collection.should_receive(:find_one).with(criteria)
37
+ subject.read('foo')
38
+ subject.set_members('people')
39
+
40
+ subject.clear
41
+
42
+ collection.should_receive(:find_one).with(criteria)
43
+ subject.read('foo')
44
+ subject.set_members('people')
45
+ end
46
+ end
47
+
48
+ describe "#read" do
49
+ context "existing key" do
50
+ before do
51
+ source['baz'] = 'wick'
52
+ collection.update(criteria, {'$set' => {'foo' => 'bar'}}, options)
53
+ @result = subject.read('foo')
54
+ end
55
+
56
+ it "returns value" do
57
+ @result.should eq('bar')
58
+ end
59
+
60
+ it "clears and loads source hash" do
61
+ source.should eq({
62
+ '_id' => id,
63
+ 'foo' => 'bar',
64
+ })
65
+ end
66
+ end
67
+
68
+ context "missing key" do
69
+ before do
70
+ collection.update(criteria, {'$set' => {'foo' => 'bar'}}, options)
71
+ @result = subject.read('apple')
72
+ end
73
+
74
+ it "returns nil" do
75
+ @result.should be_nil
76
+ end
77
+ end
78
+
79
+ context "missing document" do
80
+ before do
81
+ @result = subject.read('foo')
82
+ end
83
+
84
+ it "returns nil" do
85
+ @result.should be_nil
86
+ end
87
+ end
88
+ end
89
+
90
+ describe "#write" do
91
+ context "existing key" do
92
+ before do
93
+ collection.update(criteria, {'$set' => {'foo' => 'bar'}}, options)
94
+ subject.write('foo', 'new value')
95
+ end
96
+
97
+ it "sets key" do
98
+ document.fetch('foo').should eq('new value')
99
+ source.fetch('foo').should eq('new value')
100
+ end
101
+ end
102
+
103
+ context "missing key" do
104
+ before do
105
+ collection.update(criteria, {'$set' => {'foo' => 'bar'}}, options)
106
+ subject.write('apple', 'orange')
107
+ end
108
+
109
+ it "sets key" do
110
+ document.fetch('apple').should eq('orange')
111
+ source.fetch('apple').should eq('orange')
112
+ end
113
+ end
114
+
115
+ context "missing document" do
116
+ before do
117
+ subject.write('foo', 'bar')
118
+ end
119
+
120
+ it "creates document" do
121
+ document.should_not be_nil
122
+ end
123
+
124
+ it "sets key" do
125
+ document.fetch('foo').should eq('bar')
126
+ source.fetch('foo').should eq('bar')
127
+ end
128
+ end
129
+ end
130
+
131
+ describe "#delete" do
132
+ before do
133
+ collection.update(criteria, {'$set' => {'foo' => 'bar', 'apple' => 'orange'}}, options)
134
+ @result = subject.delete('foo')
135
+ end
136
+
137
+ it "removes the key" do
138
+ document.key?('foo').should be_false
139
+ source.key?('foo').should be_false
140
+ end
141
+
142
+ it "does not remove other keys" do
143
+ document.fetch('apple').should eq('orange')
144
+ end
145
+ end
146
+
147
+ describe "#set_members" do
148
+ context "existing key" do
149
+ before do
150
+ collection.update(criteria, {'$set' => {'people' => [1, 2, 3], 'foo' => 'bar'}}, options)
151
+ @result = subject.set_members('people')
152
+ end
153
+
154
+ it "returns set" do
155
+ @result.should eq(Set[1, 2, 3])
156
+ end
157
+
158
+ it "loads source hash" do
159
+ source.should eq({
160
+ '_id' => id,
161
+ 'people' => Set[1, 2, 3],
162
+ 'foo' => 'bar',
163
+ })
164
+ end
165
+ end
166
+
167
+ context "missing key" do
168
+ before do
169
+ collection.update(criteria, {'$set' => {'people' => [1, 2, 3]}}, options)
170
+ @result = subject.set_members('users')
171
+ end
172
+
173
+ it "returns empty set" do
174
+ @result.should eq(Set.new)
175
+ end
176
+ end
177
+
178
+ context "missing document" do
179
+ it "returns empty set" do
180
+ subject.set_members('people').should eq(Set.new)
181
+ end
182
+ end
183
+ end
184
+
185
+ describe "#set_add" do
186
+ context "existing key" do
187
+ before do
188
+ collection.update(criteria, {'$set' => {'people' => [1, 2, 3]}}, options)
189
+ subject.set_add('people', 4)
190
+ end
191
+
192
+ it "adds value to set" do
193
+ document.fetch('people').should eq([1, 2, 3, 4])
194
+ source.fetch('people').should eq(Set[1, 2, 3, 4])
195
+ end
196
+ end
197
+
198
+ context "missing key" do
199
+ before do
200
+ collection.update(criteria, {'$set' => {'people' => [1, 2, 3]}}, options)
201
+ subject.set_add('users', 1)
202
+ end
203
+
204
+ it "adds value to set" do
205
+ document.fetch('users').should eq([1])
206
+ source.fetch('users').should eq(Set[1])
207
+ end
208
+ end
209
+
210
+ context "missing document" do
211
+ before do
212
+ subject.set_add('people', 1)
213
+ end
214
+
215
+ it "creates document" do
216
+ document.should_not be_nil
217
+ end
218
+
219
+ it "adds value to set" do
220
+ document.fetch('people').should eq([1])
221
+ source.fetch('people').should eq(Set[1])
222
+ end
223
+ end
224
+ end
225
+
226
+ describe "#set_delete" do
227
+ context "existing key" do
228
+ before do
229
+ collection.update(criteria, {'$set' => {'people' => [1, 2, 3]}}, options)
230
+ subject.set_delete 'people', 3
231
+ end
232
+
233
+ it "removes value to key" do
234
+ document.fetch('people').should eq([1, 2])
235
+ source.fetch('people').should eq(Set[1, 2])
236
+ end
237
+ end
238
+
239
+ context "missing key" do
240
+ before do
241
+ collection.update(criteria, {'$set' => {'people' => [1, 2, 3]}}, options)
242
+ end
243
+
244
+ it "does not error" do
245
+ expect { subject.set_delete 'foo', 1 }.to_not raise_error
246
+ end
247
+ end
248
+
249
+ context "missing document" do
250
+ it "does not error" do
251
+ expect { subject.set_delete 'foo', 1 }.to_not raise_error
252
+ end
253
+ end
254
+ end
255
+
256
+ describe "#clear" do
257
+ before do
258
+ collection.update(criteria, {'$set' => {'foo' => 'bar'}}, options)
259
+ subject.read('foo') # load the source hash
260
+ subject.clear
261
+ end
262
+
263
+ it "clears the source hash" do
264
+ source.should be_empty
265
+ end
266
+
267
+ it "does not remove the document" do
268
+ document.should_not be_empty
269
+ end
270
+
271
+ it "marks the document as not loaded" do
272
+ subject.loaded?.should be_false
273
+ end
274
+ end
275
+ end
@@ -0,0 +1,39 @@
1
+ require 'helper'
2
+ require 'flipper/adapters/mongo_single_document'
3
+ require 'flipper/spec/shared_adapter_specs'
4
+
5
+ describe Flipper::Adapters::MongoSingleDocument do
6
+ let(:collection) { Mongo::Connection.new.db('testing')['testing'] }
7
+ let(:criteria) { {:_id => id} }
8
+ let(:id) { 'flipper' }
9
+
10
+ subject { described_class.new(collection, :id => id) }
11
+
12
+ before do
13
+ collection.remove(criteria)
14
+ end
15
+
16
+ def read_key(key)
17
+ if (doc = collection.find_one(criteria))
18
+ value = doc[key]
19
+
20
+ if value.is_a?(::Array)
21
+ value = value.to_set
22
+ end
23
+
24
+ value
25
+ end
26
+ end
27
+
28
+ def write_key(key, value)
29
+ if value.is_a?(::Set)
30
+ value = value.to_a
31
+ end
32
+
33
+ options = {:upsert => true}
34
+ updates = {'$set' => {key => value}}
35
+ collection.update criteria, updates, options
36
+ end
37
+
38
+ it_should_behave_like 'a flipper adapter'
39
+ end
@@ -4,18 +4,17 @@ require 'flipper/spec/shared_adapter_specs'
4
4
 
5
5
  describe Flipper::Adapters::Mongo do
6
6
  let(:collection) { Mongo::Connection.new.db('testing')['testing'] }
7
- let(:criteria) { {:_id => id} }
8
- let(:id) { described_class::DefaultId }
7
+ let(:id) { 'flipper' }
9
8
 
10
- subject { Flipper::Adapters::Mongo.new(collection) }
9
+ subject { described_class.new(collection) }
11
10
 
12
11
  before do
13
- collection.remove(criteria)
12
+ collection.remove
14
13
  end
15
14
 
16
15
  def read_key(key)
17
- if (doc = collection.find_one(criteria))
18
- value = doc[key]
16
+ if (doc = collection.find_one(:_id => key))
17
+ value = doc['v']
19
18
 
20
19
  if value.is_a?(::Array)
21
20
  value = value.to_set
@@ -30,8 +29,9 @@ describe Flipper::Adapters::Mongo do
30
29
  value = value.to_a
31
30
  end
32
31
 
33
- options = {:upsert => true}
34
- updates = {'$set' => {key => value}}
32
+ criteria = {:_id => key}
33
+ updates = {'$set' => {'v' => value}}
34
+ options = {:upsert => true}
35
35
  collection.update criteria, updates, options
36
36
  end
37
37
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipper-mongo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,19 +9,19 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-08-03 00:00:00.000000000 Z
12
+ date: 2012-08-07 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: flipper
16
- requirement: &70164043485300 !ruby/object:Gem::Requirement
16
+ requirement: &70285634449460 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: 0.1.1
21
+ version: '0.2'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70164043485300
24
+ version_requirements: *70285634449460
25
25
  description: Mongo adapter for Flipper
26
26
  email:
27
27
  - nunemaker@gmail.com
@@ -38,10 +38,12 @@ files:
38
38
  - flipper-mongo.gemspec
39
39
  - lib/flipper-mongo.rb
40
40
  - lib/flipper/adapters/mongo.rb
41
+ - lib/flipper/adapters/mongo/document.rb
41
42
  - lib/flipper/adapters/mongo/version.rb
42
- - lib/flipper/adapters/mongo_with_ttl.rb
43
+ - lib/flipper/adapters/mongo_single_document.rb
44
+ - spec/flipper/adapters/mongo/document_spec.rb
45
+ - spec/flipper/adapters/mongo_single_document_spec.rb
43
46
  - spec/flipper/adapters/mongo_spec.rb
44
- - spec/flipper/adapters/mongo_with_ttl_spec.rb
45
47
  - spec/helper.rb
46
48
  - spec/support/accessor_helpers.rb
47
49
  homepage: http://jnunemaker.github.com/flipper-mongo
@@ -58,7 +60,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
58
60
  version: '0'
59
61
  segments:
60
62
  - 0
61
- hash: -3074058746914182371
63
+ hash: -10539439190267456
62
64
  required_rubygems_version: !ruby/object:Gem::Requirement
63
65
  none: false
64
66
  requirements:
@@ -67,7 +69,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
67
69
  version: '0'
68
70
  segments:
69
71
  - 0
70
- hash: -3074058746914182371
72
+ hash: -10539439190267456
71
73
  requirements: []
72
74
  rubyforge_project:
73
75
  rubygems_version: 1.8.10
@@ -75,7 +77,8 @@ signing_key:
75
77
  specification_version: 3
76
78
  summary: Mongo adapter for Flipper
77
79
  test_files:
80
+ - spec/flipper/adapters/mongo/document_spec.rb
81
+ - spec/flipper/adapters/mongo_single_document_spec.rb
78
82
  - spec/flipper/adapters/mongo_spec.rb
79
- - spec/flipper/adapters/mongo_with_ttl_spec.rb
80
83
  - spec/helper.rb
81
84
  - spec/support/accessor_helpers.rb
@@ -1,36 +0,0 @@
1
- require 'set'
2
- require 'mongo'
3
- require 'flipper/adapters/mongo'
4
-
5
- module Flipper
6
- module Adapters
7
- class MongoWithTTL < Mongo
8
- private
9
-
10
- # Override Mongo adapter's load
11
- def load
12
- if expired?
13
- @document = fresh_load
14
- end
15
- end
16
-
17
- def fresh_load
18
- @last_load_at = Time.now.to_i
19
- @collection.find_one(@mongo_criteria) || {}
20
- end
21
-
22
- def ttl
23
- @options.fetch(:ttl) { 0 }
24
- end
25
-
26
- def expired?
27
- return true if never_loaded?
28
- Time.now.to_i >= (@last_load_at + ttl)
29
- end
30
-
31
- def never_loaded?
32
- @last_load_at.nil?
33
- end
34
- end
35
- end
36
- end
@@ -1,44 +0,0 @@
1
- require 'helper'
2
- require 'flipper/adapters/mongo_with_ttl'
3
- require 'flipper/spec/shared_adapter_specs'
4
-
5
- describe Flipper::Adapters::MongoWithTTL do
6
- let(:collection) { Mongo::Connection.new.db('testing')['testing'] }
7
- let(:criteria) { {:_id => id} }
8
- let(:id) { described_class::DefaultId }
9
-
10
- subject { Flipper::Adapters::MongoWithTTL.new(collection) }
11
-
12
- before do
13
- collection.remove(criteria)
14
- end
15
-
16
- it_should_behave_like 'a flipper adapter'
17
-
18
- it "can cache document in process for a number of seconds" do
19
- options = {:ttl => 10}
20
- adapter = Flipper::Adapters::MongoWithTTL.new(collection, options)
21
- adapter.write('foo', 'bar')
22
- now = Time.now
23
- Timecop.freeze(now)
24
-
25
- collection.should_receive(:find_one).with(:_id => id)
26
- adapter.read('foo')
27
-
28
- adapter.read('foo')
29
- adapter.read('bar')
30
-
31
- Timecop.travel(3)
32
- adapter.read('foo')
33
-
34
- Timecop.travel(6)
35
- adapter.read('foo')
36
-
37
- collection.should_receive(:find_one).with(:_id => id)
38
- Timecop.travel(1)
39
- adapter.read('foo')
40
-
41
- Timecop.travel(4)
42
- adapter.read('foo')
43
- end
44
- end