candy 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,6 +8,7 @@ module Candy
8
8
  module Wrapper
9
9
 
10
10
  BSON_SAFE = [String,
11
+ Symbol,
11
12
  NilClass,
12
13
  TrueClass,
13
14
  FalseClass,
@@ -27,8 +28,8 @@ module Candy
27
28
  # Pass the simple cases through
28
29
  return thing if BSON_SAFE.include?(thing.class)
29
30
  case thing
30
- when Symbol
31
- wrap_symbol(thing)
31
+ # when Symbol
32
+ # wrap_symbol(thing)
32
33
  when Array
33
34
  wrap_array(thing)
34
35
  when Hash
@@ -47,21 +48,32 @@ module Candy
47
48
  end
48
49
  end
49
50
 
50
- # Takes an array and returns the same array with unsafe objects wrapped
51
+ # Takes an array and returns the same array with unsafe objects wrapped.
51
52
  def self.wrap_array(array)
52
53
  array.map {|element| wrap(element)}
53
54
  end
54
55
 
55
- # Takes a hash and returns it with both keys and values wrapped
56
+ # Takes a hash and returns it with values wrapped. Keys are converted to strings, and
57
+ # wrapped with single quotes if they were already strings. (So that we can easily tell
58
+ # hash keys from string keys later on.)
56
59
  def self.wrap_hash(hash)
57
60
  wrapped = {}
58
- hash.each {|k, v| wrapped[wrap(k)] = wrap(v)}
61
+ hash.each do |key, value|
62
+ case key
63
+ when Symbol
64
+ wrapped[key.to_s] = wrap(value)
65
+ when String
66
+ wrapped["'#{key}'"] = wrap(value)
67
+ else
68
+ wrapped[wrap(key)] = wrap(value)
69
+ end
70
+ end
59
71
  wrapped
60
72
  end
61
73
 
62
- # Returns a string that's distinctive enough for us to unwrap later and produce the same symbol
74
+ # Returns a string that's distinctive enough for us to unwrap later and produce the same symbol.
63
75
  def self.wrap_symbol(symbol)
64
- "__sym_" + symbol.to_s
76
+ "__:" + symbol.to_s
65
77
  end
66
78
 
67
79
  # Returns a nested hash containing the class and instance variables of the object. It's not the
@@ -89,19 +101,33 @@ module Candy
89
101
  unwrap_hash(thing)
90
102
  end
91
103
  when Array
92
- thing.map {|element| unwrap(element)}
93
- when /^__sym_(.+)/
94
- $1.to_sym
104
+ CandyArray.embed(*thing.map {|element| unwrap(element)})
105
+ # when /^__:(.+)/
106
+ # $1.to_sym
95
107
  else
96
108
  thing
97
109
  end
98
110
  end
99
111
 
100
- # Traverses the hash, unwrapping both keys and values. Returns the hash that results.
112
+ # Traverses the hash, unwrapping values and converting keys back to symbols. Returns
113
+ # the results as a CandyHash object.
101
114
  def self.unwrap_hash(hash)
102
115
  unwrapped = {}
103
- hash.each {|k,v| unwrapped[unwrap(k)] = unwrap(v)}
104
- unwrapped
116
+ hash.each do |key, value|
117
+ case key
118
+ when /^'(.+)'$/
119
+ unwrapped[$1] = unwrap(value)
120
+ when String
121
+ unwrapped[key.to_sym] = unwrap(value)
122
+ else
123
+ unwrapped[unwrap(key)] = unwrap(value)
124
+ end
125
+ end
126
+ if klass = unwrapped.delete(CLASS_KEY)
127
+ qualified_const_get(klass).embed(unwrapped)
128
+ else
129
+ CandyHash.embed(unwrapped)
130
+ end
105
131
  end
106
132
 
107
133
  # Turns a hashed object back into an object of the stated class, setting any captured instance
@@ -0,0 +1,50 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Candy::CandyArray do
4
+ before(:all) do
5
+ @verifier = Zagnut.collection
6
+ end
7
+
8
+ before(:each) do
9
+ @this = Zagnut.new
10
+ @this.bits = ['peanut', 'almonds', 'titanium']
11
+ end
12
+
13
+ it "writes the array" do
14
+ @verifier.find_one['bits'][1].should == 'almonds'
15
+ end
16
+
17
+ it "reads the array" do
18
+ that = Zagnut(@this.id)
19
+ that.bits[2].should == 'titanium'
20
+ end
21
+
22
+ it "cascades appends" do
23
+ @this.bits << 'kryptonite'
24
+ that = Zagnut(@this.id)
25
+ that.bits[-1].should == 'kryptonite'
26
+ end
27
+
28
+ it "cascades substitutions" do
29
+ @this.bits[0] = 'raisins'
30
+ that = Zagnut(@this.id)
31
+ that.bits.should == ['raisins', 'almonds', 'titanium']
32
+ end
33
+
34
+ it "cascades deletions" do
35
+ @this.bits.shift.should == 'peanut'
36
+ that = Zagnut(@this.id)
37
+ that.bits.size.should == 2
38
+ end
39
+
40
+ it "cascades deeply" do
41
+ @this.bits.push [5, 11, {foo: [:bar]}]
42
+ that = Zagnut(@this.id)
43
+ that.bits[3][2][:foo][0].should == :bar
44
+ end
45
+
46
+ after(:each) do
47
+ Zagnut.collection.remove
48
+ end
49
+
50
+ end
@@ -0,0 +1,102 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Candy::Collection do
4
+
5
+ before(:each) do
6
+ @this = Zagnut.new
7
+ @this.color = "red"
8
+ @this.weight = 11.8
9
+ @that = Zagnut.new
10
+ @that.color = "red"
11
+ @that.pieces = 6
12
+ @that.weight = -5
13
+ @the_other = Zagnut.new
14
+ @the_other.color = "blue"
15
+ @the_other.pieces = 7
16
+ @the_other.weight = 0
17
+ end
18
+
19
+
20
+ it "can get all objects matching a search condition" do
21
+ those = Zagnuts.color("red")
22
+ those.count.should == 2
23
+ end
24
+
25
+ it "can get all objects in a collection" do
26
+ those = Zagnuts.all
27
+ those.count.should == 3
28
+ end
29
+
30
+ it "still returns if nothing matches" do
31
+ Zagnuts.color("green").to_a.should == []
32
+ end
33
+
34
+ it "can take options" do
35
+ those = Zagnuts.color("red").sort("weight", :down)
36
+ those.collect{|z| z.weight}.should == [11.8, -5]
37
+ end
38
+
39
+ it "can be iterated" do
40
+ these = Zagnuts.color("red").sort(:weight)
41
+ this = these.first
42
+ this.pieces.should == 6
43
+ this.weight.should == -5
44
+ this = these.next
45
+ this.pieces.should be_nil
46
+ this.weight.should == 11.8
47
+ end
48
+
49
+ it "can take scoping on a class or instance level" do
50
+ these = Zagnuts.color("red")
51
+ these.pieces(6)
52
+ these.count.should == 1
53
+ end
54
+
55
+ it "can get the collection by a method at the top level" do
56
+ these = Zagnuts
57
+ these.count.should == 3
58
+ end
59
+
60
+ # Test class for scoped magic method generation
61
+ class BabyRuth
62
+ include Candy::Piece
63
+ end
64
+
65
+ class BabiesRuth
66
+ include Candy::Collection
67
+ collects :baby_ruth
68
+ end
69
+
70
+ it "can get the object by a method within the enclosing namespace" do
71
+ foo = BabyRuth.new
72
+ foo.color = 'green'
73
+ bar = BabiesRuth(color: 'green')
74
+ bar.count.should == 1
75
+ end
76
+
77
+ # Test class to verify magic method generation doesn't override anything
78
+ def Jawbreakers(param=nil)
79
+ "Broken everywhere!"
80
+ end
81
+
82
+ class Jawbreaker
83
+ include Candy::Piece
84
+ end
85
+
86
+ class Jawbreakers
87
+ include Candy::Collection
88
+ collects :jawbreaker
89
+ end
90
+
91
+ it "doesn't create a namespace method if one already exists" do
92
+ too = Jawbreaker.new
93
+ too.calories = 55
94
+ tar = Jawbreakers()
95
+ tar.should == 'Broken everywhere!'
96
+ end
97
+
98
+
99
+ after(:each) do
100
+ Zagnut.collection.remove
101
+ end
102
+ end
@@ -11,7 +11,7 @@ describe Candy::Crunch do
11
11
  describe "connection" do
12
12
  before(:each) do
13
13
  # Make sure we don't waste time making bogus connections
14
- PeanutBrittle.options[:connect] = false
14
+ Candy.connection_options[:connect] = false
15
15
  end
16
16
 
17
17
  it "takes yours if you give it one" do
@@ -24,39 +24,22 @@ describe Candy::Crunch do
24
24
  PeanutBrittle.connection.nodes.should == [["localhost", 27017]]
25
25
  end
26
26
 
27
- it "uses the host you provide" do
28
- PeanutBrittle.host = 'example.org'
29
- PeanutBrittle.connection.nodes.should == [["example.org", 27017]]
30
- end
31
-
32
- it "uses the port you provide" do
33
- PeanutBrittle.host = 'localhost'
34
- PeanutBrittle.port = 3000
35
- PeanutBrittle.connection.nodes.should == [["localhost", 3000]]
36
- end
37
27
 
38
- it "uses any options you provide" do
39
- l = Logger.new(STDOUT)
40
- PeanutBrittle.options[:logger] = l
41
- PeanutBrittle.connection.logger.should == l
42
- end
43
-
44
- it "uses the $MONGO_HOST setting if you don't override it" do
45
- $MONGO_HOST = 'example.net'
28
+ it "uses the Candy.host setting if you don't override it" do
29
+ Candy.host = 'example.net'
46
30
  PeanutBrittle.connection.nodes.should == [["example.net", 27017]]
47
31
  end
48
32
 
49
- it "uses the $MONGO_PORT setting if you don't override it" do
50
- PeanutBrittle.host = 'localhost'
51
- $MONGO_PORT = 33333
33
+ it "uses the Candy.port setting if you don't override it" do
34
+ Candy.host = 'localhost'
35
+ Candy.port = 33333
52
36
  PeanutBrittle.connection.nodes.should == [["localhost", 33333]]
53
37
  end
54
38
 
55
- it "uses the $MONGO_OPTIONS setting if you don't override it" do
39
+ it "uses the Candy.connection_options setting if you don't override it" do
56
40
  l = Logger.new(STDOUT)
57
- PeanutBrittle.options = nil # Gotta be careful of our order on this one
58
- $MONGO_OPTIONS = {:logger => l, :connect => false}
59
- PeanutBrittle.connection.logger.should == $MONGO_OPTIONS[:logger]
41
+ Candy.connection_options = {:logger => l, :connect => false}
42
+ PeanutBrittle.connection.logger.should == Candy.connection_options[:logger]
60
43
  end
61
44
 
62
45
  it "clears the database when you set it" do
@@ -66,18 +49,16 @@ describe Candy::Crunch do
66
49
  end
67
50
 
68
51
  after(:each) do
69
- $MONGO_HOST = nil
70
- $MONGO_PORT = nil
71
- $MONGO_OPTIONS = nil
72
- PeanutBrittle.host = nil
73
- PeanutBrittle.port = nil
74
- PeanutBrittle.options = nil
52
+ Candy.host = nil
53
+ Candy.port = nil
54
+ Candy.connection_options = nil
55
+ PeanutBrittle.connection = nil
75
56
  end
76
57
  end
77
58
 
78
59
  describe "database" do
79
60
  before(:each) do
80
- $MONGO_DB = nil
61
+ Candy.db = nil
81
62
  end
82
63
 
83
64
  it "takes yours if you give it one" do
@@ -95,8 +76,8 @@ describe Candy::Crunch do
95
76
  lambda{PeanutBrittle.db = 5}.should raise_error(Candy::ConnectionError, "The db attribute needs a Mongo::DB object or a name string.")
96
77
  end
97
78
 
98
- it "uses the $MONGO_DB setting if you don't override it" do
99
- $MONGO_DB = 'foobar'
79
+ it "uses the Candy.db setting if you don't override it" do
80
+ Candy.db = 'foobar'
100
81
  PeanutBrittle.db.name.should == 'foobar'
101
82
  end
102
83
 
@@ -118,7 +99,7 @@ describe Candy::Crunch do
118
99
  end
119
100
 
120
101
  after(:all) do
121
- $MONGO_DB = 'candy_test' # Get back to our starting point
102
+ Candy.db = 'candy_test' # Get back to our starting point
122
103
  end
123
104
  end
124
105
 
@@ -0,0 +1,38 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Candy::CandyHash do
4
+ before(:all) do
5
+ @verifier = Zagnut.collection
6
+ end
7
+
8
+ before(:each) do
9
+ @this = Zagnut.new
10
+ @this.filling = {taste: 'caramel', ounces: 0.75}
11
+ end
12
+
13
+ it "writes the hash" do
14
+ @verifier.find_one['filling']['ounces'].should == 0.75
15
+ end
16
+
17
+ it "reads the hash" do
18
+ that = Zagnut(@this.id)
19
+ that.filling.taste.should == 'caramel'
20
+ that.filling.should be_a(Hash)
21
+ end
22
+
23
+ it "cascades changes" do
24
+ @this.filling[:calories] = 250
25
+ that = Zagnut(@this.id)
26
+ that.filling.calories.should == 250
27
+ end
28
+
29
+ it "cascades deeply" do
30
+ @this.filling.subfilling = {texture: :gravel}
31
+ that = Zagnut(@this.id)
32
+ that.filling.subfilling.texture.should == :gravel
33
+ end
34
+
35
+ after(:each) do
36
+ Zagnut.collection.remove
37
+ end
38
+ end
@@ -0,0 +1,259 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe Candy::Piece do
4
+
5
+ class Nougat
6
+ attr_accessor :foo
7
+ end
8
+
9
+ before(:all) do
10
+ @verifier = Zagnut.collection
11
+ end
12
+
13
+ before(:each) do
14
+ @this = Zagnut.new
15
+ end
16
+
17
+
18
+ it "lazy inserts" do
19
+ @this.id.should be_nil
20
+ end
21
+
22
+ it "knows its ID after inserting" do
23
+ @this.name = 'Zagnut'
24
+ @this.id.should be_a(Mongo::ObjectID)
25
+ end
26
+
27
+
28
+ it "can be given a hash of data to insert immediately" do
29
+ that = Zagnut.new({calories: 500, morsels: "chewy"})
30
+ @verifier.find_one(calories: 500)["morsels"].should == "chewy"
31
+ end
32
+
33
+ it "saves any attribute it doesn't already handle to the database" do
34
+ @this.bite = "Tasty!"
35
+ @verifier.find_one["bite"].should == "Tasty!"
36
+ end
37
+
38
+ it "retrieves any attribute it doesn't already know about from the database" do
39
+ @this.chew = "Munchy!"
40
+ @verifier.update({:_id => @this.id}, {:chew => "Yummy!"})
41
+ that = Zagnut(@this.id)
42
+ that.chew.should == "Yummy!"
43
+ end
44
+
45
+ it "can roundtrip effectively" do
46
+ @this.swallow = "Gulp."
47
+ @this.swallow.should == "Gulp."
48
+ end
49
+
50
+ it "handles missing attributes gracefully" do
51
+ @this.licks.should == nil
52
+ end
53
+
54
+ it "allows multiple attributes to be set" do
55
+ @this.licks = 7
56
+ @this.center = 0.5
57
+ @this.licks.should == 7
58
+ end
59
+
60
+
61
+ it "wraps objects" do
62
+ nougat = Nougat.new
63
+ nougat.foo = 5
64
+ @this.center = nougat
65
+ @verifier.find_one["center"]["__object_"]["class"].should == Nougat.name
66
+ end
67
+
68
+ it "unwraps objects" do
69
+ @this.blank = "" # To force a save
70
+ center = Nougat.new
71
+ center.foo = :bar
72
+ @verifier.update({'_id' => @this.id}, '$set' => {:center => {"__object_" => {:class => Nougat.name, :ivars => {"@foo" => 'bar'}}}})
73
+ @this.refresh.center.should be_a(Nougat)
74
+ @this.center.instance_variable_get(:@foo).should == 'bar'
75
+ end
76
+
77
+ it "wraps symbols" do
78
+ @this.crunch = :chomp
79
+ @verifier.find_one["crunch"].should == :chomp
80
+ end
81
+
82
+
83
+ it "considers objects equal if they point to the same MongoDB ref" do
84
+ @this.blank = ""
85
+ that = Zagnut(@this.id)
86
+ that.should == @this
87
+ end
88
+
89
+ it "considers objects unequal if they don't have the same MongoDB ref" do
90
+ @this.calories = 5
91
+ that = Zagnut.new(calories: 5)
92
+ @this.should_not == that
93
+ end
94
+
95
+ it "considers objects unequal if this one hasn't been saved yet" do
96
+ that = Zagnut.new
97
+ @this.should_not == that
98
+ end
99
+
100
+ it "considers objects unequal if compared to something without an id" do
101
+ that = Object.new
102
+ @this.should_not == that
103
+ end
104
+
105
+
106
+
107
+ describe "retrieval" do
108
+ it "can find a record by its ID" do
109
+ @this.licks = 10
110
+ that = Zagnut.first(@this.id)
111
+ that.licks.should == 10
112
+ end
113
+
114
+ it "returns nil on an object that can't be found" do
115
+ id = Mongo::ObjectID.new
116
+ Zagnut(id).should be_nil
117
+ end
118
+
119
+ it "can get a single object by attributes" do
120
+ @this.pieces = 7.5
121
+ @this.color = "red"
122
+ that = Zagnut.first("pieces" => 7.5)
123
+ that.color.should == "red"
124
+ end
125
+
126
+ it "returns nil if a first object can't be found" do
127
+ @this.pieces = 11
128
+ Zagnut.first("pieces" => 5).should be_nil
129
+ end
130
+
131
+ it "can get a single object by attribute method" do
132
+ @this.color = "blue"
133
+ @this.smushy = true
134
+ that = Zagnut.color("blue")
135
+ that.should be_smushy
136
+ end
137
+
138
+ it "can get the object by a method at the top level" do
139
+ @this.intensity = :Yowza
140
+ that = Zagnut(@this.id)
141
+ that.intensity.should == :Yowza
142
+ end
143
+
144
+ # Test class for scoped magic method generation
145
+ class BabyRuth
146
+ include Candy::Piece
147
+ end
148
+
149
+ it "can get the object by a method within the enclosing namespace" do
150
+ foo = BabyRuth.new
151
+ foo.color = 'green'
152
+ bar = BabyRuth(foo.id)
153
+ bar.color.should == 'green'
154
+ end
155
+
156
+ # Test class to verify magic method generation doesn't override anything
157
+ def Jawbreaker(param=nil)
158
+ "Broken!"
159
+ end
160
+
161
+ class Jawbreaker
162
+ include Candy::Piece
163
+ end
164
+
165
+ it "doesn't create a namespace method if one already exists" do
166
+ too = Jawbreaker.new
167
+ too.calories = 55
168
+ tar = Jawbreaker(too.id)
169
+ tar.should == 'Broken!'
170
+ end
171
+
172
+ end
173
+
174
+ describe "updates" do
175
+ before(:each) do
176
+ @this.ounces = 17
177
+ @this.crunchy = :very
178
+ @that = Zagnut.new
179
+ @that.ounces = 11
180
+ @that.crunchy = :very
181
+ end
182
+
183
+ it "will insert a document if the key field's value isn't found" do
184
+ Zagnut.update(:ounces, {ounces: 15, crunchy: :not_very, flavor: 'butterscotch'})
185
+ @verifier.count.should == 3
186
+ Zagnut.ounces(15).flavor.should == 'butterscotch'
187
+ end
188
+
189
+ it "will update a document if the key field's value is found" do
190
+ Zagnut.update(:ounces, {ounces: 11, crunchy: :barely, salt: 0})
191
+ @verifier.count.should == 2
192
+ @that.refresh
193
+ @that.crunchy.should == :barely
194
+ end
195
+
196
+ it "can match on multiple keys" do
197
+ Zagnut.update([:crunchy, :ounces], {ounces: 17, crunchy: :very, calories: 715})
198
+ @verifier.count.should == 2
199
+ @this.refresh
200
+ @this.calories.should == 715
201
+ end
202
+
203
+ it "won't match on multiple keys if they aren't all found" do
204
+ Zagnut.update([:crunchy, :ounces], {ounces: 11, crunchy: :not_quite, color: 'brown'})
205
+ @verifier.count.should == 3
206
+ Zagnut.crunchy(:not_quite).color.should == 'brown'
207
+ end
208
+ end
209
+
210
+ describe "embedding" do
211
+ describe "Candy objects" do
212
+ before(:each) do
213
+ @inner = KitKat.new
214
+ @inner.crunch = 'wafer'
215
+ @this.inner = @inner
216
+ end
217
+
218
+ it "writes the object" do
219
+ @verifier.find_one['inner']['crunch'].should == 'wafer'
220
+ end
221
+
222
+ it "reads the object" do
223
+ that = Zagnut(@this.id)
224
+ that.inner.crunch.should == 'wafer'
225
+ end
226
+
227
+ it "maintains the class" do
228
+ that = Zagnut(@this.id)
229
+ that.inner.should be_a(KitKat)
230
+ end
231
+
232
+ it "cascades changes" do
233
+ @this.inner.coating = 'chocolate'
234
+ @verifier.find_one['inner']['coating'].should == 'chocolate'
235
+ end
236
+
237
+ it "cascades deeply" do
238
+ @this.inner.inner = Zagnut.embed(beauty: 'recursive!')
239
+ that = Zagnut(@this.id)
240
+ that.inner.inner.beauty.should == 'recursive!'
241
+ end
242
+ end
243
+
244
+
245
+
246
+
247
+ end
248
+
249
+
250
+
251
+
252
+
253
+
254
+ after(:each) do
255
+ KitKat.collection.remove
256
+ Zagnut.collection.remove
257
+ end
258
+
259
+ end