fcoury-mongomapper 0.2.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 +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 +70 -0
- data/lib/mongomapper/associations.rb +69 -0
- data/lib/mongomapper/associations/array_proxy.rb +6 -0
- data/lib/mongomapper/associations/base.rb +54 -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 +29 -0
- data/lib/mongomapper/associations/polymorphic_belongs_to_proxy.rb +31 -0
- data/lib/mongomapper/associations/proxy.rb +66 -0
- data/lib/mongomapper/callbacks.rb +106 -0
- data/lib/mongomapper/document.rb +276 -0
- data/lib/mongomapper/document_rails_compatibility.rb +13 -0
- data/lib/mongomapper/embedded_document.rb +248 -0
- data/lib/mongomapper/embedded_document_rails_compatibility.rb +22 -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/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 +444 -0
- data/test/test_callbacks.rb +84 -0
- data/test/test_document.rb +1002 -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 +73 -0
- data/test/test_serializations.rb +54 -0
- data/test/test_validations.rb +409 -0
- metadata +155 -0
@@ -0,0 +1,248 @@
|
|
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 EmbeddedDocumentRailsCompatibility
|
16
|
+
include Validatable
|
17
|
+
include Serialization
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def keys
|
23
|
+
@keys ||= if parent = parent_model
|
24
|
+
parent.keys.dup
|
25
|
+
else
|
26
|
+
HashWithIndifferentAccess.new
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def key(name, type, options={})
|
31
|
+
key = Key.new(name, type, options)
|
32
|
+
keys[key.name] = key
|
33
|
+
apply_validations_for(key)
|
34
|
+
create_indexes_for(key)
|
35
|
+
key
|
36
|
+
end
|
37
|
+
|
38
|
+
def ensure_index(name_or_array, options={})
|
39
|
+
keys_to_index = if name_or_array.is_a?(Array)
|
40
|
+
name_or_array.map { |pair| [pair[0], pair[1]] }
|
41
|
+
else
|
42
|
+
name_or_array
|
43
|
+
end
|
44
|
+
|
45
|
+
collection.create_index(keys_to_index, options.delete(:unique))
|
46
|
+
end
|
47
|
+
|
48
|
+
def embeddable?
|
49
|
+
!self.ancestors.include?(Document)
|
50
|
+
end
|
51
|
+
|
52
|
+
def parent_model
|
53
|
+
if parent = ancestors[1]
|
54
|
+
parent if parent.ancestors.include?(EmbeddedDocument)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
def create_indexes_for(key)
|
60
|
+
ensure_index key.name if key.options[:index]
|
61
|
+
end
|
62
|
+
|
63
|
+
def apply_validations_for(key)
|
64
|
+
attribute = key.name.to_sym
|
65
|
+
|
66
|
+
if key.options[:required]
|
67
|
+
validates_presence_of(attribute)
|
68
|
+
end
|
69
|
+
|
70
|
+
if key.options[:unique]
|
71
|
+
validates_uniqueness_of(attribute)
|
72
|
+
end
|
73
|
+
|
74
|
+
if key.options[:numeric]
|
75
|
+
number_options = key.type == Integer ? {:only_integer => true} : {}
|
76
|
+
validates_numericality_of(attribute, number_options)
|
77
|
+
end
|
78
|
+
|
79
|
+
if key.options[:format]
|
80
|
+
validates_format_of(attribute, :with => key.options[:format])
|
81
|
+
end
|
82
|
+
|
83
|
+
if key.options[:length]
|
84
|
+
length_options = case key.options[:length]
|
85
|
+
when Integer
|
86
|
+
{:minimum => 0, :maximum => key.options[:length]}
|
87
|
+
when Range
|
88
|
+
{:within => key.options[:length]}
|
89
|
+
when Hash
|
90
|
+
key.options[:length]
|
91
|
+
end
|
92
|
+
validates_length_of(attribute, length_options)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
end
|
97
|
+
|
98
|
+
module InstanceMethods
|
99
|
+
def initialize(attrs={})
|
100
|
+
unless attrs.nil?
|
101
|
+
initialize_associations(attrs)
|
102
|
+
self.attributes = attrs
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def attributes=(attrs)
|
107
|
+
return if attrs.blank?
|
108
|
+
attrs.each_pair do |key_name, value|
|
109
|
+
if writer?(key_name)
|
110
|
+
write_attribute(key_name, value)
|
111
|
+
else
|
112
|
+
writer_method ="#{key_name}="
|
113
|
+
self.send(writer_method, value) if respond_to?(writer_method)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def attributes
|
119
|
+
self.class.keys.inject(HashWithIndifferentAccess.new) do |attributes, key_hash|
|
120
|
+
name, key = key_hash
|
121
|
+
value = value_for_key(key)
|
122
|
+
attributes[name] = value unless value.nil?
|
123
|
+
attributes
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def reader?(name)
|
128
|
+
defined_key_names.include?(name.to_s)
|
129
|
+
end
|
130
|
+
|
131
|
+
def writer?(name)
|
132
|
+
name = name.to_s
|
133
|
+
name = name.chop if name.ends_with?('=')
|
134
|
+
reader?(name)
|
135
|
+
end
|
136
|
+
|
137
|
+
def before_typecast_reader?(name)
|
138
|
+
name.to_s.match(/^(.*)_before_typecast$/) && reader?($1)
|
139
|
+
end
|
140
|
+
|
141
|
+
def [](name)
|
142
|
+
read_attribute(name)
|
143
|
+
end
|
144
|
+
|
145
|
+
def []=(name, value)
|
146
|
+
write_attribute(name, value)
|
147
|
+
end
|
148
|
+
|
149
|
+
def method_missing(method, *args, &block)
|
150
|
+
attribute = method.to_s
|
151
|
+
|
152
|
+
if reader?(attribute)
|
153
|
+
read_attribute(attribute)
|
154
|
+
elsif writer?(attribute)
|
155
|
+
write_attribute(attribute.chop, args[0])
|
156
|
+
elsif before_typecast_reader?(attribute)
|
157
|
+
read_attribute_before_typecast(attribute.gsub(/_before_typecast$/, ''))
|
158
|
+
else
|
159
|
+
super
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def ==(other)
|
164
|
+
other.is_a?(self.class) && attributes == other.attributes
|
165
|
+
end
|
166
|
+
|
167
|
+
def inspect
|
168
|
+
attributes_as_nice_string = defined_key_names.collect do |name|
|
169
|
+
"#{name}: #{read_attribute(name)}"
|
170
|
+
end.join(", ")
|
171
|
+
"#<#{self.class} #{attributes_as_nice_string}>"
|
172
|
+
end
|
173
|
+
|
174
|
+
alias :respond_to_without_attributes? :respond_to?
|
175
|
+
|
176
|
+
def respond_to?(method, include_private=false)
|
177
|
+
return true if reader?(method) || writer?(method) || before_typecast_reader?(method)
|
178
|
+
super
|
179
|
+
end
|
180
|
+
|
181
|
+
private
|
182
|
+
def value_for_key(key)
|
183
|
+
if key.native?
|
184
|
+
read_attribute(key.name)
|
185
|
+
else
|
186
|
+
embedded_document = read_attribute(key.name)
|
187
|
+
embedded_document && embedded_document.attributes
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
def read_attribute(name)
|
192
|
+
defined_key(name).get(instance_variable_get("@#{name}"))
|
193
|
+
end
|
194
|
+
|
195
|
+
def read_attribute_before_typecast(name)
|
196
|
+
instance_variable_get("@#{name}_before_typecast")
|
197
|
+
end
|
198
|
+
|
199
|
+
def write_attribute(name, value)
|
200
|
+
instance_variable_set "@#{name}_before_typecast", value
|
201
|
+
instance_variable_set "@#{name}", defined_key(name).set(value)
|
202
|
+
end
|
203
|
+
|
204
|
+
def defined_key(name)
|
205
|
+
self.class.keys[name]
|
206
|
+
end
|
207
|
+
|
208
|
+
def defined_key_names
|
209
|
+
self.class.keys.keys
|
210
|
+
end
|
211
|
+
|
212
|
+
def only_defined_keys(hash={})
|
213
|
+
defined_key_names = defined_key_names()
|
214
|
+
hash.delete_if { |k, v| !defined_key_names.include?(k.to_s) }
|
215
|
+
end
|
216
|
+
|
217
|
+
def embedded_association_attributes
|
218
|
+
embedded_attributes = HashWithIndifferentAccess.new
|
219
|
+
self.class.associations.each_pair do |name, association|
|
220
|
+
|
221
|
+
if association.type == :many && association.klass.embeddable?
|
222
|
+
if documents = instance_variable_get(association.ivar)
|
223
|
+
embedded_attributes[name] = documents.collect do |item|
|
224
|
+
attributes_hash = item.attributes
|
225
|
+
|
226
|
+
item.send(:embedded_association_attributes).each_pair do |association_name, association_value|
|
227
|
+
attributes_hash[association_name] = association_value
|
228
|
+
end
|
229
|
+
|
230
|
+
attributes_hash
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
embedded_attributes
|
237
|
+
end
|
238
|
+
|
239
|
+
def initialize_associations(attrs={})
|
240
|
+
self.class.associations.each_pair do |name, association|
|
241
|
+
if collection = attrs.delete(name)
|
242
|
+
__send__("#{association.name}=", collection)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
module EmbeddedDocumentRailsCompatibility
|
3
|
+
def self.included(model)
|
4
|
+
model.class_eval do
|
5
|
+
extend ClassMethods
|
6
|
+
end
|
7
|
+
class << model
|
8
|
+
alias_method :has_many, :many
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
module ClassMethods
|
13
|
+
def column_names
|
14
|
+
keys.keys
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_param
|
19
|
+
raise "Missing to_param method in #{self.class.name}. You should implement it to return the unique identifier of this document within a collection."
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
class FinderOptions
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def self.to_mongo_criteria(conditions)
|
6
|
+
conditions = conditions.dup
|
7
|
+
criteria = {}
|
8
|
+
conditions.each_pair do |field, value|
|
9
|
+
case value
|
10
|
+
when Array
|
11
|
+
criteria[field] = {'$in' => value}
|
12
|
+
when Hash
|
13
|
+
criteria[field] = to_mongo_criteria(value)
|
14
|
+
else
|
15
|
+
criteria[field] = value
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
criteria
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.to_mongo_options(options)
|
23
|
+
options = options.dup
|
24
|
+
{
|
25
|
+
:fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
|
26
|
+
:offset => (options.delete(:offset) || 0).to_i,
|
27
|
+
:limit => (options.delete(:limit) || 0).to_i,
|
28
|
+
:sort => to_mongo_sort(options.delete(:order))
|
29
|
+
}
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(options)
|
33
|
+
raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
|
34
|
+
@options = options.symbolize_keys
|
35
|
+
@conditions = @options.delete(:conditions) || {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def criteria
|
39
|
+
self.class.to_mongo_criteria(@conditions)
|
40
|
+
end
|
41
|
+
|
42
|
+
def options
|
43
|
+
self.class.to_mongo_options(@options)
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_a
|
47
|
+
[criteria, options]
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
def self.to_mongo_fields(fields)
|
52
|
+
return if fields.blank?
|
53
|
+
|
54
|
+
if fields.is_a?(String)
|
55
|
+
fields.split(',').map { |field| field.strip }
|
56
|
+
else
|
57
|
+
fields.flatten.compact
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.to_mongo_sort(sort)
|
62
|
+
return if sort.blank?
|
63
|
+
pieces = sort.split(',')
|
64
|
+
pairs = pieces.map { |s| to_mongo_sort_piece(s) }
|
65
|
+
|
66
|
+
hash = OrderedHash.new
|
67
|
+
pairs.each do |pair|
|
68
|
+
field, sort_direction = pair
|
69
|
+
hash[field] = sort_direction
|
70
|
+
end
|
71
|
+
hash.symbolize_keys
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.to_mongo_sort_piece(str)
|
75
|
+
field, direction = str.strip.split(' ')
|
76
|
+
direction ||= 'ASC'
|
77
|
+
direction = direction.upcase == 'ASC' ? 1 : -1
|
78
|
+
[field, direction]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
class Boolean; end
|
2
|
+
class Ref; end
|
3
|
+
|
4
|
+
module MongoMapper
|
5
|
+
class Key
|
6
|
+
# DateTime and Date are currently not supported by mongo's bson so just use Time
|
7
|
+
NativeTypes = [String, Float, Time, Integer, Boolean, Array, Hash, Ref]
|
8
|
+
|
9
|
+
attr_accessor :name, :type, :options, :default_value
|
10
|
+
|
11
|
+
def initialize(name, type, options={})
|
12
|
+
@name, @type = name.to_s, type
|
13
|
+
self.options = options.symbolize_keys
|
14
|
+
self.default_value = options.delete(:default)
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
@name == other.name && @type == other.type
|
19
|
+
end
|
20
|
+
|
21
|
+
def set(value)
|
22
|
+
typecast(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def native?
|
26
|
+
@native ||= NativeTypes.include?(type)
|
27
|
+
end
|
28
|
+
|
29
|
+
def embedded_document?
|
30
|
+
type.ancestors.include?(EmbeddedDocument) && !type.ancestors.include?(Document)
|
31
|
+
end
|
32
|
+
|
33
|
+
def get(value)
|
34
|
+
return default_value if value.nil? && !default_value.nil?
|
35
|
+
if type == Array
|
36
|
+
value || []
|
37
|
+
elsif type == Hash
|
38
|
+
HashWithIndifferentAccess.new(value || {})
|
39
|
+
else
|
40
|
+
value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def typecast(value)
|
46
|
+
return HashWithIndifferentAccess.new(value) if value.is_a?(Hash) && type == Hash
|
47
|
+
return value if value.kind_of?(type) || value.nil?
|
48
|
+
begin
|
49
|
+
if type == String then value.to_s
|
50
|
+
elsif type == Float then value.to_f
|
51
|
+
elsif type == Array then value.to_a
|
52
|
+
elsif type == Time then Time.parse(value.to_s)
|
53
|
+
#elsif type == Date then Date.parse(value.to_s)
|
54
|
+
elsif type == Boolean then ['true', 't', '1'].include?(value.to_s.downcase)
|
55
|
+
elsif type == Integer
|
56
|
+
# ganked from datamapper
|
57
|
+
value_to_i = value.to_i
|
58
|
+
if value_to_i == 0 && value != '0'
|
59
|
+
value_to_s = value.to_s
|
60
|
+
begin
|
61
|
+
Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
|
62
|
+
rescue ArgumentError
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
else
|
66
|
+
value_to_i
|
67
|
+
end
|
68
|
+
elsif embedded_document?
|
69
|
+
typecast_embedded_document(value)
|
70
|
+
else
|
71
|
+
value
|
72
|
+
end
|
73
|
+
rescue
|
74
|
+
value
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def typecast_embedded_document(value)
|
79
|
+
value.is_a?(type) ? value : type.new(value)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|