ramsingla-mongomapper 0.2.1
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 +7 -0
- data/History +30 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +71 -0
- data/VERSION +1 -0
- data/lib/mongomapper.rb +60 -0
- data/lib/mongomapper/associations.rb +69 -0
- data/lib/mongomapper/associations/array_proxy.rb +6 -0
- data/lib/mongomapper/associations/base.rb +50 -0
- data/lib/mongomapper/associations/belongs_to_proxy.rb +26 -0
- data/lib/mongomapper/associations/has_many_embedded_proxy.rb +19 -0
- data/lib/mongomapper/associations/has_many_proxy.rb +28 -0
- data/lib/mongomapper/associations/polymorphic_belongs_to_proxy.rb +31 -0
- data/lib/mongomapper/associations/proxy.rb +60 -0
- data/lib/mongomapper/callbacks.rb +106 -0
- data/lib/mongomapper/document.rb +263 -0
- data/lib/mongomapper/embedded_document.rb +295 -0
- data/lib/mongomapper/finder_options.rb +81 -0
- data/lib/mongomapper/key.rb +82 -0
- data/lib/mongomapper/observing.rb +50 -0
- data/lib/mongomapper/pagination.rb +52 -0
- data/lib/mongomapper/rails_compatibility.rb +23 -0
- data/lib/mongomapper/save_with_validation.rb +19 -0
- data/lib/mongomapper/serialization.rb +55 -0
- data/lib/mongomapper/serializers/json_serializer.rb +77 -0
- data/lib/mongomapper/validations.rb +47 -0
- data/mongomapper.gemspec +105 -0
- data/test/serializers/test_json_serializer.rb +104 -0
- data/test/test_associations.rb +211 -0
- data/test/test_callbacks.rb +84 -0
- data/test/test_document.rb +995 -0
- data/test/test_embedded_document.rb +253 -0
- data/test/test_finder_options.rb +148 -0
- data/test/test_helper.rb +62 -0
- data/test/test_key.rb +200 -0
- data/test/test_mongomapper.rb +28 -0
- data/test/test_observing.rb +101 -0
- data/test/test_rails_compatibility.rb +29 -0
- data/test/test_serializations.rb +54 -0
- data/test/test_validations.rb +409 -0
- metadata +156 -0
@@ -0,0 +1,60 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
module Associations
|
3
|
+
class Proxy
|
4
|
+
attr_reader :owner, :association
|
5
|
+
|
6
|
+
instance_methods.each do |m|
|
7
|
+
undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(owner, association)
|
11
|
+
@owner= owner
|
12
|
+
@association = association
|
13
|
+
|
14
|
+
reset
|
15
|
+
end
|
16
|
+
|
17
|
+
def respond_to?(*methods)
|
18
|
+
(load_target && @target.respond_to?(*methods))
|
19
|
+
end
|
20
|
+
|
21
|
+
def reset
|
22
|
+
@target = nil
|
23
|
+
end
|
24
|
+
|
25
|
+
def reload_target
|
26
|
+
reset
|
27
|
+
load_target
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def send(method, *args)
|
32
|
+
load_target
|
33
|
+
@target.send(method, *args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def replace(v)
|
37
|
+
raise NotImplementedError
|
38
|
+
end
|
39
|
+
|
40
|
+
protected
|
41
|
+
def method_missing(method, *args)
|
42
|
+
if load_target
|
43
|
+
if block_given?
|
44
|
+
@target.send(method, *args) { |*block_args| yield(*block_args) }
|
45
|
+
else
|
46
|
+
@target.send(method, *args)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def load_target
|
52
|
+
@target ||= find_target
|
53
|
+
end
|
54
|
+
|
55
|
+
def find_target
|
56
|
+
raise NotImplementedError
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
module Callbacks
|
3
|
+
def self.included(model) #:nodoc:
|
4
|
+
model.class_eval do
|
5
|
+
extend Observable
|
6
|
+
include ActiveSupport::Callbacks
|
7
|
+
|
8
|
+
define_callbacks *%w(
|
9
|
+
before_save after_save before_create after_create before_update after_update before_validation
|
10
|
+
after_validation before_validation_on_create after_validation_on_create before_validation_on_update
|
11
|
+
after_validation_on_update before_destroy after_destroy
|
12
|
+
)
|
13
|
+
|
14
|
+
[:create_or_update, :valid?, :create, :update, :destroy].each do |method|
|
15
|
+
alias_method_chain method, :callbacks
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def before_save() end
|
21
|
+
|
22
|
+
def after_save() end
|
23
|
+
def create_or_update_with_callbacks #:nodoc:
|
24
|
+
return false if callback(:before_save) == false
|
25
|
+
if result = create_or_update_without_callbacks
|
26
|
+
callback(:after_save)
|
27
|
+
end
|
28
|
+
result
|
29
|
+
end
|
30
|
+
private :create_or_update_with_callbacks
|
31
|
+
|
32
|
+
def before_create() end
|
33
|
+
|
34
|
+
def after_create() end
|
35
|
+
def create_with_callbacks #:nodoc:
|
36
|
+
return false if callback(:before_create) == false
|
37
|
+
result = create_without_callbacks
|
38
|
+
callback(:after_create)
|
39
|
+
result
|
40
|
+
end
|
41
|
+
private :create_with_callbacks
|
42
|
+
|
43
|
+
def before_update() end
|
44
|
+
|
45
|
+
def after_update() end
|
46
|
+
|
47
|
+
def update_with_callbacks(*args) #:nodoc:
|
48
|
+
return false if callback(:before_update) == false
|
49
|
+
result = update_without_callbacks(*args)
|
50
|
+
callback(:after_update)
|
51
|
+
result
|
52
|
+
end
|
53
|
+
private :update_with_callbacks
|
54
|
+
|
55
|
+
def before_validation() end
|
56
|
+
|
57
|
+
def after_validation() end
|
58
|
+
|
59
|
+
def before_validation_on_create() end
|
60
|
+
|
61
|
+
def after_validation_on_create() end
|
62
|
+
|
63
|
+
def before_validation_on_update() end
|
64
|
+
|
65
|
+
def after_validation_on_update() end
|
66
|
+
|
67
|
+
def valid_with_callbacks? #:nodoc:
|
68
|
+
return false if callback(:before_validation) == false
|
69
|
+
result = new? ? callback(:before_validation_on_create) : callback(:before_validation_on_update)
|
70
|
+
return false if false == result
|
71
|
+
|
72
|
+
result = valid_without_callbacks?
|
73
|
+
callback(:after_validation)
|
74
|
+
|
75
|
+
new? ? callback(:after_validation_on_create) : callback(:after_validation_on_update)
|
76
|
+
return result
|
77
|
+
end
|
78
|
+
|
79
|
+
def before_destroy() end
|
80
|
+
|
81
|
+
def after_destroy() end
|
82
|
+
def destroy_with_callbacks #:nodoc:
|
83
|
+
return false if callback(:before_destroy) == false
|
84
|
+
result = destroy_without_callbacks
|
85
|
+
callback(:after_destroy)
|
86
|
+
result
|
87
|
+
end
|
88
|
+
|
89
|
+
private
|
90
|
+
def callback(method)
|
91
|
+
result = run_callbacks(method) { |result, object| false == result }
|
92
|
+
|
93
|
+
if result != false && respond_to_without_attributes?(method)
|
94
|
+
result = send(method)
|
95
|
+
end
|
96
|
+
|
97
|
+
notify(method)
|
98
|
+
return result
|
99
|
+
end
|
100
|
+
|
101
|
+
def notify(method) #:nodoc:
|
102
|
+
self.class.changed
|
103
|
+
self.class.notify_observers(method, self)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module MongoMapper
|
4
|
+
module Document
|
5
|
+
def self.included(model)
|
6
|
+
model.class_eval do
|
7
|
+
include EmbeddedDocument
|
8
|
+
include InstanceMethods
|
9
|
+
include Observing
|
10
|
+
include Callbacks
|
11
|
+
include SaveWithValidation
|
12
|
+
extend ClassMethods
|
13
|
+
|
14
|
+
key :_id, String
|
15
|
+
key :created_at, Time
|
16
|
+
key :updated_at, Time
|
17
|
+
end
|
18
|
+
|
19
|
+
descendants << model
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.descendants
|
23
|
+
@descendants ||= Set.new
|
24
|
+
end
|
25
|
+
|
26
|
+
module ClassMethods
|
27
|
+
def find(*args)
|
28
|
+
options = args.extract_options!
|
29
|
+
|
30
|
+
case args.first
|
31
|
+
when :first then find_first(options)
|
32
|
+
when :last then find_last(options)
|
33
|
+
when :all then find_every(options)
|
34
|
+
else find_from_ids(args)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def paginate(options)
|
39
|
+
per_page = options.delete(:per_page)
|
40
|
+
page = options.delete(:page)
|
41
|
+
total_entries = count(options)
|
42
|
+
|
43
|
+
collection = Pagination::PaginationProxy.new(total_entries, page, per_page)
|
44
|
+
|
45
|
+
options[:limit] = collection.limit
|
46
|
+
options[:skip] = collection.skip
|
47
|
+
|
48
|
+
collection.subject = find_every(options)
|
49
|
+
collection
|
50
|
+
end
|
51
|
+
|
52
|
+
def first(options={})
|
53
|
+
find_first(options)
|
54
|
+
end
|
55
|
+
|
56
|
+
def last(options={})
|
57
|
+
find_last(options)
|
58
|
+
end
|
59
|
+
|
60
|
+
def all(options={})
|
61
|
+
find_every(options)
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_by_id(id)
|
65
|
+
if doc = collection.find_first({:_id => id})
|
66
|
+
new(doc)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# TODO: remove the rescuing when ruby driver works correctly
|
71
|
+
def count(conditions={})
|
72
|
+
collection.count(FinderOptions.to_mongo_criteria(conditions))
|
73
|
+
end
|
74
|
+
|
75
|
+
def create(*docs)
|
76
|
+
instances = []
|
77
|
+
docs.flatten.each do |attrs|
|
78
|
+
doc = new(attrs); doc.save
|
79
|
+
instances << doc
|
80
|
+
end
|
81
|
+
instances.size == 1 ? instances[0] : instances
|
82
|
+
end
|
83
|
+
|
84
|
+
# For updating single document
|
85
|
+
# Person.update(1, {:foo => 'bar'})
|
86
|
+
#
|
87
|
+
# For updating multiple documents at once:
|
88
|
+
# Person.update({'1' => {:foo => 'bar'}, '2' => {:baz => 'wick'}})
|
89
|
+
def update(*args)
|
90
|
+
updating_multiple = args.length == 1
|
91
|
+
if updating_multiple
|
92
|
+
update_multiple(args[0])
|
93
|
+
else
|
94
|
+
id, attributes = args
|
95
|
+
update_single(id, attributes)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete(*ids)
|
100
|
+
collection.remove(:_id => {'$in' => ids.flatten})
|
101
|
+
end
|
102
|
+
|
103
|
+
def delete_all(conditions={})
|
104
|
+
collection.remove(FinderOptions.to_mongo_criteria(conditions))
|
105
|
+
end
|
106
|
+
|
107
|
+
def destroy(*ids)
|
108
|
+
find_some(ids.flatten).each(&:destroy)
|
109
|
+
end
|
110
|
+
|
111
|
+
def destroy_all(conditions={})
|
112
|
+
find(:all, :conditions => conditions).each(&:destroy)
|
113
|
+
end
|
114
|
+
|
115
|
+
def connection(mongo_connection=nil)
|
116
|
+
if mongo_connection.nil?
|
117
|
+
@connection ||= MongoMapper.connection
|
118
|
+
else
|
119
|
+
@connection = mongo_connection
|
120
|
+
end
|
121
|
+
@connection
|
122
|
+
end
|
123
|
+
|
124
|
+
def database(name=nil)
|
125
|
+
if name.nil?
|
126
|
+
@database ||= MongoMapper.database
|
127
|
+
else
|
128
|
+
@database = connection.db(name)
|
129
|
+
end
|
130
|
+
@database
|
131
|
+
end
|
132
|
+
|
133
|
+
def collection(name=nil)
|
134
|
+
if name.nil?
|
135
|
+
@collection ||= database.collection(self.to_s.demodulize.tableize)
|
136
|
+
else
|
137
|
+
@collection = database.collection(name)
|
138
|
+
end
|
139
|
+
@collection
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
def find_every(options)
|
144
|
+
criteria, options = FinderOptions.new(options).to_a
|
145
|
+
collection.find(criteria, options).to_a.map { |doc| new(doc) }
|
146
|
+
end
|
147
|
+
|
148
|
+
def find_first(options)
|
149
|
+
find_every(options.merge(:limit => 1, :order => 'created_at')).first
|
150
|
+
end
|
151
|
+
|
152
|
+
def find_last(options)
|
153
|
+
find_every(options.merge(:limit => 1, :order => 'created_at desc')).first
|
154
|
+
end
|
155
|
+
|
156
|
+
def find_some(ids)
|
157
|
+
documents = find_every(:conditions => {'_id' => ids})
|
158
|
+
if ids.size == documents.size
|
159
|
+
documents
|
160
|
+
else
|
161
|
+
raise DocumentNotFound, "Couldn't find all of the ids (#{ids.to_sentence}). Found #{documents.size}, but was expecting #{ids.size}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def find_from_ids(*ids)
|
166
|
+
ids = ids.flatten.compact.uniq
|
167
|
+
|
168
|
+
case ids.size
|
169
|
+
when 0
|
170
|
+
raise(DocumentNotFound, "Couldn't find without an ID")
|
171
|
+
when 1
|
172
|
+
find_by_id(ids[0]) || raise(DocumentNotFound, "Document with id of #{ids[0]} does not exist in collection named #{collection.name}")
|
173
|
+
else
|
174
|
+
find_some(ids)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def update_single(id, attrs)
|
179
|
+
if id.blank? || attrs.blank? || !attrs.is_a?(Hash)
|
180
|
+
raise ArgumentError, "Updating a single document requires an id and a hash of attributes"
|
181
|
+
end
|
182
|
+
|
183
|
+
find(id).update_attributes(attrs)
|
184
|
+
end
|
185
|
+
|
186
|
+
def update_multiple(docs)
|
187
|
+
unless docs.is_a?(Hash)
|
188
|
+
raise ArgumentError, "Updating multiple documents takes 1 argument and it must be hash"
|
189
|
+
end
|
190
|
+
instances = []
|
191
|
+
docs.each_pair { |id, attrs| instances << update(id, attrs) }
|
192
|
+
instances
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
module InstanceMethods
|
197
|
+
def collection
|
198
|
+
self.class.collection
|
199
|
+
end
|
200
|
+
|
201
|
+
def new?
|
202
|
+
read_attribute('_id').blank? || self.class.find_by_id(id).blank?
|
203
|
+
end
|
204
|
+
|
205
|
+
def save
|
206
|
+
create_or_update
|
207
|
+
end
|
208
|
+
|
209
|
+
def save!
|
210
|
+
create_or_update || raise(DocumentNotValid.new(self))
|
211
|
+
end
|
212
|
+
|
213
|
+
def update_attributes(attrs={})
|
214
|
+
self.attributes = attrs
|
215
|
+
save
|
216
|
+
self
|
217
|
+
end
|
218
|
+
|
219
|
+
def destroy
|
220
|
+
collection.remove(:_id => id) unless new?
|
221
|
+
freeze
|
222
|
+
end
|
223
|
+
|
224
|
+
def ==(other)
|
225
|
+
other.is_a?(self.class) && id == other.id
|
226
|
+
end
|
227
|
+
|
228
|
+
def id
|
229
|
+
read_attribute('_id')
|
230
|
+
end
|
231
|
+
|
232
|
+
private
|
233
|
+
def create_or_update
|
234
|
+
result = new? ? create : update
|
235
|
+
result != false
|
236
|
+
end
|
237
|
+
|
238
|
+
def create
|
239
|
+
write_attribute('_id', generate_id) if read_attribute('_id').blank?
|
240
|
+
update_timestamps
|
241
|
+
save_to_collection
|
242
|
+
end
|
243
|
+
|
244
|
+
def update
|
245
|
+
update_timestamps
|
246
|
+
save_to_collection
|
247
|
+
end
|
248
|
+
|
249
|
+
def save_to_collection
|
250
|
+
collection.save(attributes.merge!(embedded_association_attributes))
|
251
|
+
end
|
252
|
+
|
253
|
+
def update_timestamps
|
254
|
+
write_attribute('created_at', Time.now.utc) if new?
|
255
|
+
write_attribute('updated_at', Time.now.utc)
|
256
|
+
end
|
257
|
+
|
258
|
+
def generate_id
|
259
|
+
XGen::Mongo::Driver::ObjectID.new
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end # Document
|
263
|
+
end # MongoMapper
|
@@ -0,0 +1,295 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
module MongoMapper
|
4
|
+
module EmbeddedDocument
|
5
|
+
class NotImplemented < StandardError; end
|
6
|
+
|
7
|
+
def self.included(model)
|
8
|
+
model.class_eval do
|
9
|
+
extend ClassMethods
|
10
|
+
include InstanceMethods
|
11
|
+
|
12
|
+
extend Associations::ClassMethods
|
13
|
+
include Associations::InstanceMethods
|
14
|
+
|
15
|
+
include Validatable
|
16
|
+
include Serialization
|
17
|
+
include RailsCompatibility
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
|
23
|
+
def keys
|
24
|
+
@keys ||= if parent = parent_model
|
25
|
+
parent.keys.dup
|
26
|
+
else
|
27
|
+
HashWithIndifferentAccess.new
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def key(name, type, options={})
|
32
|
+
key = Key.new(name, type, options)
|
33
|
+
keys[key.name] = key
|
34
|
+
apply_validations_for(key)
|
35
|
+
create_indexes_for(key)
|
36
|
+
key
|
37
|
+
end
|
38
|
+
|
39
|
+
def ensure_index(name_or_array, options={})
|
40
|
+
keys_to_index = if name_or_array.is_a?(Array)
|
41
|
+
name_or_array.map { |pair| [pair[0], pair[1]] }
|
42
|
+
else
|
43
|
+
name_or_array
|
44
|
+
end
|
45
|
+
|
46
|
+
collection.create_index(keys_to_index, options.delete(:unique))
|
47
|
+
end
|
48
|
+
|
49
|
+
def embeddable?
|
50
|
+
!self.ancestors.include?(Document)
|
51
|
+
end
|
52
|
+
|
53
|
+
def parent_model
|
54
|
+
if parent = ancestors[1]
|
55
|
+
parent if parent.ancestors.include?(EmbeddedDocument)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
def create_indexes_for(key)
|
61
|
+
ensure_index key.name if key.options[:index]
|
62
|
+
end
|
63
|
+
|
64
|
+
def apply_validations_for(key)
|
65
|
+
attribute = key.name.to_sym
|
66
|
+
|
67
|
+
if key.options[:required]
|
68
|
+
validates_presence_of(attribute, :message => "#{attribute}.not.present")
|
69
|
+
end
|
70
|
+
|
71
|
+
if key.options[:valid]
|
72
|
+
validates_true_for attribute, :logic => lambda{ ( valid = send( attribute ).try( :valid? ) ).nil? ? true : valid }, :message => "#{attribute}.record.invalid"
|
73
|
+
end
|
74
|
+
|
75
|
+
if key.options[:unique]
|
76
|
+
validates_uniqueness_of(attribute, :message => "#{attribute}.already.taken")
|
77
|
+
end
|
78
|
+
|
79
|
+
if key.options[:numeric]
|
80
|
+
number_options = key.type == Integer ? { :only_integer => true, :message => "#{attribute}.not.integer" } : { :message => "#{attribute}.not.numeric" }
|
81
|
+
validates_numericality_of(attribute, number_options)
|
82
|
+
end
|
83
|
+
|
84
|
+
if key.options[:format]
|
85
|
+
validates_format_of(attribute, :with => key.options[:format], :message => "#{attribute}.format.invalid")
|
86
|
+
end
|
87
|
+
|
88
|
+
if key.options[:length]
|
89
|
+
length_options = case key.options[:length]
|
90
|
+
when Integer
|
91
|
+
{:minimum => 0, :maximum => key.options[:length], :message => "#{attribute}.exceeds.max_length"}
|
92
|
+
when Range
|
93
|
+
{:within => key.options[:length], :message => "#{attribute}.length.out_of_bounds"}
|
94
|
+
when Hash
|
95
|
+
key.options[:length].merge!(:message => "#{attribute}.length.invalid")
|
96
|
+
end
|
97
|
+
validates_length_of(attribute, length_options)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def validates_uniqueness_of(*args)
|
102
|
+
add_validations(args, MongoMapper::Validations::ValidatesUniquenessOf)
|
103
|
+
end
|
104
|
+
|
105
|
+
def validates_exclusion_of(*args)
|
106
|
+
add_validations(args, MongoMapper::Validations::ValidatesExclusionOf)
|
107
|
+
end
|
108
|
+
|
109
|
+
def validates_inclusion_of(*args)
|
110
|
+
add_validations(args, MongoMapper::Validations::ValidatesInclusionOf)
|
111
|
+
end
|
112
|
+
|
113
|
+
end
|
114
|
+
|
115
|
+
module InstanceMethods
|
116
|
+
|
117
|
+
#
|
118
|
+
# Metadata attributes for fancy magic
|
119
|
+
# e.g. full_key_name e.g. account.login, name.first
|
120
|
+
# user.account.new? => user.new?
|
121
|
+
# validates_uniqueness_of in embedded document
|
122
|
+
# callbacks in embedded document (TODO)
|
123
|
+
#
|
124
|
+
attr_reader :_root # The Mongo::Document which is the root doc
|
125
|
+
attr_reader :_parent # The parent document. For root parent is nil
|
126
|
+
attr_reader :_parent_key # The key name of the embedded document in parent doc
|
127
|
+
|
128
|
+
def initialize(attrs={})
|
129
|
+
@_root = self.class.include?(MongoMapper::Document) ? self : nil
|
130
|
+
@_parent = nil
|
131
|
+
@_parent_key = nil
|
132
|
+
unless attrs.nil?
|
133
|
+
initialize_associations(attrs)
|
134
|
+
self.attributes = attrs
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def new?
|
139
|
+
_root.try(:new?)
|
140
|
+
end
|
141
|
+
|
142
|
+
def full_key_path(name)
|
143
|
+
_parent ? _parent.full_key_path("#{_parent_key}.#{name}") : name
|
144
|
+
end
|
145
|
+
|
146
|
+
def attributes=(attrs)
|
147
|
+
return if attrs.blank?
|
148
|
+
attrs.each_pair do |key_name, value|
|
149
|
+
if writer?(key_name)
|
150
|
+
write_attribute(key_name, value)
|
151
|
+
else
|
152
|
+
writer_method ="#{key_name}="
|
153
|
+
self.send(writer_method, value) if respond_to?(writer_method)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def attributes
|
159
|
+
self.class.keys.inject(HashWithIndifferentAccess.new) do |attributes, key_hash|
|
160
|
+
name, key = key_hash
|
161
|
+
value = value_for_key(key)
|
162
|
+
attributes[name] = value unless value.nil?
|
163
|
+
attributes
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def reader?(name)
|
168
|
+
defined_key_names.include?(name.to_s)
|
169
|
+
end
|
170
|
+
|
171
|
+
def writer?(name)
|
172
|
+
name = name.to_s
|
173
|
+
name = name.chop if name.ends_with?('=')
|
174
|
+
reader?(name)
|
175
|
+
end
|
176
|
+
|
177
|
+
def before_typecast_reader?(name)
|
178
|
+
name.to_s.match(/^(.*)_before_typecast$/) && reader?($1)
|
179
|
+
end
|
180
|
+
|
181
|
+
def [](name)
|
182
|
+
read_attribute(name)
|
183
|
+
end
|
184
|
+
|
185
|
+
def []=(name, value)
|
186
|
+
write_attribute(name, value)
|
187
|
+
end
|
188
|
+
|
189
|
+
def method_missing(method, *args, &block)
|
190
|
+
attribute = method.to_s
|
191
|
+
|
192
|
+
if reader?(attribute)
|
193
|
+
read_attribute(attribute)
|
194
|
+
elsif writer?(attribute)
|
195
|
+
write_attribute(attribute.chop, args[0])
|
196
|
+
elsif before_typecast_reader?(attribute)
|
197
|
+
read_attribute_before_typecast(attribute.gsub(/_before_typecast$/, ''))
|
198
|
+
else
|
199
|
+
super
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def ==(other)
|
204
|
+
other.is_a?(self.class) && attributes == other.attributes
|
205
|
+
end
|
206
|
+
|
207
|
+
def inspect
|
208
|
+
attributes_as_nice_string = defined_key_names.collect do |name|
|
209
|
+
"#{name}: #{read_attribute(name)}"
|
210
|
+
end.join(", ")
|
211
|
+
"#<#{self.class} #{attributes_as_nice_string}>"
|
212
|
+
end
|
213
|
+
|
214
|
+
alias :respond_to_without_attributes? :respond_to?
|
215
|
+
|
216
|
+
def respond_to?(method, include_private=false)
|
217
|
+
return true if reader?(method) || writer?(method) || before_typecast_reader?(method)
|
218
|
+
super
|
219
|
+
end
|
220
|
+
|
221
|
+
private
|
222
|
+
def value_for_key(key)
|
223
|
+
if key.native?
|
224
|
+
read_attribute(key.name)
|
225
|
+
else
|
226
|
+
embedded_document = read_attribute(key.name)
|
227
|
+
embedded_document && embedded_document.attributes
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def read_attribute(name)
|
232
|
+
defined_key(name).get(instance_variable_get("@#{name}"))
|
233
|
+
end
|
234
|
+
|
235
|
+
def read_attribute_before_typecast(name)
|
236
|
+
instance_variable_get("@#{name}_before_typecast")
|
237
|
+
end
|
238
|
+
|
239
|
+
def write_attribute(name, value)
|
240
|
+
instance_variable_set "@#{name}_before_typecast", value
|
241
|
+
key = defined_key(name)
|
242
|
+
result = instance_variable_set "@#{name}", key.set(value)
|
243
|
+
if key.type.include?(MongoMapper::EmbeddedDocument) && !key.type.include?(MongoMapper::Document)
|
244
|
+
attribute_val = read_attribute(name)
|
245
|
+
attribute_val.instance_variable_set("@_root", self._root)
|
246
|
+
attribute_val.instance_variable_set("@_parent", self)
|
247
|
+
attribute_val.instance_variable_set("@_parent_key", name)
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
def defined_key(name)
|
252
|
+
self.class.keys[name]
|
253
|
+
end
|
254
|
+
|
255
|
+
def defined_key_names
|
256
|
+
self.class.keys.keys
|
257
|
+
end
|
258
|
+
|
259
|
+
def only_defined_keys(hash={})
|
260
|
+
defined_key_names = defined_key_names()
|
261
|
+
hash.delete_if { |k, v| !defined_key_names.include?(k.to_s) }
|
262
|
+
end
|
263
|
+
|
264
|
+
def embedded_association_attributes
|
265
|
+
embedded_attributes = HashWithIndifferentAccess.new
|
266
|
+
self.class.associations.each_pair do |name, association|
|
267
|
+
|
268
|
+
if association.type == :many && association.klass.embeddable?
|
269
|
+
if documents = instance_variable_get(association.ivar)
|
270
|
+
embedded_attributes[name] = documents.collect do |item|
|
271
|
+
attributes_hash = item.attributes
|
272
|
+
|
273
|
+
item.send(:embedded_association_attributes).each_pair do |association_name, association_value|
|
274
|
+
attributes_hash[association_name] = association_value
|
275
|
+
end
|
276
|
+
|
277
|
+
attributes_hash
|
278
|
+
end
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
embedded_attributes
|
284
|
+
end
|
285
|
+
|
286
|
+
def initialize_associations(attrs={})
|
287
|
+
self.class.associations.each_pair do |name, association|
|
288
|
+
if collection = attrs.delete(name)
|
289
|
+
__send__("#{association.name}=", collection)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
end
|
295
|
+
end
|