candy 0.1.0 → 0.2.1

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.
@@ -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