candy 0.0.2 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +105 -0
- data/VERSION +1 -1
- data/candy.gemspec +8 -4
- data/lib/candy/crunch.rb +13 -1
- data/lib/candy/exceptions.rb +2 -0
- data/lib/candy/qualified_const_get.rb +24 -0
- data/lib/candy/wrapper.rb +123 -0
- data/lib/candy.rb +48 -3
- data/spec/candy/crunch_spec.rb +20 -0
- data/spec/candy/wrapper_spec.rb +197 -0
- data/spec/candy_spec.rb +86 -0
- metadata +8 -4
- data/README.rdoc +0 -17
data/README.markdown
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# Candy
|
2
|
+
|
3
|
+
__"Mongo like candy!"__ — _Blazing Saddles_
|
4
|
+
|
5
|
+
Candy aims to be the simplest possible ORM for the MongoDB database. If MongoMapper is Rails, Candy is Sinatra. Mix the Candy module into any class, and every new object for that class will create a Mongo document. Objects act like OpenStructs -- you can assign or retrieve any property without declaring it in code.
|
6
|
+
|
7
|
+
Other distinctive features:
|
8
|
+
|
9
|
+
* Sane defaults are used for the connection, database, and collection. If you're running Mongo locally you can have zero configuration.
|
10
|
+
* Candy has no `save` or `save!` method. Property changes are persisted to the database immediately. Mongo's atomic update operators (in particular $set) are used so that updates are as fast as possible and don't clobber unrelated fields.
|
11
|
+
* Candy properties have _no memory._ Every value retrieval goes against the database, so your objects are never stale. (You can always implement caching via Ruby attributes if you want it.)
|
12
|
+
* Query result sets are Enumerator objects on top of Mongo cursors. If you're using Ruby 1.9 you'll get very efficient enumeration using fibers.
|
13
|
+
* Whole documents are never written nor read. Queries only return the **_id** field, and getting or setting a property only accesses that property.
|
14
|
+
* __Coming soon:__ Array and embedded document operations.
|
15
|
+
* __Coming soon:__ A smart serializer (Candy::Wrapper) to convert almost any object for assignment to a Candy property.
|
16
|
+
|
17
|
+
Candy was extracted from [Candygram](http://github.com/SFEley/candygram), my delayed job system for MongoDB. I'm presently in the middle of refactoring Candygram to depend on Candy instead, which will simplify a lot of the Mongo internals.
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Come on, you've done this before:
|
22
|
+
|
23
|
+
$ sudo gem install candy
|
24
|
+
|
25
|
+
Candygram requires the **mongo** gem, and you'll probably be much happier if you install the **mongo\_ext** gem as well. The author uses only Ruby 1.9, but it _should_ work in Ruby 1.8.7. If it doesn't, please report a bug in Github's issue tracking system. (If you're using 1.8.6, I hosed you by using the Enumerator class. Sorry. I might fix this if enough noise gets made.)
|
26
|
+
|
27
|
+
## Configuration
|
28
|
+
|
29
|
+
The simplest possible thing that works:
|
30
|
+
|
31
|
+
class Zagnut
|
32
|
+
include Candy
|
33
|
+
end
|
34
|
+
|
35
|
+
That's it. Honest. Some Mongo plumbing is hooked in and instantiated the first time the `.collection` attribute is accessed:
|
36
|
+
|
37
|
+
Zagnut.connection # => Defaults to localhost port 27017
|
38
|
+
Zagnut.db # => Defaults to your username
|
39
|
+
Zagnut.collection # => Defaults to the class name ('Zagnut')
|
40
|
+
|
41
|
+
You can override the DB or collection by providing new name strings or Mongo::DB and Mongo::Collection objects. Or you can set certain global variables to make it easier for multiple Candy classes in an application to use the same database:
|
42
|
+
|
43
|
+
* **$MONGO_HOST**
|
44
|
+
* **$MONGO_PORT**
|
45
|
+
* **$MONGO_OPTIONS** (A hash of options to the Connection object)
|
46
|
+
* **$MONGO_DB** (A simple string with the database name)
|
47
|
+
|
48
|
+
All of the above is pretty general-purpose. If you want to use this class-based Mongo functionality in your own projects, simply include `Candy::Crunch` in your own classes.
|
49
|
+
|
50
|
+
## Using It
|
51
|
+
|
52
|
+
The trick here is to think of Candy objects like OpenStructs. Or if that's too technical, imagine the objects as thin candy shells around a chewy `method_missing` center:
|
53
|
+
|
54
|
+
class Zagnut
|
55
|
+
include Candy
|
56
|
+
end
|
57
|
+
|
58
|
+
zag = Zagnut.new # A blank document enters the Zagnut collection
|
59
|
+
zag.taste = "Chewy!" # Properties are created and saved as they're used
|
60
|
+
zag.calories = 600
|
61
|
+
|
62
|
+
nut = Zagnut.first(:taste => "Chewy!")
|
63
|
+
nut.calories # => 600
|
64
|
+
|
65
|
+
kingsize = Zagnut.new
|
66
|
+
kingsize.calories = 900
|
67
|
+
|
68
|
+
bars = Zagnut.all # => An Enumerator object with #each and friends
|
69
|
+
sum = bars.inject {|sum,bar| sum + bar.calories} # => 1500
|
70
|
+
|
71
|
+
|
72
|
+
If, in the middle of that code execution, somebody else changed the properties of one of the objects, you might get different answers. Every property access requeries the Mongo document. That sounds insane, but Mongo is _fast_ so we can get away with it; and it avoids any brittleness or complexity of having refresh methods or checking for stale data. (Later versions may include more document-based access via block operations. Let me know if you would like to see that.)
|
73
|
+
|
74
|
+
### Method_missing? _Really?_
|
75
|
+
|
76
|
+
Yes. It may seem at first like an inversion: Candy only stores attributes that you _don't_ declare in your class definition. But there's a method to this madn... (No, wait, it's missing.)
|
77
|
+
|
78
|
+
Here's the reason. I have no idea what kind of logic you might want to put in your classes. I don't want to guess what you want to store or not -- and more to the point, I don't want to make _you_ guess. Unless you want to.
|
79
|
+
|
80
|
+
Candy properties are dumb. They don't have calculations. They don't cache. They have nothing to do with instance variables. If you _want_ to make something smarter, just set up your accessors and have them talk to Candy behind the scenes:
|
81
|
+
|
82
|
+
class Zagnut
|
83
|
+
include Candy
|
84
|
+
|
85
|
+
def weight
|
86
|
+
@weight ||= _weight # _weight is undeclared, so Candy looks it up
|
87
|
+
end
|
88
|
+
|
89
|
+
def weight=(val)
|
90
|
+
self._weight = @weight = val # _weight= is undeclared, so Candy stores it
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
== Contributing
|
96
|
+
|
97
|
+
At this early stage, one of the best things you could do is just to tell me that you have an interest in using this thing. You can email me at sfeley@gmail.com -- if I get more than, say, three votes of interest, I'll throw a projects page on the wiki.
|
98
|
+
|
99
|
+
Beyond that, report issues, please. If you want to fork it and add features, fabulous. Send me a pull request.
|
100
|
+
|
101
|
+
Oh, and if you like science fiction stories, check out my podcast [Escape Pod](http://escapepod.org). End of plug.
|
102
|
+
|
103
|
+
== Copyright
|
104
|
+
|
105
|
+
Copyright (c) 2010 Stephen Eley. See LICENSE for details.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
0.1.0
|
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.0
|
8
|
+
s.version = "0.1.0"
|
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-
|
12
|
+
s.date = %q{2010-02-16}
|
13
13
|
s.description = %q{Candy is a lightweight ORM for the MongoDB database. If MongoMapper is Rails, Candy is Sinatra.
|
14
14
|
It provides a module you mix into any class, enabling the class to connect to Mongo on its own
|
15
15
|
and push its objects into a collection. Candied objects act like OpenStructs, allowing attributes
|
@@ -20,20 +20,23 @@ converts almost any object for assignment to any attribute.
|
|
20
20
|
s.email = %q{sfeley@gmail.com}
|
21
21
|
s.extra_rdoc_files = [
|
22
22
|
"LICENSE",
|
23
|
-
"README.
|
23
|
+
"README.markdown"
|
24
24
|
]
|
25
25
|
s.files = [
|
26
26
|
".document",
|
27
27
|
".gitignore",
|
28
28
|
"LICENSE",
|
29
|
-
"README.
|
29
|
+
"README.markdown",
|
30
30
|
"Rakefile",
|
31
31
|
"VERSION",
|
32
32
|
"candy.gemspec",
|
33
33
|
"lib/candy.rb",
|
34
34
|
"lib/candy/crunch.rb",
|
35
35
|
"lib/candy/exceptions.rb",
|
36
|
+
"lib/candy/qualified_const_get.rb",
|
37
|
+
"lib/candy/wrapper.rb",
|
36
38
|
"spec/candy/crunch_spec.rb",
|
39
|
+
"spec/candy/wrapper_spec.rb",
|
37
40
|
"spec/candy_spec.rb",
|
38
41
|
"spec/spec.opts",
|
39
42
|
"spec/spec.watchr",
|
@@ -46,6 +49,7 @@ converts almost any object for assignment to any attribute.
|
|
46
49
|
s.summary = %q{The simplest MongoDB ORM}
|
47
50
|
s.test_files = [
|
48
51
|
"spec/candy/crunch_spec.rb",
|
52
|
+
"spec/candy/wrapper_spec.rb",
|
49
53
|
"spec/candy_spec.rb",
|
50
54
|
"spec/spec_helper.rb"
|
51
55
|
]
|
data/lib/candy/crunch.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
require 'mongo'
|
2
|
-
require 'etc'
|
2
|
+
require 'etc' # To get the current username for database default
|
3
3
|
|
4
4
|
module Candy
|
5
5
|
|
@@ -95,6 +95,18 @@ module Candy
|
|
95
95
|
@collection ||= db.create_collection(name)
|
96
96
|
end
|
97
97
|
|
98
|
+
# Creates an index on the specified property, with an optional direction specified as either :asc or :desc.
|
99
|
+
# (Note that this is deliberately a very simple method. If you want multi-key or unique indexes, just call
|
100
|
+
# #create_index directly on the collection.)
|
101
|
+
def index(property, direction=:asc)
|
102
|
+
case direction
|
103
|
+
when :asc then mongo_direction = Mongo::ASCENDING
|
104
|
+
when :desc then mongo_direction = Mongo::DESCENDING
|
105
|
+
else
|
106
|
+
raise TypeError, "Index direction should be :asc or :desc"
|
107
|
+
end
|
108
|
+
collection.create_index(property => mongo_direction)
|
109
|
+
end
|
98
110
|
end
|
99
111
|
|
100
112
|
module InstanceMethods
|
data/lib/candy/exceptions.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
# From: http://redcorundum.blogspot.com/2006/05/kernelqualifiedconstget.html
|
2
|
+
# We need this to properly handle namespaced class resolution in Candy::Wrapper.unwrap.
|
3
|
+
|
4
|
+
module Kernel
|
5
|
+
def qualified_const_get(str)
|
6
|
+
path = str.to_s.split('::')
|
7
|
+
from_root = path[0].empty?
|
8
|
+
if from_root
|
9
|
+
from_root = []
|
10
|
+
path = path[1..-1]
|
11
|
+
else
|
12
|
+
start_ns = ((Class === self)||(Module === self)) ? self : self.class
|
13
|
+
from_root = start_ns.to_s.split('::')
|
14
|
+
end
|
15
|
+
until from_root.empty?
|
16
|
+
begin
|
17
|
+
return (from_root+path).inject(Object) { |ns,name| ns.const_get(name) }
|
18
|
+
rescue NameError
|
19
|
+
from_root.delete_at(-1)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
path.inject(Object) { |ns,name| ns.const_get(name) }
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'mongo'
|
2
|
+
require 'date' # Only so we know what one is. Argh.
|
3
|
+
require 'candy/qualified_const_get'
|
4
|
+
|
5
|
+
module Candy
|
6
|
+
|
7
|
+
# Utility methods to serialize and unserialize many types of objects into BSON.
|
8
|
+
module Wrapper
|
9
|
+
|
10
|
+
BSON_SAFE = [String,
|
11
|
+
NilClass,
|
12
|
+
TrueClass,
|
13
|
+
FalseClass,
|
14
|
+
Fixnum,
|
15
|
+
Float,
|
16
|
+
Time,
|
17
|
+
Regexp,
|
18
|
+
ByteBuffer,
|
19
|
+
Mongo::ObjectID,
|
20
|
+
Mongo::Code,
|
21
|
+
Mongo::DBRef]
|
22
|
+
|
23
|
+
# Makes an object safe for the sharp pointy edges of MongoDB. Types properly serialized
|
24
|
+
# by the BSON.serialize call get passed through unmolested; others are unpacked and their
|
25
|
+
# pieces individually shrink-wrapped.
|
26
|
+
def self.wrap(thing)
|
27
|
+
# Pass the simple cases through
|
28
|
+
return thing if BSON_SAFE.include?(thing.class)
|
29
|
+
case thing
|
30
|
+
when Symbol
|
31
|
+
wrap_symbol(thing)
|
32
|
+
when Array
|
33
|
+
wrap_array(thing)
|
34
|
+
when Hash
|
35
|
+
wrap_hash(thing)
|
36
|
+
when Numeric # The most obvious are in BSON_SAFE, but not all
|
37
|
+
thing
|
38
|
+
when Date
|
39
|
+
thing.to_time
|
40
|
+
# Problem children
|
41
|
+
when Proc
|
42
|
+
raise TypeError, "Candy can't wrap Proc objects!"
|
43
|
+
when Range
|
44
|
+
raise TypeError, "Candy can't wrap ranges!"
|
45
|
+
else
|
46
|
+
wrap_object(thing) # Our catchall machinery
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Takes an array and returns the same array with unsafe objects wrapped
|
51
|
+
def self.wrap_array(array)
|
52
|
+
array.map {|element| wrap(element)}
|
53
|
+
end
|
54
|
+
|
55
|
+
# Takes a hash and returns it with both keys and values wrapped
|
56
|
+
def self.wrap_hash(hash)
|
57
|
+
wrapped = {}
|
58
|
+
hash.each {|k, v| wrapped[wrap(k)] = wrap(v)}
|
59
|
+
wrapped
|
60
|
+
end
|
61
|
+
|
62
|
+
# Returns a string that's distinctive enough for us to unwrap later and produce the same symbol
|
63
|
+
def self.wrap_symbol(symbol)
|
64
|
+
"__sym_" + symbol.to_s
|
65
|
+
end
|
66
|
+
|
67
|
+
# Returns a nested hash containing the class and instance variables of the object. It's not the
|
68
|
+
# deepest we could ever go (it doesn't handle singleton methods, etc.) but it's a start.
|
69
|
+
def self.wrap_object(object)
|
70
|
+
wrapped = {"class" => object.class.name}
|
71
|
+
ivars = {}
|
72
|
+
object.instance_variables.each do |ivar|
|
73
|
+
# Different Ruby versions spit different things out for instance_variables. Annoying.
|
74
|
+
ivar_name = '@' + ivar.to_s.sub(/^@/,'')
|
75
|
+
ivars[ivar_name] = wrap(object.instance_variable_get(ivar_name))
|
76
|
+
end
|
77
|
+
wrapped["ivars"] = ivars unless ivars.empty?
|
78
|
+
{"__object_" => wrapped}
|
79
|
+
end
|
80
|
+
|
81
|
+
# Undoes any complicated magic from the Wrapper.wrap method. Almost everything falls through
|
82
|
+
# untouched except for symbol strings and hashed objects.
|
83
|
+
def self.unwrap(thing)
|
84
|
+
case thing
|
85
|
+
when Hash
|
86
|
+
if thing["__object_"]
|
87
|
+
unwrap_object(thing)
|
88
|
+
else
|
89
|
+
unwrap_hash(thing)
|
90
|
+
end
|
91
|
+
when Array
|
92
|
+
thing.map {|element| unwrap(element)}
|
93
|
+
when /^__sym_(.+)/
|
94
|
+
$1.to_sym
|
95
|
+
else
|
96
|
+
thing
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Traverses the hash, unwrapping both keys and values. Returns the hash that results.
|
101
|
+
def self.unwrap_hash(hash)
|
102
|
+
unwrapped = {}
|
103
|
+
hash.each {|k,v| unwrapped[unwrap(k)] = unwrap(v)}
|
104
|
+
unwrapped
|
105
|
+
end
|
106
|
+
|
107
|
+
# Turns a hashed object back into an object of the stated class, setting any captured instance
|
108
|
+
# variables. The main limitation is that the object's class *must* respond to Class.new without
|
109
|
+
# any parameters; we will not attempt to guess at any complex initialization behavior.
|
110
|
+
def self.unwrap_object(hash)
|
111
|
+
if innards = hash["__object_"]
|
112
|
+
klass = Kernel.qualified_const_get(innards["class"])
|
113
|
+
object = klass.new
|
114
|
+
if innards["ivars"]
|
115
|
+
innards["ivars"].each do |name, value|
|
116
|
+
object.instance_variable_set(name, unwrap(value))
|
117
|
+
end
|
118
|
+
end
|
119
|
+
object
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/lib/candy.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'candy/exceptions'
|
2
2
|
require 'candy/crunch'
|
3
|
+
require 'candy/wrapper'
|
3
4
|
|
4
5
|
# Mix me into your classes and Mongo will like them!
|
5
6
|
module Candy
|
@@ -7,6 +8,8 @@ module Candy
|
|
7
8
|
module ClassMethods
|
8
9
|
include Crunch::ClassMethods
|
9
10
|
|
11
|
+
attr_reader :stamp_create, :stamp_update
|
12
|
+
|
10
13
|
# Retrieves an object from Mongo by its ID and returns it. Returns nil if the ID isn't found in Mongo.
|
11
14
|
def find(id)
|
12
15
|
if collection.find_one(id)
|
@@ -37,6 +40,14 @@ module Candy
|
|
37
40
|
end
|
38
41
|
end
|
39
42
|
|
43
|
+
# Configures objects to set `created_at` and `updated_at` properties at the appropriate times.
|
44
|
+
# Pass `:create` or `:update` to limit it to just one or the other. Defaults to both.
|
45
|
+
def timestamp(*args)
|
46
|
+
args = [:create, :update] if args.empty?
|
47
|
+
@stamp_create = args.include?(:create)
|
48
|
+
@stamp_update = args.include?(:update)
|
49
|
+
end
|
50
|
+
|
40
51
|
private
|
41
52
|
# Returns a hash of options matching those enabled in Mongo::Collection#find, if any of them exist
|
42
53
|
# in the set of search conditions.
|
@@ -55,7 +66,8 @@ module Candy
|
|
55
66
|
|
56
67
|
# We push ourselves into the DB before going on with our day.
|
57
68
|
def initialize(*args, &block)
|
58
|
-
@__candy = check_for_candy(args) ||
|
69
|
+
@__candy = check_for_candy(args) ||
|
70
|
+
self.class.collection.insert(self.class.stamp_create ? {:created_at => Time.now.utc} : {})
|
59
71
|
super
|
60
72
|
end
|
61
73
|
|
@@ -68,12 +80,40 @@ module Candy
|
|
68
80
|
# Retrieving any unknown attribute will return that value from this record in the Mongo collection.
|
69
81
|
def method_missing(name, *args, &block)
|
70
82
|
if name =~ /(.*)=$/ # We're assigning
|
71
|
-
|
83
|
+
set $1, Wrapper.wrap(args[0])
|
84
|
+
else
|
85
|
+
Wrapper.unwrap(self.class.collection.find_one(@__candy, :fields => [name.to_s])[name.to_s])
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Given either a property/value pair or a hash (which can contain several property/value pairs), sets those
|
90
|
+
# values in Mongo using the atomic $set. The first form is functionally equivalent to simply using the
|
91
|
+
# magic assignment operator; i.e., `me.set(:foo, 'bar')` is the same as `me.foo = bar`.
|
92
|
+
def set(*args)
|
93
|
+
if args.length > 1 # This is the property/value form
|
94
|
+
hash = {args[0] => args[1]}
|
72
95
|
else
|
73
|
-
|
96
|
+
hash = args[0]
|
74
97
|
end
|
98
|
+
hash.merge!(:updated_at => Time.now.utc) if self.class.stamp_update
|
99
|
+
update '$set' => hash
|
75
100
|
end
|
76
101
|
|
102
|
+
# Given a Candy array property, appends a value or values to the end of that array using the atomic $push.
|
103
|
+
# (Note that we don't actually check the property to make sure it's an array and $push is valid. If it isn't,
|
104
|
+
# this operation will silently fail.)
|
105
|
+
def push(property, *values)
|
106
|
+
values.each do |value|
|
107
|
+
update '$push' => {property => Wrapper.wrap(value)}
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
# Given a Candy integer property, increments it by the given value (which defaults to 1) using the atomic $inc.
|
112
|
+
# (Note that we don't actually check the property to make sure it's an integer and $inc is valid. If it isn't,
|
113
|
+
# this operation will silently fail.)
|
114
|
+
def inc(property, increment=1)
|
115
|
+
update '$inc' => {property => increment}
|
116
|
+
end
|
77
117
|
private
|
78
118
|
|
79
119
|
# Returns the secret decoder ring buried in the arguments to "new"
|
@@ -86,6 +126,11 @@ module Candy
|
|
86
126
|
end
|
87
127
|
end
|
88
128
|
|
129
|
+
# Updates the Mongo document with the given element or elements.
|
130
|
+
def update(element)
|
131
|
+
self.class.collection.update({"_id" => @__candy}, element)
|
132
|
+
end
|
133
|
+
|
89
134
|
end
|
90
135
|
|
91
136
|
def self.included(receiver)
|
data/spec/candy/crunch_spec.rb
CHANGED
@@ -144,6 +144,26 @@ describe Candy::Crunch do
|
|
144
144
|
|
145
145
|
end
|
146
146
|
|
147
|
+
describe "index" do
|
148
|
+
it "can be created with just a property name" do
|
149
|
+
PeanutBrittle.index(:blah)
|
150
|
+
PeanutBrittle.collection.index_information.values[1].should == [["blah", Mongo::ASCENDING]]
|
151
|
+
end
|
152
|
+
|
153
|
+
it "can be created with a direction" do
|
154
|
+
PeanutBrittle.index(:fwah, :desc)
|
155
|
+
PeanutBrittle.collection.index_information.values[1].should == [["fwah", Mongo::DESCENDING]]
|
156
|
+
end
|
157
|
+
|
158
|
+
it "throws an exception if you give it a type other than :asc or :desc" do
|
159
|
+
lambda{PeanutBrittle.index(:yah, 5)}.should raise_error(Candy::TypeError, "Index direction should be :asc or :desc")
|
160
|
+
end
|
161
|
+
|
162
|
+
after(:each) do
|
163
|
+
PeanutBrittle.collection.drop_indexes
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
147
167
|
after(:each) do
|
148
168
|
PeanutBrittle.connection = nil
|
149
169
|
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
|
2
|
+
|
3
|
+
module Candy
|
4
|
+
describe Candy::Wrapper do
|
5
|
+
|
6
|
+
# A simple class to test argument encoding
|
7
|
+
class Missile
|
8
|
+
attr_accessor :payload
|
9
|
+
attr_accessor :rocket
|
10
|
+
|
11
|
+
def explode
|
12
|
+
"Dropped the #{payload}."
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "wrapping" do
|
17
|
+
|
18
|
+
it "can wrap an array of simple arguments" do
|
19
|
+
a = ["Hi", 1, nil, 17.536]
|
20
|
+
Wrapper.wrap_array(a).should == a
|
21
|
+
end
|
22
|
+
|
23
|
+
it "can wrap a string" do
|
24
|
+
Wrapper.wrap("Hi").should == "Hi"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "can wrap nil" do
|
28
|
+
Wrapper.wrap(nil).should == nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it "can wrap true" do
|
32
|
+
Wrapper.wrap(true).should be_true
|
33
|
+
end
|
34
|
+
|
35
|
+
it "can wrap false" do
|
36
|
+
Wrapper.wrap(false).should be_false
|
37
|
+
end
|
38
|
+
|
39
|
+
it "can wrap an integer" do
|
40
|
+
Wrapper.wrap(5).should == 5
|
41
|
+
end
|
42
|
+
|
43
|
+
it "can wrap a float" do
|
44
|
+
Wrapper.wrap(17.950).should == 17.950
|
45
|
+
end
|
46
|
+
|
47
|
+
it "can wrap an already serialized bytestream" do
|
48
|
+
b = BSON.serialize(:foo => 'bar')
|
49
|
+
Wrapper.wrap(b).should == b
|
50
|
+
end
|
51
|
+
|
52
|
+
it "can wrap an ObjectID" do
|
53
|
+
i = Mongo::ObjectID.new
|
54
|
+
Wrapper.wrap(i).should == i
|
55
|
+
end
|
56
|
+
|
57
|
+
it "can wrap the time" do
|
58
|
+
t = Time.now
|
59
|
+
Wrapper.wrap(t).should == t
|
60
|
+
end
|
61
|
+
|
62
|
+
it "can wrap a regular expression" do
|
63
|
+
r = /ha(l+)eluja(h?)/i
|
64
|
+
Wrapper.wrap(r).should == r
|
65
|
+
end
|
66
|
+
|
67
|
+
it "can wrap a Mongo code object (if we ever need to)" do
|
68
|
+
c = Mongo::Code.new('5')
|
69
|
+
Wrapper.wrap(c).should == c
|
70
|
+
end
|
71
|
+
|
72
|
+
it "can wrap a Mongo DBRef (if we ever need to)" do
|
73
|
+
d = Mongo::DBRef.new('foo', Mongo::ObjectID.new)
|
74
|
+
Wrapper.wrap(d).should == d
|
75
|
+
end
|
76
|
+
|
77
|
+
it "can wrap a date as a time" do
|
78
|
+
d = Date.today
|
79
|
+
Wrapper.wrap(d).should == Date.today.to_time
|
80
|
+
end
|
81
|
+
|
82
|
+
it "can wrap other numeric types (which might throw exceptions later but oh well)" do
|
83
|
+
c = Complex(2, 5)
|
84
|
+
Wrapper.wrap(c).should == c
|
85
|
+
end
|
86
|
+
|
87
|
+
it "can wrap a symbol in a way that preserves its symbolic nature" do
|
88
|
+
Wrapper.wrap(:oldglory).should == "__sym_oldglory"
|
89
|
+
end
|
90
|
+
|
91
|
+
it "wraps an array recursively" do
|
92
|
+
a = [5, 'hi', [':symbol', 0], nil]
|
93
|
+
Wrapper.wrap(a).should == a
|
94
|
+
end
|
95
|
+
|
96
|
+
it "wraps a hash's keys" do
|
97
|
+
h = {"foo" => "bar", :yoo => "yar"}
|
98
|
+
Wrapper.wrap(h).keys.should == ["foo", "__sym_yoo"]
|
99
|
+
end
|
100
|
+
|
101
|
+
it "wraps a hash's values" do
|
102
|
+
h = {:foo => :bar, :yoo => [:yar, 5]}
|
103
|
+
Wrapper.wrap(h).values.should == ["__sym_bar", ["__sym_yar", 5]]
|
104
|
+
end
|
105
|
+
|
106
|
+
it "rejects procs" do
|
107
|
+
p = Proc.new {puts "This should fail!"}
|
108
|
+
lambda{Wrapper.wrap(p)}.should raise_error(TypeError)
|
109
|
+
end
|
110
|
+
|
111
|
+
it "rejects ranges" do
|
112
|
+
r = (1..3)
|
113
|
+
lambda{Wrapper.wrap(r)}.should raise_error(TypeError)
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "objects" do
|
117
|
+
before(:each) do
|
118
|
+
@missile = Missile.new
|
119
|
+
@missile.payload = "15 megatons"
|
120
|
+
@missile.rocket = [2, Object.new]
|
121
|
+
@this = Wrapper.wrap(@missile)
|
122
|
+
end
|
123
|
+
|
124
|
+
it "returns a hash" do
|
125
|
+
@this.should be_a(Hash)
|
126
|
+
end
|
127
|
+
|
128
|
+
it "keys the hash to be an object" do
|
129
|
+
@this.keys.should == ["__object_"]
|
130
|
+
end
|
131
|
+
|
132
|
+
it "knows the object's class" do
|
133
|
+
@this["__object_"]["class"].should =~ /Missile$/
|
134
|
+
end
|
135
|
+
|
136
|
+
it "captures all the instance variables" do
|
137
|
+
ivars = @this["__object_"]["ivars"]
|
138
|
+
ivars.should have(2).elements
|
139
|
+
ivars["@payload"].should == "15 megatons"
|
140
|
+
ivars["@rocket"][1]["__object_"]["class"].should == "Object"
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "unwrapping" do
|
149
|
+
before(:each) do
|
150
|
+
@wrapped = {"__object_" => {
|
151
|
+
"class" => Missile.name,
|
152
|
+
"ivars" => {
|
153
|
+
"@payload" => "6 kilotons",
|
154
|
+
"@rocket" => [1, {"__object_" => {
|
155
|
+
"class" => "Object"
|
156
|
+
}}]
|
157
|
+
}
|
158
|
+
}}
|
159
|
+
end
|
160
|
+
it "passes most things through untouched" do
|
161
|
+
Wrapper.unwrap(5).should == 5
|
162
|
+
end
|
163
|
+
|
164
|
+
it "turns symbolized strings back into symbols" do
|
165
|
+
Wrapper.unwrap("__sym_blah").should == :blah
|
166
|
+
end
|
167
|
+
|
168
|
+
it "turns hashed objects back into objects" do
|
169
|
+
obj = Wrapper.unwrap(@wrapped)
|
170
|
+
obj.should be_a(Missile)
|
171
|
+
obj.payload.should == "6 kilotons"
|
172
|
+
obj.rocket[0].should == 1
|
173
|
+
obj.rocket[1].should be_an(Object)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "traverses a hash and unwraps whatever it needs to" do
|
177
|
+
hash = {"__sym_foo" => "__sym_bar", "missile" => @wrapped}
|
178
|
+
unwrapped = Wrapper.unwrap(hash)
|
179
|
+
unwrapped[:foo].should == :bar
|
180
|
+
unwrapped["missile"].should be_a(Missile)
|
181
|
+
end
|
182
|
+
|
183
|
+
it "traverses an array and unwraps whatever it needs to" do
|
184
|
+
array = ["__sym_foo", 5, @wrapped, nil, "hi"]
|
185
|
+
unwrapped = Wrapper.unwrap(array)
|
186
|
+
unwrapped[0].should == :foo
|
187
|
+
unwrapped[1].should == 5
|
188
|
+
unwrapped[2].should be_a(Missile)
|
189
|
+
unwrapped[3].should be_nil
|
190
|
+
unwrapped[4].should == "hi"
|
191
|
+
end
|
192
|
+
|
193
|
+
end
|
194
|
+
|
195
|
+
|
196
|
+
end
|
197
|
+
end
|
data/spec/candy_spec.rb
CHANGED
@@ -44,6 +44,29 @@ describe "Candy" do
|
|
44
44
|
@this.licks.should == 7
|
45
45
|
end
|
46
46
|
|
47
|
+
it "can set properties explicity" do
|
48
|
+
@this.set(:licks, 17)
|
49
|
+
@this.licks.should == 17
|
50
|
+
end
|
51
|
+
|
52
|
+
it "can set properties from a hash" do
|
53
|
+
@this.set(:licks => 19, :center => -2.5)
|
54
|
+
@this.licks.should == 19
|
55
|
+
@this.center.should == -2.5
|
56
|
+
end
|
57
|
+
|
58
|
+
it "wraps objects" do
|
59
|
+
o = Object.new
|
60
|
+
@this.object = o
|
61
|
+
@verifier.find_one["object"]["__object_"]["class"].should == "Object"
|
62
|
+
end
|
63
|
+
|
64
|
+
it "unwraps objects" do
|
65
|
+
@verifier.update({:_id => @this.id}, {:center => {"__object_" => {:class => "Object", :ivars => {"@foo" => "bar"}}}})
|
66
|
+
@this.center.should be_an(Object)
|
67
|
+
@this.center.instance_variable_get(:@foo).should == "bar"
|
68
|
+
end
|
69
|
+
|
47
70
|
describe "retrieval" do
|
48
71
|
it "can find a record by its ID" do
|
49
72
|
@this.licks = 10
|
@@ -110,6 +133,69 @@ describe "Candy" do
|
|
110
133
|
|
111
134
|
end
|
112
135
|
|
136
|
+
describe "arrays" do
|
137
|
+
it "can push items" do
|
138
|
+
@this.push(:colors, 'red')
|
139
|
+
@this.colors.should == ['red']
|
140
|
+
end
|
141
|
+
|
142
|
+
it "can push an array of items" do
|
143
|
+
@this.push(:potpourri, 'red', 75, nil)
|
144
|
+
@this.potpourri.should == ['red', 75, nil]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe "numbers" do
|
149
|
+
it "can be incremented by 1 when not set" do
|
150
|
+
@this.inc(:bites)
|
151
|
+
@this.bites.should == 1
|
152
|
+
end
|
153
|
+
|
154
|
+
it "can be incremented by 1 when set" do
|
155
|
+
@this.bites = 11
|
156
|
+
@this.inc(:bites)
|
157
|
+
@this.bites.should == 12
|
158
|
+
end
|
159
|
+
|
160
|
+
it "can be incremented by any number" do
|
161
|
+
@this.bites = -6
|
162
|
+
@this.inc(:bites, 15)
|
163
|
+
@this.bites.should == 9
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "timestamp" do
|
168
|
+
it "can be set on creation" do
|
169
|
+
Zagnut.class_eval("timestamp :create")
|
170
|
+
z = Zagnut.new
|
171
|
+
z.created_at.should be_a(Time)
|
172
|
+
z.updated_at.should be_nil
|
173
|
+
end
|
174
|
+
|
175
|
+
it "can be set on modification" do
|
176
|
+
Zagnut.class_eval("timestamp :update")
|
177
|
+
z = Zagnut.new
|
178
|
+
z.created_at.should be_nil
|
179
|
+
z.updated_at.should be_nil
|
180
|
+
z.bites = 11
|
181
|
+
z.created_at.should be_nil
|
182
|
+
z.updated_at.should be_a(Time)
|
183
|
+
end
|
184
|
+
|
185
|
+
it "sets both by default" do
|
186
|
+
Zagnut.class_eval("timestamp")
|
187
|
+
z = Zagnut.new
|
188
|
+
z.bites = 11
|
189
|
+
z.created_at.should be_a(Time)
|
190
|
+
z.updated_at.should be_a(Time)
|
191
|
+
end
|
192
|
+
|
193
|
+
|
194
|
+
after(:each) do
|
195
|
+
Zagnut.class_eval("timestamp nil")
|
196
|
+
end
|
197
|
+
|
198
|
+
end
|
113
199
|
|
114
200
|
after(:each) do
|
115
201
|
Zagnut.collection.remove
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: candy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Stephen Eley
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2010-
|
12
|
+
date: 2010-02-16 00:00:00 -05:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -67,19 +67,22 @@ extensions: []
|
|
67
67
|
|
68
68
|
extra_rdoc_files:
|
69
69
|
- LICENSE
|
70
|
-
- README.
|
70
|
+
- README.markdown
|
71
71
|
files:
|
72
72
|
- .document
|
73
73
|
- .gitignore
|
74
74
|
- LICENSE
|
75
|
-
- README.
|
75
|
+
- README.markdown
|
76
76
|
- Rakefile
|
77
77
|
- VERSION
|
78
78
|
- candy.gemspec
|
79
79
|
- lib/candy.rb
|
80
80
|
- lib/candy/crunch.rb
|
81
81
|
- lib/candy/exceptions.rb
|
82
|
+
- lib/candy/qualified_const_get.rb
|
83
|
+
- lib/candy/wrapper.rb
|
82
84
|
- spec/candy/crunch_spec.rb
|
85
|
+
- spec/candy/wrapper_spec.rb
|
83
86
|
- spec/candy_spec.rb
|
84
87
|
- spec/spec.opts
|
85
88
|
- spec/spec.watchr
|
@@ -114,5 +117,6 @@ specification_version: 3
|
|
114
117
|
summary: The simplest MongoDB ORM
|
115
118
|
test_files:
|
116
119
|
- spec/candy/crunch_spec.rb
|
120
|
+
- spec/candy/wrapper_spec.rb
|
117
121
|
- spec/candy_spec.rb
|
118
122
|
- spec/spec_helper.rb
|
data/README.rdoc
DELETED
@@ -1,17 +0,0 @@
|
|
1
|
-
= candy
|
2
|
-
|
3
|
-
Description goes here.
|
4
|
-
|
5
|
-
== Note on Patches/Pull Requests
|
6
|
-
|
7
|
-
* Fork the project.
|
8
|
-
* Make your feature addition or bug fix.
|
9
|
-
* Add tests for it. This is important so I don't break it in a
|
10
|
-
future version unintentionally.
|
11
|
-
* Commit, do not mess with rakefile, version, or history.
|
12
|
-
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
13
|
-
* Send me a pull request. Bonus points for topic branches.
|
14
|
-
|
15
|
-
== Copyright
|
16
|
-
|
17
|
-
Copyright (c) 2010 Stephen Eley. See LICENSE for details.
|