candy 0.2.7 → 0.2.8
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.
- data/HISTORY.markdown +8 -0
- data/VERSION +1 -1
- data/candy.gemspec +2 -2
- data/lib/candy/array.rb +21 -10
- data/lib/candy/embeddable.rb +11 -7
- data/lib/candy/piece.rb +36 -24
- data/lib/candy/wrapper.rb +49 -35
- data/spec/candy/array_spec.rb +8 -0
- data/spec/candy/piece_spec.rb +1 -1
- data/spec/candy/wrapper_spec.rb +29 -1
- metadata +3 -3
data/HISTORY.markdown
CHANGED
@@ -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.
|
1
|
+
0.2.8
|
data/candy.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{candy}
|
8
|
-
s.version = "0.2.
|
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-
|
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
|
data/lib/candy/array.rb
CHANGED
@@ -10,23 +10,23 @@ module Candy
|
|
10
10
|
include Crunch
|
11
11
|
include Embeddable
|
12
12
|
|
13
|
-
#
|
14
|
-
|
15
|
-
|
16
|
-
|
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
|
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(
|
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.
|
51
|
-
@__candy = doc[
|
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
|
data/lib/candy/embeddable.rb
CHANGED
@@ -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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
32
|
-
piece
|
36
|
+
end
|
33
37
|
end
|
34
38
|
|
35
39
|
end
|
data/lib/candy/piece.rb
CHANGED
@@ -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
|
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
|
76
|
-
#
|
77
|
-
# otherwise we assume we're a new document and
|
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(
|
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
|
-
|
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
|
-
#
|
173
|
-
#
|
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
|
-
|
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
|
|
data/lib/candy/wrapper.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
88
|
-
# untouched except for
|
89
|
-
|
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
|
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
|
-
|
99
|
-
|
100
|
-
|
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
|
-
|
107
|
-
# the
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
-
|
121
|
-
|
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
|
-
|
137
|
+
key.to_sym
|
124
138
|
end
|
125
139
|
end
|
126
140
|
|
data/spec/candy/array_spec.rb
CHANGED
@@ -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
|
data/spec/candy/piece_spec.rb
CHANGED
@@ -235,7 +235,7 @@ describe Candy::Piece do
|
|
235
235
|
end
|
236
236
|
|
237
237
|
it "cascades deeply" do
|
238
|
-
@this.inner.inner = Zagnut.
|
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
|
data/spec/candy/wrapper_spec.rb
CHANGED
@@ -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
|
-
-
|
9
|
-
version: 0.2.
|
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-
|
17
|
+
date: 2010-05-13 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|