candy 0.1.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -5,22 +5,20 @@ begin
5
5
  require 'jeweler'
6
6
  Jeweler::Tasks.new do |gem|
7
7
  gem.name = "candy"
8
- gem.summary = %Q{The simplest MongoDB ORM}
8
+ gem.summary = %Q{Transparent persistence for MongoDB}
9
9
  gem.description = <<DESCRIPTION
10
- Candy is a lightweight ORM for the MongoDB database. If MongoMapper is Rails, Candy is Sinatra.
11
- It provides a module you mix into any class, enabling the class to connect to Mongo on its own
12
- and push its objects into a collection. Candied objects act like OpenStructs, allowing attributes
13
- to be defined and updated in Mongo immediately without having to be declared in the class.
14
- Mongo's atomic operators are used whenever possible, and a smart serializer (Candy::Wrapper)
15
- converts almost any object for assignment to any attribute.
10
+ Candy provides simple, transparent object persistence for the MongoDB database. Classes that
11
+ include Candy modules save all properties to Mongo automatically, can be recursively embedded,
12
+ and can retrieve records with chainable open-ended class methods, eliminating the need for
13
+ method calls like 'save' and 'find.'
16
14
  DESCRIPTION
17
15
 
18
16
  gem.email = "sfeley@gmail.com"
19
17
  gem.homepage = "http://github.com/SFEley/candy"
20
18
  gem.authors = ["Stephen Eley"]
21
- gem.add_dependency "mongo", ">= 0.18"
19
+ gem.add_dependency "mongo", ">= 0.19.1"
22
20
  gem.add_development_dependency "rspec", ">= 1.2.9"
23
- gem.add_development_dependency "yard", ">= 0"
21
+ # gem.add_development_dependency "yard", ">= 0"
24
22
  gem.add_development_dependency "mocha", ">= 0.9.8"
25
23
  # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
