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 +5 -0
- data/History +2 -0
- data/LICENSE +20 -0
- data/README.rdoc +25 -0
- data/Rakefile +72 -0
- data/VERSION +1 -0
- data/lib/mongomapper/document.rb +234 -0
- data/lib/mongomapper/embedded_document.rb +260 -0
- data/lib/mongomapper/finder_options.rb +79 -0
- data/lib/mongomapper/key.rb +81 -0
- data/lib/mongomapper/rails_compatibility.rb +20 -0
- data/lib/mongomapper/save_with_validation.rb +27 -0
- data/lib/mongomapper/serialization.rb +55 -0
- data/lib/mongomapper/serializers/json_serializer.rb +77 -0
- data/lib/mongomapper.rb +46 -0
- data/test/serializers/test_json_serializer.rb +104 -0
- data/test/test_associations.rb +52 -0
- data/test/test_callbacks.rb +86 -0
- data/test/test_document.rb +938 -0
- data/test/test_embedded_document.rb +241 -0
- data/test/test_finder_options.rb +133 -0
- data/test/test_helper.rb +64 -0
- data/test/test_key.rb +200 -0
- data/test/test_mongo_mapper.rb +28 -0
- data/test/test_rails_compatibility.rb +24 -0
- data/test/test_serializations.rb +54 -0
- data/test/test_validations.rb +222 -0
- metadata +150 -0
data/History
ADDED
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
|