candy 0.2.7 → 0.2.8

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,6 +3,14 @@ Candy History
3
3
 
4
4
  This document aims to provide only an overview. Further, we've only really been tracking things since **v0.2**. For obsessive detail, just check out the `git log`.
5
5
 
6
+ v0.2.8 - 2010-05-13 (the "Holy crap, that was ten pomodoros" release)
7
+ ---------------------------------------------------------------------
8
+ Major refactoring to fix a major bug: embedded documents weren't being loaded properly on document retrieval. This resulted in a lot of code being moved around, and some regrettable circular connascence between Piece and Wrapper that I hope to address later. Overall, though, it's simpler now.
9
+
10
+ **API CHANGE:** The `.embed` class method has two new required parameters and is now used _only_ when you know the parent you want to embed something in. To make a Candy piece that you don't want to be saved right away, use `.piece` instead.
11
+
12
+ * Fixed Github issue #11
13
+
6
14
  v0.2.7 - 2010-05-05 (the "yes, you MAY put your peanut butter in my chocolate" release)
7
15
  --------------------------------------------------------------------------------------
8
16
  Found and fixed a convoluted bug that was preventing embedded Candy objects from being saved properly. (It was treating them as _non_-Candy objects, which makes the world a gray and boring place.) While I was at it, refactored some methods and chipped away at some complexity.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.7