26
24
  end
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.0
1
+ 0.2.1
@@ -5,53 +5,70 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{candy}
8
- s.version = "0.1.0"
8
+ s.version = "0.2.1"
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-02-16}
13
- s.description = %q{Candy is a lightweight ORM for the MongoDB database. If MongoMapper is Rails, Candy is Sinatra.
14
- It provides a module you mix into any class, enabling the class to connect to Mongo on its own
15
- and push its objects into a collection. Candied objects act like OpenStructs, allowing attributes
16
- to be defined and updated in Mongo immediately without having to be declared in the class.
17
- Mongo's atomic operators are used whenever possible, and a smart serializer (Candy::Wrapper)
18
- converts almost any object for assignment to any attribute.
12
+ s.date = %q{2010-04-05}
13
+ s.description = %q{Candy provides simple, transparent object persistence for the MongoDB database. Classes that
14
+ include Candy modules save all properties to Mongo automatically, can be recursively embedded,
15
+ and can retrieve records with chainable open-ended class methods, eliminating the need for
16
+ method calls like 'save' and 'find.'
19
17
  }
20
18
  s.email = %q{sfeley@gmail.com}
21
19
  s.extra_rdoc_files = [
22
- "LICENSE",
20
+ "LICENSE.markdown",
23
21
  "README.markdown"
24
22
  ]
25
23
  s.files = [
26
24
  ".document",
27
25
  ".gitignore",
28
- "LICENSE",
26
+ "HISTORY.markdown",
27
+ "LICENSE.markdown",
29
28
  "README.markdown",
30
29
  "Rakefile",
31
30
  "VERSION",
32
31
  "candy.gemspec",
33
32
  "lib/candy.rb",
33
+ "lib/candy/array.rb",
34
+ "lib/candy/collection.rb",
34
35
  "lib/candy/crunch.rb",
36
+ "lib/candy/embeddable.rb",
35
37
  "lib/candy/exceptions.rb",
38
+ "lib/candy/factory.rb",
39
+ "lib/candy/hash.rb",
40
+ "lib/candy/piece.rb",
36
41
  "lib/candy/qualified_const_get.rb",
37
42
  "lib/candy/wrapper.rb",
43
+ "spec/candy/array_spec.rb",
44
+ "spec/candy/collection_spec.rb",
38
45
  "spec/candy/crunch_spec.rb",
46
+ "spec/candy/hash_spec.rb",
47
+ "spec/candy/piece_spec.rb",
39
48
  "spec/candy/wrapper_spec.rb",
40
49
  "spec/candy_spec.rb",
41
50
  "spec/spec.opts",
42
51
  "spec/spec.watchr",
43
- "spec/spec_helper.rb"
52
+ "spec/spec_helper.rb",
53
+ "spec/support/kitkat_fixture.rb",
54
+ "spec/support/zagnuts_fixture.rb"
44
55
  ]
45
56
  s.homepage = %q{http://github.com/SFEley/candy}
46
57
  s.rdoc_options = ["--charset=UTF-8"]
47
58
  s.require_paths = ["lib"]
48
- s.rubygems_version = %q{1.3.5}
49
- s.summary = %q{The simplest MongoDB ORM}
59
+ s.rubygems_version = %q{1.3.6}
60
+ s.summary = %q{Transparent persistence for MongoDB}
50
61
  s.test_files = [
51
- "spec/candy/crunch_spec.rb",
62
+ "spec/candy/array_spec.rb",
63
+ "spec/candy/collection_spec.rb",
64
+ "spec/candy/crunch_spec.rb",
65
+ "spec/candy/hash_spec.rb",
66
+ "spec/candy/piece_spec.rb",
52
67
  "spec/candy/wrapper_spec.rb",
53
68
  "spec/candy_spec.rb",
54
- "spec/spec_helper.rb"
69
+ "spec/spec_helper.rb",
70
+ "spec/support/kitkat_fixture.rb",
71
+ "spec/support/zagnuts_fixture.rb"
55
72
  ]
56
73
 
57
74
  if s.respond_to? :specification_version then
@@ -59,20 +76,17 @@ converts almost any object for assignment to any attribute.
59
76
  s.specification_version = 3
60
77
 
61
78
  if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
62
- s.add_runtime_dependency(%q<mongo>, [">= 0.18"])
79
+ s.add_runtime_dependency(%q<mongo>, [">= 0.19.1"])
63
80
  s.add_development_dependency(%q<rspec>, [">= 1.2.9"])
64
- s.add_development_dependency(%q<yard>, [">= 0"])
65
81
  s.add_development_dependency(%q<mocha>, [">= 0.9.8"])
66
82
  else
67
- s.add_dependency(%q<mongo>, [">= 0.18"])
83
+ s.add_dependency(%q<mongo>, [">= 0.19.1"])
68
84
  s.add_dependency(%q<rspec>, [">= 1.2.9"])
69
- s.add_dependency(%q<yard>, [">= 0"])
70
85
  s.add_dependency(%q<mocha>, [">= 0.9.8"])
71
86
  end
72
87
  else
73
- s.add_dependency(%q<mongo>, [">= 0.18"])
88
+ s.add_dependency(%q<mongo>, [">= 0.19.1"])
74
89
  s.add_dependency(%q<rspec>, [">= 1.2.9"])
75
- s.add_dependency(%q<yard>, [">= 0"])
76
90
  s.add_dependency(%q<mocha>, [">= 0.9.8"])
77
91
  end
78
92
  end
@@ -1,140 +1,13 @@
1
- require 'candy/exceptions'
2
- require 'candy/crunch'
3
- require 'candy/wrapper'
1
+ # encoding: utf-8
4
2
 
5
- # Mix me into your classes and Mongo will like them!
6
- module Candy
7
-
8
- module ClassMethods
9
- include Crunch::ClassMethods
10
-
11
- attr_reader :stamp_create, :stamp_update
12
-
13
- # Retrieves an object from Mongo by its ID and returns it. Returns nil if the ID isn't found in Mongo.
14
- def find(id)
15
- if collection.find_one(id)
16
- self.new({:_candy => id})
17
- end
18
- end
19
-
20
- # Retrieves a single object from Mongo by its search attributes, or nil if it can't be found.
21
- def first(conditions={})
22
- options = extract_options(conditions)
23
- if record = collection.find_one(conditions, options)
24
- find(record["_id"])
25
- else
26
- nil
27
- end
28
- end
29
-
30
- # Retrieves all objects matching the search attributes as an Enumerator (even if it's an empty one).
31
- # The option fields from Mongo::Collection#find can be passed as well, and will be extracted from the
32
- # condition set if they're found.
33
- def all(conditions={})
34
- options = extract_options(conditions)
35
- cursor = collection.find(conditions, options)
36
- Enumerator.new do |yielder|
37
- while record = cursor.next_document do
38
- yielder.yield find(record["_id"])
39
- end
40
- end
41
- end
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
-
51
- private
52
- # Returns a hash of options matching those enabled in Mongo::Collection#find, if any of them exist
53
- # in the set of search conditions.
54
- def extract_options(conditions)
55
- options = {:fields => []}
56
- [:fields, :skip, :limit, :sort, :hint, :snapshot, :timeout].each do |option|
57
- options[option] = conditions.delete(option) if conditions[option]
58
- end
59
- options
60
- end
61
-
62
- end
63
-
64
- module InstanceMethods
65
- include Crunch::InstanceMethods
66
-
67
- # We push ourselves into the DB before going on with our day.
68
- def initialize(*args, &block)
69
- @__candy = check_for_candy(args) ||
70
- self.class.collection.insert(self.class.stamp_create ? {:created_at => Time.now.utc} : {})
71
- super
72
- end
3
+ # Make me one with everything...
4
+ Dir[File.join(File.dirname(__FILE__), 'candy', '*.rb')].each {|f| require f}
73
5
 
74
- # Shortcut to the document ID.
75
- def id
76
- @__candy
77
- end
78
-
79
- # Candy's magic ingredient. Assigning to any unknown attribute will push that value into the Mongo collection.
80
- # Retrieving any unknown attribute will return that value from this record in the Mongo collection.
81
- def method_missing(name, *args, &block)
82
- if name =~ /(.*)=$/ # We're assigning
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]}
95
- else
96
- hash = args[0]
97
- end
98
- hash.merge!(:updated_at => Time.now.utc) if self.class.stamp_update
99
- update '$set' => hash
100
- end
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
117
- private
118
-
119
- # Returns the secret decoder ring buried in the arguments to "new"
120
- def check_for_candy(args)
121
- if (candidate = args.pop).is_a?(Hash) and candidate[:_candy]
122
- candidate[:_candy]
123
- else # This must not be for us, so put it back
124
- args.push candidate if candidate
125
- nil
126
- end
127
- end
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
-
134
- end
135
-
136
- def self.included(receiver)
137
- receiver.extend ClassMethods
138
- receiver.send :include, InstanceMethods
139
- end
140
- end
6
+ # Special keys for Candy metadata in the Mongo store. We try to keep these to a minimum,
7
+ # and we're using moderately obscure Unicode symbols to reduce the odds of collisions.
8
+ # If by some strange happenstance you might have single-character keys in your Mongo
9
+ # collections that use the 'CIRCLED LATIN SMALL LETTER' Unicode set, you may need to
10
+ # change these constants. Just be consistent about it if you want to use embedded
11
+ # documents in Candy.
12
+ CLASS_KEY = 'ⓒ'.to_sym
13
+ EMBED_KEY = 'ⓔ'.to_sym
@@ -0,0 +1,74 @@
1
+ require 'candy/crunch'
2
+ require 'candy/embeddable'
3
+
4
+ module Candy
5
+
6
+ # An array-like object that saves itself to a parent Candy::Piece object. MongoDB's atomic
7
+ # array operators are used extensively to perform concurrency-friendly updates of individual
8
+ # array elements without rewriting the whole array.
9
+ class CandyArray
10
+ include Crunch
11
+ include Embeddable
12
+
13
+ # Included for purposes of 'embeddable' compatbility, but does nothing except pass its
14
+ # parameters to the new object. Since you can't save an array on its own anyway, there's
15
+ # no need to flag it as "don't save."
16
+ def self.embed(*args, &block)
17
+ self.new(*args, &block)
18
+ end
19
+
20
+ # Sets the initial array state.
21
+ def initialize(*args, &block)
22
+ @__candy = args
23
+ end
24
+
25
+ # Set a value at a specified index in our array. Note that this operation _does not_ confirm that the
26
+ # array in Mongo still matches our current state. If concurrent updates have happened, you might end up
27
+ # overwriting something other than what you thought.
28
+ def []=(index, val)
29
+ property = embeddify(val)
30
+ @__candy_parent.set embedded(index => property)
31
+ self.candy[index] = property
32
+ end
33
+
34
+ # Retrieves the value from our internal array.
35
+ def [](index)
36
+ candy[index]
37
+ end
38
+
39
+ # Appends a value to our array.
40
+ def <<(val)
41
+ property = embeddify(val)
42
+ @__candy_parent.operate :push, @__candy_parent_key => property
43
+ self.candy << property
44
+ end
45
+ alias_method :push, :<<
46
+
47
+ # Pops the front off the MongoDB array and returns it, then resyncs the array.
48
+ # (Thus supporting real-time concurrency for queue-like behavior.)
49
+ def shift(n=1)
50
+ doc = @__candy_parent.findAndModify({"_id" => @__candy_parent.id}, {'$pop' => {@__candy_parent_key => -1}})
51
+ @__candy = doc['value'][@__candy_parent_key.to_s]
52
+ @__candy.shift
53
+ end
54
+
55
+ # Returns the array of memoized values.
56
+ def candy
57
+ @__candy ||= []
58
+ end
59
+ alias_method :to_mongo, :candy
60
+ alias_method :to_ary, :candy
61
+
62
+ # Array equality.
63
+ def ==(val)
64
+ self.to_ary == val
65
+ end
66
+
67
+ # Array length.
68
+ def length
69
+ self.to_ary.length
70
+ end
71
+ alias_method :size, :length
72
+
73
+ end
74
+ end
@@ -0,0 +1,151 @@
1
+ require 'candy/crunch'
2
+ require 'candy/hash'
3
+
4
+ module Candy
5
+
6
+ # Handles Mongo queries for cursors upon a particular Mongo collection.
7
+ module Collection
8
+ FIND_OPTIONS = [:fields, :skip, :limit, :sort, :hint, :snapshot, :timeout]
9
+ UP_SORTS = [Mongo::ASCENDING, 'ascending', 'asc', :ascending, :asc, 1, :up]
10
+ DOWN_SORTS = [Mongo::DESCENDING, 'descending', 'desc', :descending, :desc, -1, :down]
11
+
12
+ include Enumerable
13
+
14
+ module ClassMethods
15
+ include Crunch::ClassMethods
16
+
17
+ attr_reader :_candy_piece
18
+
19
+ # Sets the collection that all queries run against, qualified by
20
+ # the namespace of the current class. (I.e., if this class is
21
+ # BigModule::LittleModule::People, `collects :person` will look for
22
+ # a collection named "BigModule::LittleModule::Person".) You can also
23
+ # specify a class that includes Candy::Piece that will be instantiated
24
+ # for all found records. Otherwise the collection name is used as a
25
+ # default, and CandyHash is a fallback.
26
+ def collects(collection, piece = nil)
27
+ collectible = namespace + camelcase(collection)
28
+ piecemeal = (piece ? namespace + camelcase(piece) : collectible)
29
+ self.collection = collectible
30
+ @_candy_piece = Kernel.qualified_const_get(piecemeal) || CandyHash
31
+ end
32
+
33
+ def all(options={})
34
+ self.new(options)
35
+ end
36
+
37
+ def method_missing(name, *args, &block)
38
+ coll = self.new
39
+ coll.send(name, *args, &block)
40
+ end
41
+ private
42
+ # Retrieves the 'BlahModule::BleeModule::' part of the class name, so that we
43
+ # can put other things in the same namespace.
44
+ def namespace
45
+ name[/^.*::/] || '' # Hooray for greedy matching
46
+ end
47
+
48
+ # Modified from ActiveSupport (http://rubyonrails.org)
49
+ def camelcase(stringy)
50
+ stringy.to_s.gsub(/(?:^|_)(.)/) {$1.upcase}
51
+ end
52
+
53
+ # Creates a method in the same namespace as the included class that points to
54
+ # 'all', for easier semantics.
55
+ def self.extended(receiver)
56
+ Factory.magic_method(receiver, 'all', 'conditions={}')
57
+ end
58
+
59
+ end # Here endeth the ClassMethods module
60
+
61
+ def initialize(*args, &block)
62
+ conditions = args.pop || {}
63
+ super
64
+ @_candy_query = {}
65
+ if conditions.is_a?(Hash)
66
+ @_candy_options = {:fields => '_id'}.merge(extract_options(conditions))
67
+ @_candy_query.merge!(conditions)
68
+ else
69
+ @_candy_options = {:fields => '_id'}
70
+ end
71
+ refresh_cursor
72
+ end
73
+
74
+ def method_missing(name, *args, &block)
75
+ if @_candy_cursor.respond_to?(name)
76
+ @_candy_cursor.send(name, *args, &block)
77
+ elsif FIND_OPTIONS.include?(name)
78
+ @_candy_options[name] = args.shift
79
+ refresh_cursor
80
+ self
81
+ else
82
+ @_candy_query[name] = args.shift
83
+ @_candy_query.merge!(args) if args.is_a?(Hash)
84
+ refresh_cursor
85
+ self
86
+ end
87
+ end
88
+
89
+
90
+ # Makes our collection enumerable. This relies heavily on Mongo::Cursor methods --
91
+ # we only reimplement it so that the objects we return can be Candy objects.
92
+ def each
93
+ while this = @_candy_cursor.next_document
94
+ yield self.class._candy_piece.new(this)
95
+ end
96
+ end
97
+
98
+ # Get our next document as a Candy object, if there is one.
99
+ def next
100
+ if this = @_candy_cursor.next_document
101
+ self.class._candy_piece.new(this)
102
+ end
103
+ end
104
+
105
+ # Determines the sort order for this collection, with somewhat simpler semantics than
106
+ # the MongoDB options. Each value is either a field name (which defaults to ascending sort)
107
+ # or a direction (which modifies the field name immediately prior) or a two-element array of
108
+ # same. Direction can be :up or :down in addition to Mongo's accepted values of :ascending,
109
+ # 'asc', 1, etc.
110
+ #
111
+ # As an added bonus, sorts can also be chained. If you call #sort more than once then all
112
+ # sorts will be applied in the order in which you called them.
113
+ def sort(*fields)
114
+ @_candy_sort ||= []
115
+ temp_sort = []
116
+ until fields.flatten.empty?
117
+ this = fields.pop # We're going backwards so that we can test for modifiers
118
+ if UP_SORTS.include?(this)
119
+ temp_sort.unshift [fields.pop, Mongo::ASCENDING]
120
+ elsif DOWN_SORTS.include?(this)
121
+ temp_sort.unshift [fields.pop, Mongo::DESCENDING]
122
+ else
123
+ temp_sort.unshift [this, Mongo::ASCENDING]
124
+ end
125
+ end
126
+ @_candy_sort += temp_sort
127
+ @_candy_cursor.sort(@_candy_sort)
128
+ self
129
+ end
130
+
131
+
132
+ private
133
+
134
+ def refresh_cursor
135
+ @_candy_cursor = self.class.collection.find(@_candy_query, @_candy_options)
136
+ end
137
+
138
+ def extract_options(hash)
139
+ options = {}
140
+ (FIND_OPTIONS & hash.keys).each do |key|
141
+ options[key] = hash.delete(key)
142
+ end
143
+ options
144
+ end
145
+
146
+ def self.included(receiver)
147
+ receiver.extend ClassMethods
148
+ end
149
+ end
150
+ end
151
+