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 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.2
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.2"
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-01-20}
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.rdoc"
23
+ "README.markdown"
24
24
  ]
25
25
  s.files = [
26
26
  ".document",
27
27
  ".gitignore",
28
28
  "LICENSE",
29
- "README.rdoc",
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
@@ -4,4 +4,6 @@ module Candy
4
4
  class CandyError < StandardError; end
5
5
 
6
6
  class ConnectionError < CandyError; end
7
+
8
+ class TypeError < CandyError; end
7
9
  end
@@ -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) || self.class.collection.insert({})
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
- self.class.collection.update({"_id" => @__candy}, {"$set" => {$1 => args[0]}})
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
- self.class.collection.find_one(@__candy, :fields => [name.to_s])[name.to_s]
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)
@@ -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.2
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-01-20 00:00:00 -05:00
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.rdoc
70
+ - README.markdown
71
71
  files:
72
72
  - .document
73
73
  - .gitignore
74
74
  - LICENSE
75
- - README.rdoc
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.