1
+ 0.2.8
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{candy}
8
- s.version = "0.2.7"
8
+ s.version = "0.2.8"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Stephen Eley"]
12
- s.date = %q{2010-05-05}
12
+ s.date = %q{2010-05-13}
13
13
  s.description = %q{Candy provides simple, transparent object persistence for the MongoDB database. Classes that
14
14
  include Candy modules save all properties to Mongo automatically, can be recursively embedded,
15
15
  and can retrieve records with chainable open-ended class methods, eliminating the need for
@@ -10,23 +10,23 @@ module Candy
10
10
  include Crunch
11
11
  include Embeddable
12
12
 
13
- # Included for purposes of 'embeddable' compatibility, but does nothing except pass its
14
- # parameters to the new object. Since you can't save an array on its own anyway, there's
15
- # no need to flag it as "don't save."
16
- def self.embed(*args, &block)
17
- self.new(*args, &block)
13
+ # Creates the object with parent and attribute values set properly on the object and any children.
14
+ def self.embed(parent, attribute, *args)
15
+ this = self.new(*args)
16
+ this.candy_adopt(parent, attribute)
18
17
  end
19
18
 
20
19
  # Sets the initial array state.
21
- def initialize(*args, &block)
22
- @__candy = args
20
+ def initialize(*args)
21
+ @__candy = from_candy(args)
22
+ super()
23
23
  end
24
24
 
25
25
  # Set a value at a specified index in our array. Note that this operation _does not_ confirm that the
26
26
  # array in Mongo still matches our current state. If concurrent updates have happened, you might end up
27
27
  # overwriting something other than what you thought.
28
28
  def []=(index, val)
29
- property = candy_coat(@__candy_parent_key, val)
29
+ property = candy_coat(nil, val) # There are no attribute names on array inheritance
30
30
  @__candy_parent.set embedded(index => property)
31
31
  self.candy[index] = property
32
32
  end
@@ -47,8 +47,8 @@ module Candy
47
47
  # Pops the front off the MongoDB array and returns it, then resyncs the array.
48
48
  # (Thus supporting real-time concurrency for queue-like behavior.)
49
49
  def shift(n=1)
50
- doc = @__candy_parent.findAndModify({"_id" => @__candy_parent.id}, {'$pop' => {@__candy_parent_key => -1}})
51
- @__candy = doc['value'][@__candy_parent_key.to_s]
50
+ doc = @__candy_parent.collection.find_and_modify query: {"_id" => @__candy_parent.id}, update: {'$pop' => {@__candy_parent_key => -1}}, new: false
51
+ @__candy = doc[@__candy_parent_key.to_s]
52
52
  @__candy.shift
53
53
  end
54
54
 
@@ -59,6 +59,11 @@ module Candy
59
59
  alias_method :to_candy, :candy
60
60
  alias_method :to_ary, :candy
61
61
 
62
+ # Unwraps all elements of the array, telling them who their parent is. The attribute doesn't matter because arrays don't have them.
63
+ def from_candy(array)
64
+ array.map {|element| Wrapper.unwrap(element, self)}
65
+ end
66
+
62
67
  # Array equality.
63
68
  def ==(val)
64
69
  self.to_ary == val
@@ -70,5 +75,11 @@ module Candy
70
75
  end
71
76
  alias_method :size, :length
72
77
 
78
+ protected
79
+
80
+ # Sets the array. Primarily used by the .embed class method.
81
+ def candy=(val)
82
+ @__candy = val
83
+ end
73
84
  end
74
85
  end
@@ -3,11 +3,13 @@ module Candy
3
3
  # Shared methods to create associations between top-level objects and embedded objects (hashes,
4
4
  # arrays, or Candy::Pieces).
5
5
  module Embeddable
6
+
6
7
  # Tells an embedded object about its parent. When its own state changes, it can use this
7
8
  # information to write home and update the parent.
8
9
  def candy_adopt(parent, attribute)
9
10
  @__candy_parent = parent
10
11
  @__candy_parent_key = attribute
12
+ self
11
13
  end
12
14
 
13
15
  private
@@ -20,16 +22,18 @@ module Candy
20
22
 
21
23
  # Convert hashes and arrays to CandyHashes and CandyArrays, and set the parent key for any Candy pieces.
22
24
  def candy_coat(key, value)
23
- piece = case value
24
- when CandyHash then value
25
- when Hash then CandyHash.embed(value)
26
- when CandyArray then value
27
- when Array then CandyArray.embed(*value) # Explode our array into separate arguments
25
+ case value
26
+ when Hash then CandyHash.embed(self, key, value)
27
+ when Array then CandyArray.embed(self, key, *value) # Explode our array into separate arguments
28
+ when CandyHash then value.candy_adopt(self, key)
29
+ when CandyArray then value.candy_adopt(self, key)
30
+ else
31
+ if value.respond_to?(:candy_adopt)
32
+ value.candy_adopt(self, key)
28
33
  else
29
34
  value
30
35
  end
31
- piece.candy_adopt(self, key) if piece.respond_to?(:candy_adopt)
32
- piece
36
+ end
33
37
  end
34
38
 
35
39
  end
@@ -33,13 +33,32 @@ module Candy
33
33
  collection.update search_keys, fields, :upsert => true
34
34
  end
35
35
 
36
+
37
+ # Deep magic! Finds and returns a single object by the named attribute.
38
+ def method_missing(name, *args, &block)
39
+ if args.size == 1 or args.size == 2 # If we don't have a value, or have more than
40
+ search = {name => args.shift} # just a simple options hash, this must not be for us.
41
+ search.merge!(args.shift) if args[0] # We might have other conditions
42
+ first(search)
43
+ else
44
+ super
45
+ end
46
+ end
47
+
48
+ # Creates the object with parent and attribute values set properly on the object and any children.
49
+ def embed(parent, attribute, *args)
50
+ this = self.piece(*args)
51
+ this.candy_adopt(parent, attribute)
52
+ end
53
+
54
+
36
55
  # Makes a new object with a given state that is _not_ immediately saved, but is
37
56
  # held in memory instead. The principal use for this is to embed it in other
38
57
  # documents. Except for the unsaved state, this functions identically to 'new'
39
58
  # and will pass all its arguments to the initializer. (Note that you can still
40
59
  # embed documents that _have_ been saved--but then you'll have the data in two
41
60
  # places.)
42
- def embed(*args, &block)
61
+ def piece(*args)
43
62
  if args[-1].is_a?(Hash)
44
63
  args[-1].merge!(EMBED_KEY => true)
45
64
  else
@@ -47,17 +66,7 @@ module Candy
47
66
  end
48
67
  self.new(*args)
49
68
  end
50
-
51
- # Deep magic! Finds and returns a single object by the named attribute.
52
- def method_missing(name, *args, &block)
53
- if args.size == 1 or args.size == 2 # If we don't have a value, or have more than
54
- search = {name => args.shift} # just a simple options hash, this must not be for us.
55
- search.merge!(args.shift) if args[0] # We might have other conditions
56
- first(search)
57
- else
58
- super
59
- end
60
- end
69
+
61
70
 
62
71
  private
63
72
  # Creates a method in the same namespace as the included class that points to
@@ -72,16 +81,15 @@ module Candy
72
81
  include Embeddable
73
82
 
74
83
 
75
- # Our initializer checks the LAST argument passed to it, and pops it off the chain if it's a hash.
76
- # If the hash contains an '_id' field we assume we're being constructed from a MongoDB document;
77
- # otherwise we assume we're a new document and insert ourselves into the database.
84
+ # Our initializer expects the last argument to be a hash of values. If the hash contains an '_id'
85
+ # field we assume we're being constructed from a MongoDB document and we unwrap the remaining
86
+ # values; otherwise we assume we're a new document and set any values in the hash as if they
87
+ # were assigned. Any other arguments are not our business and will be passed down the chain.
78
88
  def initialize(*args, &block)
79
89
  if args[-1].is_a?(Hash)
80
90
  data = args.pop
81
- if @__candy_id = data.delete('_id') # We're an existing document
82
- @__candy = self.from_candy(Wrapper.unwrap(data))
83
- elsif data.delete(EMBED_KEY) # We're being embedded: take any data, but don't save to Mongo
84
- @__candy = data
91
+ if data.delete(EMBED_KEY) or @__candy_id = data.delete('_id') # We're an embedded or existing document
92
+ @__candy = self.from_candy(data)
85
93
  else
86
94
  data.each {|key, value| send("#{key}=", value)} # Assign all the data we're given
87
95
  end
@@ -96,7 +104,7 @@ module Candy
96
104
 
97
105
  # Pull our document from the database if we know our ID.
98
106
  def retrieve_document
99
- Wrapper.unwrap(collection.find_one({'_id' => id})) if id
107
+ from_candy(collection.find_one({'_id' => id})) if id
100
108
  end
101
109
 
102
110
 
@@ -169,11 +177,15 @@ module Candy
169
177
  candy.merge(CLASS_KEY => self.class.name)
170
178
  end
171
179
 
172
- # A hoook for specific object classes to set their internal state using the hash passed in by
173
- # MongoDB. If you override this method, delete any hash keys you need for your own purposes
174
- # and then call 'super' on the remainder.
180
+ # Unwraps the values passed to us from MongoDB, setting parent attributes on any embedded Candy
181
+ # objects.
175
182
  def from_candy(hash)
176
- hash
183
+ unwrapped = {}
184
+ hash.each do |key, value|
185
+ field = Wrapper.unwrap_key(key)
186
+ unwrapped[field] = Wrapper.unwrap(value, self, field)
187
+ end
188
+ unwrapped
177
189
  end
178
190
 
179
191
 
@@ -52,24 +52,28 @@ module Candy
52
52
  array.map {|element| wrap(element)}
53
53
  end
54
54
 
55
- # Takes a hash and returns it with values wrapped. Keys are converted to strings, and
56
- # wrapped with single quotes if they were already strings. (So that we can easily tell
57
- # hash keys from string keys later on.)
55
+ # Takes a hash and returns it with values wrapped. Symbol keys are reversibly converted to strings.
58
56
  def self.wrap_hash(hash)
59
57
  wrapped = {}
60
58
  hash.each do |key, value|
61
- case key
62
- when Symbol
63
- wrapped[key.to_s] = wrap(value)
64
- when String
65
- wrapped["'#{key}'"] = wrap(value)
66
- else
67
- wrapped[wrap(key)] = wrap(value)
68
- end
59
+ wrapped[wrap_key(key)] = wrap(value)
69
60
  end
70
61
  wrapped
71
62
  end
72
63
 
64
+ # Lightly wraps hash keys, converting symbols to strings and wrapping strings in single quotes.
65
+ # Thus, we can recover symbols when we _unwrap_ them later. Other key types will raise an exception.
66
+ def self.wrap_key(key)
67
+ case key
68
+ when String
69
+ "'#{key}'"
70
+ when Symbol
71
+ key.to_s
72
+ else
73
+ raise TypeError, "Candy field names must be strings or symbols. You gave us #{key.class}: #{key}"
74
+ end
75
+ end
76
+
73
77
  # Returns a nested hash containing the class and instance variables of the object. It's not the
74
78
  # deepest we could ever go (it doesn't handle singleton methods, etc.) but it's a start.
75
79
  def self.wrap_object(object)
@@ -84,43 +88,53 @@ module Candy
84
88
  {"__object_" => wrapped}
85
89
  end
86
90
 
87
- # Undoes any complicated magic from the Wrapper.wrap method. Almost everything falls through
88
- # untouched except for symbol strings and hashed objects.
89
- def self.unwrap(thing)
91
+ # Undoes any magic from the Wrapper.wrap method. Almost everything falls through
92
+ # untouched except for arrays and hashes. The 'parent' and 'attribute' parameters
93
+ # are for recursively setting the parent properties of embedded Candy objects.
94
+ def self.unwrap(thing, parent=nil, attribute=nil)
90
95
  case thing
91
96
  when Hash
92
- if thing["__object_"]
97
+ if thing.has_key?("__object_")
93
98
  unwrap_object(thing)
94
99
  else
95
- unwrap_hash(thing)
100
+ unwrap_hash(thing, parent, attribute)
96
101
  end
97
102
  when Array
98
- CandyArray.embed(*thing.map {|element| unwrap(element)})
99
- # when /^__:(.+)/
100
- # $1.to_sym
103
+ if parent # We only want to create CandyArrays inside Candy pieces
104
+ CandyArray.embed(parent, attribute, *thing)
105
+ else
106
+ thing.collect {|element| unwrap(element)}
107
+ end
101
108
  else
102
109
  thing
103
110
  end
104
111
  end
105
112
 
106
- # Traverses the hash, unwrapping values and converting keys back to symbols. Returns
107
- # the results as a CandyHash object.
108
- def self.unwrap_hash(hash)
109
- unwrapped = {}
110
- hash.each do |key, value|
111
- case key
112
- when /^'(.+)'$/
113
- unwrapped[$1] = unwrap(value)
114
- when String
115
- unwrapped[key.to_sym] = unwrap(value)
116
- else
117
- unwrapped[unwrap(key)] = unwrap(value)
118
- end
113
+
114
+ # Returns the hash as a Candy::Piece if a class name is embedded, or a CandyHash object otherwise.
115
+ # The 'parent' and 'attribute' parameters should be set by the caller if this is an embedded
116
+ # Candy object.
117
+ def self.unwrap_hash(hash, parent=nil, attribute=nil)
118
+ if class_name = hash.delete(CLASS_KEY.to_s)
119
+ klass = qualified_const_get(class_name)
120
+ else
121
+ klass = CandyHash
119
122
  end
120
- if klass = unwrapped.delete(CLASS_KEY)
121
- qualified_const_get(klass).embed(unwrapped)
123
+
124
+ if parent
125
+ klass.embed(parent, attribute, hash)
126
+ else
127
+ klass.piece(hash)
128
+ end
129
+ end
130
+
131
+ # The inverse of Wrapper.wrap_key -- removes single-quoting from strings and converts other strings
132
+ # to symbols.
133
+ def self.unwrap_key(key)
134
+ if key =~ /^'(.*)'$/
135
+ $1
122
136
  else
123
- CandyHash.embed(unwrapped)
137
+ key.to_sym
124
138
  end
125
139
  end
126
140
 
@@ -42,6 +42,14 @@ describe Candy::CandyArray do
42
42
  that = Zagnut(@this.id)
43
43
  that.bits[3][2][:foo][0].should == :bar
44
44
  end
45
+
46
+ # Github issue #11
47
+ it "can be updated after load" do
48
+ that = Zagnut(@this.id)
49
+ that.bits << 'schadenfreude'
50
+ @this.refresh
51
+ @this.bits[3].should == 'schadenfreude'
52
+ end
45
53
 
46
54
  after(:each) do
47
55
  Zagnut.collection.remove
@@ -235,7 +235,7 @@ describe Candy::Piece do
235
235
  end
236
236
 
237
237
  it "cascades deeply" do
238
- @this.inner.inner = Zagnut.embed(beauty: 'recursive!')
238
+ @this.inner.inner = Zagnut.piece(beauty: 'recursive!')
239
239
  that = Zagnut(@this.id)
240
240
  that.inner.inner.beauty.should == 'recursive!'
241
241
  end
@@ -166,10 +166,16 @@ module Candy
166
166
  obj.rocket[1].should be_an(Object)
167
167
  end
168
168
 
169
+ it "doesn't turn arrays into CandyArrays inside non-Candy objects" do
170
+ obj = Wrapper.unwrap(@wrapped)
171
+ obj.rocket.should_not be_a(CandyArray)
172
+ end
173
+
169
174
  it "traverses a hash and unwraps whatever it needs to" do
170
175
  hash = {"foo" => :bar, "'missile'" => @wrapped}
171
176
  unwrapped = Wrapper.unwrap(hash)
172
177
  unwrapped[:foo].should == :bar
178
+ puts unwrapped
173
179
  unwrapped["missile"].should be_a(Missile)
174
180
  end
175
181
 
@@ -182,9 +188,31 @@ module Candy
182
188
  unwrapped[3].should be_nil
183
189
  unwrapped[4].should == "hi"
184
190
  end
191
+
185
192
 
186
193
  end
187
-
188
194
 
195
+ describe "key names" do
196
+ it "can wrap symbols" do
197
+ Wrapper.wrap_key(:foo).should == 'foo'
198
+ end
199
+
200
+ it "can wrap strings" do
201
+ Wrapper.wrap_key('foo').should == "'foo'"
202
+ end
203
+
204
+ it "refuses to wrap complicated objects" do
205
+ lambda{Wrapper.wrap_key(Object.new)}.should raise_error(TypeError, /Object/)
206
+ end
207
+
208
+ it "can unwrap symbols" do
209
+ Wrapper.unwrap_key('foo').should == :foo
210
+ end
211
+
212
+ it "can unwrap strings" do
213
+ Wrapper.unwrap_key("'foo'").should == 'foo'
214
+ end
215
+ end
216
+
189
217
  end
190
218
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 2
8
- - 7
9
- version: 0.2.7
8
+ - 8
9
+ version: 0.2.8
10
10
  platform: ruby
11
11
  authors:
12
12
  - Stephen Eley
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2010-05-05 00:00:00 -04:00
17
+ date: 2010-05-13 00:00:00 -04:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency