candy 0.2.7 → 0.2.8
Sign up to get free protection for your applications and to get access to all the features.
- 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
|