jnunemaker-mongomapper 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/History ADDED
@@ -0,0 +1,2 @@
1
+ 0.1.0 6/26/2009
2
+ * Initial release
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 John Nunemaker
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,25 @@
1
+ = MongoMapper
2
+
3
+ Awesome gem for modeling your domain and storing it in mongo.
4
+
5
+ == Dependencies
6
+
7
+ * ActiveSupport (activesupport)
8
+ * Mongo Ruby Driver (mongodb-mongo)
9
+ * My fork of the validatable gem (jnunemaker-validatable)
10
+
11
+ == Documentation
12
+
13
+ http://rdoc.info/projects/jnunemaker/mongomapper
14
+
15
+ == More Info
16
+
17
+ You can learn more about mongo here:
18
+ http://www.mongodb.org/
19
+
20
+ You can learn more about the mongo ruby driver here:
21
+ http://github.com/mongodb/mongo-ruby-driver/tree/master
22
+
23
+ == Copyright
24
+
25
+ Copyright (c) 2009 John Nunemaker. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,72 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "mongomapper"
8
+ gem.summary = %Q{Awesome gem for modeling your domain and storing it in mongo}
9
+ gem.email = "nunemaker@gmail.com"
10
+ gem.homepage = "http://github.com/jnunemaker/mongomapper"
11
+ gem.authors = ["John Nunemaker"]
12
+ gem.rubyforge_project = "mongomapper"
13
+
14
+ gem.add_dependency('activesupport')
15
+ gem.add_dependency('mongodb-mongo')
16
+ gem.add_dependency('jnunemaker-validatable')
17
+
18
+ gem.add_development_dependency('mocha')
19
+ gem.add_development_dependency('jnunemaker-matchy', '0.4.0')
20
+ gem.add_development_dependency('thoughtbot-quietbacktrace')
21
+ end
22
+
23
+ Jeweler::RubyforgeTasks.new
24
+ rescue LoadError
25
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
26
+ end
27
+
28
+ require 'rake/testtask'
29
+ Rake::TestTask.new(:test) do |test|
30
+ test.libs << 'lib' << 'test'
31
+ test.pattern = 'test/**/test_*.rb'
32
+ test.verbose = true
33
+ end
34
+
35
+ begin
36
+ require 'rcov/rcovtask'
37
+ Rcov::RcovTask.new do |test|
38
+ test.libs << 'test'
39
+ test.pattern = 'test/**/test_*.rb'
40
+ test.verbose = true
41
+ end
42
+ rescue LoadError
43
+ task :rcov do
44
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
45
+ end
46
+ end
47
+
48
+ begin
49
+ require 'cucumber/rake/task'
50
+ Cucumber::Rake::Task.new(:features)
51
+ rescue LoadError
52
+ task :features do
53
+ abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
54
+ end
55
+ end
56
+
57
+ task :default => :test
58
+
59
+ require 'rake/rdoctask'
60
+ Rake::RDocTask.new do |rdoc|
61
+ if File.exist?('VERSION.yml')
62
+ config = YAML.load(File.read('VERSION.yml'))
63
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
64
+ else
65
+ version = ""
66
+ end
67
+
68
+ rdoc.rdoc_dir = 'rdoc'
69
+ rdoc.title = "MongoMapper #{version}"
70
+ rdoc.rdoc_files.include('README*')
71
+ rdoc.rdoc_files.include('lib/**/*.rb')
72
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.0
@@ -0,0 +1,234 @@
1
+ module MongoMapper
2
+ module Document
3
+ def self.included(model)
4
+ model.class_eval do
5
+ include MongoMapper::EmbeddedDocument
6
+ include InstanceMethods
7
+ include SaveWithValidation
8
+ include RailsCompatibility
9
+ extend ClassMethods
10
+
11
+ key :_id, String
12
+ key :created_at, Time
13
+ key :updated_at, Time
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def find(*args)
19
+ options = args.extract_options!
20
+
21
+ case args.first
22
+ when :first then find_first(options)
23
+ when :last then find_last(options)
24
+ when :all then find_every(options)
25
+ else find_from_ids(args)
26
+ end
27
+ end
28
+
29
+ def first(options={})
30
+ find_first(options)
31
+ end
32
+
33
+ def last(options={})
34
+ find_last(options)
35
+ end
36
+
37
+ def all(options={})
38
+ find_every(options)
39
+ end
40
+
41
+ def find_by_id(id)
42
+ if doc = collection.find_first({:_id => id})
43
+ new(doc)
44
+ end
45
+ end
46
+
47
+ # TODO: remove the rescuing when ruby driver works correctly
48
+ def count(conditions={})
49
+ collection.count(conditions)
50
+ rescue => exception
51
+ if exception.message =~ /Error with count command/
52
+ 0
53
+ else
54
+ raise exception
55
+ end
56
+ end
57
+
58
+ def create(*docs)
59
+ instances = []
60
+ docs.flatten.each { |attrs| instances << new(attrs).save }
61
+ instances.size == 1 ? instances[0] : instances
62
+ end
63
+
64
+ # For updating single document
65
+ # Person.update(1, {:foo => 'bar'})
66
+ #
67
+ # For updating multiple documents at once:
68
+ # Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
69
+ def update(*args)
70
+ updating_multiple = args.length == 1
71
+ if updating_multiple
72
+ update_multiple(args[0])
73
+ else
74
+ id, attributes = args
75
+ update_single(id, attributes)
76
+ end
77
+ end
78
+
79
+ def delete(*ids)
80
+ collection.remove(:_id => {'$in' => ids.flatten})
81
+ end
82
+
83
+ def delete_all(conditions={})
84
+ collection.remove(conditions)
85
+ end
86
+
87
+ def destroy(*ids)
88
+ find_some(ids.flatten).each(&:destroy)
89
+ end
90
+
91
+ def destroy_all(conditions={})
92
+ find(:all, :conditions => conditions).each(&:destroy)
93
+ end
94
+
95
+ def connection(mongo_connection=nil)
96
+ if mongo_connection.nil?
97
+ @connection ||= MongoMapper.connection
98
+ else
99
+ @connection = mongo_connection
100
+ end
101
+ @connection
102
+ end
103
+
104
+ def database(name=nil)
105
+ if name.nil?
106
+ @database ||= MongoMapper.database
107
+ else
108
+ @database = connection.db(name)
109
+ end
110
+ @database
111
+ end
112
+
113
+ def collection(name=nil)
114
+ if name.nil?
115
+ @collection ||= database.collection(self.to_s.demodulize.tableize)
116
+ else
117
+ @collection = database.collection(name)
118
+ end
119
+ @collection
120
+ end
121
+
122
+ private
123
+ def find_every(options)
124
+ criteria, options = FinderOptions.new(options).to_a
125
+ collection.find(criteria, options).to_a.map { |doc| new(doc) }
126
+ end
127
+
128
+ def find_first(options)
129
+ find_every(options.merge(:limit => 1, :order => 'created_at')).first
130
+ end
131
+
132
+ def find_last(options)
133
+ find_every(options.merge(:limit => 1, :order => 'created_at desc')).first
134
+ end
135
+
136
+ def find_some(ids)
137
+ documents = find_every(:conditions => {'_id' => ids})
138
+ if ids.size == documents.size
139
+ documents
140
+ else
141
+ raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
142
+ end
143
+ end
144
+
145
+ def find_from_ids(*ids)
146
+ ids = ids.flatten.compact.uniq
147
+
148
+ case ids.size
149
+ when 0
150
+ raise(DocumentNotFound, "Couldn't find without an ID")
151
+ when 1
152
+ find_by_id(ids[0]) || raise(DocumentNotFound, "Document with id of #{ids[0]} does not exist in collection named #{collection.name}")
153
+ else
154
+ find_some(ids)
155
+ end
156
+ end
157
+
158
+ def update_single(id, attrs)
159
+ if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
160
+ raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
161
+ end
162
+
163
+ find(id).update_attributes(attrs)
164
+ end
165
+
166
+ def update_multiple(docs)
167
+ unless docs.is_a?(Hash)
168
+ raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
169
+ end
170
+ instances = []
171
+ docs.each_pair { |id, attrs| instances << update(id, attrs) }
172
+ instances
173
+ end
174
+ end
175
+
176
+ module InstanceMethods
177
+ def collection
178
+ self.class.collection
179
+ end
180
+
181
+ def new?
182
+ read_attribute('_id').blank? || self.class.find_by_id(id).blank?
183
+ end
184
+
185
+ def save
186
+ run_callbacks(:before_save)
187
+ new? ? create : update
188
+ run_callbacks(:after_save)
189
+ self
190
+ end
191
+
192
+ def update_attributes(attrs={})
193
+ self.attributes = attrs
194
+ save
195
+ end
196
+
197
+ def destroy
198
+ run_callbacks(:before_destroy)
199
+ collection.remove(:_id => id) unless new?
200
+ run_callbacks(:after_destroy)
201
+ freeze
202
+ end
203
+
204
+ def ==(other)
205
+ other.is_a?(self.class) && id == other.id
206
+ end
207
+
208
+ private
209
+ def create
210
+ write_attribute('_id', generate_id) if read_attribute('_id').blank?
211
+ update_timestamps
212
+ run_callbacks(:before_create)
213
+ collection.insert(attributes.merge!(embedded_association_attributes))
214
+ run_callbacks(:after_create)
215
+ end
216
+
217
+ def update
218
+ update_timestamps
219
+ run_callbacks(:before_update)
220
+ collection.modify({:_id => id}, attributes.merge!(embedded_association_attributes))
221
+ run_callbacks(:after_update)
222
+ end
223
+
224
+ def update_timestamps
225
+ write_attribute('created_at', Time.now.utc) if new?
226
+ write_attribute('updated_at', Time.now.utc)
227
+ end
228
+
229
+ def generate_id
230
+ XGen::Mongo::Driver::ObjectID.new
231
+ end
232
+ end
233
+ end # Document
234
+ end # MongoMapper
@@ -0,0 +1,260 @@
1
+ module MongoMapper
2
+ module EmbeddedDocument
3
+ class NotImplemented < StandardError; end
4
+
5
+ def self.included(model)
6
+ model.class_eval do
7
+ include InstanceMethods
8
+ extend ClassMethods
9
+ include Validatable
10
+ include ActiveSupport::Callbacks
11
+ include MongoMapper::Serialization
12
+
13
+ define_callbacks :before_validation_on_create, :before_validation_on_update,
14
+ :before_validation, :after_validation,
15
+ :before_create, :after_create,
16
+ :before_update, :after_update,
17
+ :before_save, :after_save,
18
+ :before_destroy, :after_destroy
19
+ end
20
+ end
21
+
22
+ module ClassMethods
23
+ class Association
24
+ attr_accessor :name
25
+
26
+ def initialize(name)
27
+ @name = name.to_s
28
+ end
29
+
30
+ def klass
31
+ @klass ||= name.classify.constantize
32
+ end
33
+
34
+ def ivar
35
+ @ivar ||= "@#{name}"
36
+ end
37
+ end
38
+
39
+ def many(association_name)
40
+ association = Association.new(association_name)
41
+ associations[association.name] = association
42
+ class_eval <<-EOS
43
+ def #{association.name}
44
+ #{association.ivar} ||= []
45
+ #{association.ivar}
46
+ end
47
+ EOS
48
+ end
49
+
50
+ def associations
51
+ @associations ||= HashWithIndifferentAccess.new
52
+ end
53
+
54
+ def keys
55
+ @keys ||= HashWithIndifferentAccess.new
56
+ end
57
+
58
+ def key(name, type, options={})
59
+ key = Key.new(name, type, options)
60
+ keys[key.name] = key
61
+ apply_validations_for(key)
62
+ define_embedded_document_accessors_for(key)
63
+ create_indexes_for(key)
64
+ key
65
+ end
66
+
67
+ # TODO: remove to_s when ruby driver supports symbols (I sent patch)
68
+ def ensure_index(name_or_array, options={})
69
+ keys_to_index = if name_or_array.is_a?(Array)
70
+ name_or_array.map { |pair| [pair[0].to_s, pair[1]] }
71
+ else
72
+ name_or_array.to_s
73
+ end
74
+
75
+ collection.create_index(keys_to_index, options.delete(:unique))
76
+ end
77
+
78
+ private
79
+ def define_embedded_document_accessors_for(key)
80
+ return unless key.embedded_document?
81
+ instance_var = "@#{key.name}"
82
+
83
+ define_method(key.name) do
84
+ key.get(instance_variable_get(instance_var))
85
+ end
86
+
87
+ define_method("#{key.name}=") do |value|
88
+ instance_variable_set(instance_var, key.get(value))
89
+ end
90
+ end
91
+
92
+ def create_indexes_for(key)
93
+ ensure_index key.name if key.options[:index]
94
+ end
95
+
96
+ def apply_validations_for(key)
97
+ attribute = key.name.to_sym
98
+
99
+ if key.options[:required]
100
+ validates_presence_of(attribute)
101
+ end
102
+
103
+ if key.options[:numeric]
104
+ number_options = key.type == Integer ? {:only_integer => true} : {}
105
+ validates_numericality_of(attribute, number_options)
106
+ end
107
+
108
+ if key.options[:format]
109
+ validates_format_of(attribute, :with => key.options[:format])
110
+ end
111
+
112
+ if key.options[:length]
113
+ length_options = case key.options[:length]
114
+ when Integer
115
+ {:minimum => 0, :maximum => key.options[:length]}
116
+ when Range
117
+ {:within => key.options[:length]}
118
+ when Hash
119
+ key.options[:length]
120
+ end
121
+ validates_length_of(attribute, length_options)
122
+ end
123
+ end
124
+
125
+ end
126
+
127
+ module InstanceMethods
128
+ def initialize(attrs={})
129
+ initialize_embedded_associations(attrs)
130
+ self.attributes = attrs
131
+ end
132
+
133
+ def attributes=(attrs)
134
+ attrs.each_pair do |key_name, value|
135
+ write_attribute(key_name, value) if writer?(key_name)
136
+ end
137
+ end
138
+
139
+ def attributes
140
+ self.class.keys.inject(HashWithIndifferentAccess.new) do |attributes, key_hash|
141
+ name, key = key_hash
142
+ value = value_for_key(key)
143
+ attributes[name] = value unless value.nil?
144
+ attributes
145
+ end
146
+ end
147
+
148
+ def reader?(name)
149
+ defined_key_names.include?(name.to_s)
150
+ end
151
+
152
+ def writer?(name)
153
+ name = name.to_s
154
+ name = name.chop if name.ends_with?('=')
155
+ reader?(name)
156
+ end
157
+
158
+ def before_typecast_reader?(name)
159
+ name.to_s.match(/^(.*)_before_typecast$/) && reader?($1)
160
+ end
161
+
162
+ def [](name)
163
+ read_attribute(name)
164
+ end
165
+
166
+ def []=(name, value)
167
+ write_attribute(name, value)
168
+ end
169
+
170
+ def id
171
+ read_attribute('_id')
172
+ end
173
+
174
+ def method_missing(method, *args, &block)
175
+ attribute = method.to_s
176
+
177
+ if reader?(attribute)
178
+ read_attribute(attribute)
179
+ elsif writer?(attribute)
180
+ write_attribute(attribute.chop, args[0])
181
+ elsif before_typecast_reader?(attribute)
182
+ read_attribute_before_typecast(attribute.gsub(/_before_typecast$/, ''))
183
+ else
184
+ super
185
+ end
186
+ end
187
+
188
+ def ==(other)
189
+ other.is_a?(self.class) && attributes == other.attributes
190
+ end
191
+
192
+ def inspect
193
+ attributes_as_nice_string = defined_key_names.collect do |name|
194
+ "#{name}: #{read_attribute(name)}"
195
+ end.join(", ")
196
+ "#<#{self.class} #{attributes_as_nice_string}>"
197
+ end
198
+
199
+ def respond_to?(method, include_private=false)
200
+ return true if reader?(method) || writer?(method) || before_typecast_reader?(method)
201
+ super
202
+ end
203
+
204
+ private
205
+ def value_for_key(key)
206
+ if key.native?
207
+ read_attribute(key.name)
208
+ else
209
+ embedded_document = read_attribute(key.name)
210
+ embedded_document && embedded_document.attributes
211
+ end
212
+ end
213
+
214
+ def read_attribute(name)
215
+ defined_key(name).get(instance_variable_get("@#{name}"))
216
+ end
217
+
218
+ def read_attribute_before_typecast(name)
219
+ instance_variable_get("@#{name}_before_typecast")
220
+ end
221
+
222
+ def write_attribute(name, value)
223
+ instance_variable_set "@#{name}_before_typecast", value
224
+ instance_variable_set "@#{name}", defined_key(name).set(value)
225
+ end
226
+
227
+ def defined_key(name)
228
+ self.class.keys[name]
229
+ end
230
+
231
+ def defined_key_names
232
+ self.class.keys.keys
233
+ end
234
+
235
+ def only_defined_keys(hash={})
236
+ defined_key_names = defined_key_names()
237
+ hash.delete_if { |k, v| !defined_key_names.include?(k.to_s) }
238
+ end
239
+
240
+ def embedded_association_attributes
241
+ attributes = HashWithIndifferentAccess.new
242
+ self.class.associations.each_pair do |name, association|
243
+ attributes[name] = send(name).collect { |item| item.attributes }
244
+ end
245
+ attributes
246
+ end
247
+
248
+ def initialize_embedded_associations(attrs={})
249
+ self.class.associations.each_pair do |name, association|
250
+ if collection = attrs.delete(name)
251
+ association_value = collection.collect do |item|
252
+ association.klass.new(item)
253
+ end
254
+ instance_variable_set(association.ivar, association_value)
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,79 @@
1
+ module MongoMapper
2
+ class FinderOptions
3
+ attr_reader :options
4
+
5
+ def initialize(options)
6
+ raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
7
+ @options = options.symbolize_keys
8
+ @conditions = @options.delete(:conditions) || {}
9
+ end
10
+
11
+ def criteria
12
+ convert_conditions(@conditions.dup)
13
+ end
14
+
15
+ def options
16
+ convert_options(@options.dup)
17
+ end
18
+
19
+ def to_a
20
+ [criteria, options]
21
+ end
22
+
23
+ private
24
+ def convert_conditions(conditions)
25
+ criteria = {}
26
+ conditions.each_pair do |field, value|
27
+ case value
28
+ when Array
29
+ criteria[field] = {'$in' => value}
30
+ when Hash
31
+ criteria[field] = convert_conditions(value)
32
+ else
33
+ criteria[field] = value
34
+ end
35
+ end
36
+
37
+ criteria
38
+ end
39
+
40
+ def convert_options(options)
41
+ {
42
+ :fields => convert_fields(options.delete(:fields) || options.delete(:select)),
43
+ :offset => (options.delete(:offset) || 0).to_i,
44
+ :limit => (options.delete(:limit) || 0).to_i,
45
+ :sort => convert_sort(options.delete(:order))
46
+ }
47
+ end
48
+
49
+ def convert_fields(fields)
50
+ return if fields.blank?
51
+
52
+ if fields.is_a?(String)
53
+ fields.split(',').map { |field| field.strip }
54
+ else
55
+ fields.flatten.compact
56
+ end
57
+ end
58
+
59
+ def convert_sort(sort)
60
+ return if sort.blank?
61
+ pieces = sort.split(',')
62
+ pairs = pieces.map { |s| convert_sort_piece(s) }
63
+
64
+ hash = OrderedHash.new
65
+ pairs.each do |pair|
66
+ field, sort_direction = pair
67
+ hash[field] = sort_direction
68
+ end
69
+ hash.symbolize_keys
70
+ end
71
+
72
+ def convert_sort_piece(str)
73
+ field, direction = str.strip.split(' ')
74
+ direction ||= 'ASC'
75
+ direction = direction.upcase == 'ASC' ? 1 : -1
76
+ [field, direction]
77
+ end
78
+ end
79
+ end