hashrocket-mongomapper 0.3.3
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 +70 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +73 -0
- data/VERSION +1 -0
- data/bin/mmconsole +56 -0
- data/lib/mongomapper.rb +70 -0
- data/lib/mongomapper/associations.rb +84 -0
- data/lib/mongomapper/associations/base.rb +69 -0
- data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
- data/lib/mongomapper/associations/belongs_to_proxy.rb +22 -0
- data/lib/mongomapper/associations/many_documents_proxy.rb +103 -0
- data/lib/mongomapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
- data/lib/mongomapper/associations/many_embedded_proxy.rb +17 -0
- data/lib/mongomapper/associations/many_polymorphic_proxy.rb +11 -0
- data/lib/mongomapper/associations/many_proxy.rb +6 -0
- data/lib/mongomapper/associations/proxy.rb +63 -0
- data/lib/mongomapper/callbacks.rb +106 -0
- data/lib/mongomapper/document.rb +337 -0
- data/lib/mongomapper/dynamic_finder.rb +38 -0
- data/lib/mongomapper/embedded_document.rb +267 -0
- data/lib/mongomapper/finder_options.rb +85 -0
- data/lib/mongomapper/key.rb +76 -0
- data/lib/mongomapper/observing.rb +50 -0
- data/lib/mongomapper/pagination.rb +52 -0
- data/lib/mongomapper/rails_compatibility/document.rb +15 -0
- data/lib/mongomapper/rails_compatibility/embedded_document.rb +25 -0
- data/lib/mongomapper/save_with_validation.rb +19 -0
- data/lib/mongomapper/serialization.rb +55 -0
- data/lib/mongomapper/serializers/json_serializer.rb +92 -0
- data/lib/mongomapper/support.rb +30 -0
- data/lib/mongomapper/validations.rb +61 -0
- data/mongomapper.gemspec +142 -0
- data/test/NOTE_ON_TESTING +1 -0
- data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +53 -0
- data/test/functional/associations/test_belongs_to_proxy.rb +45 -0
- data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +131 -0
- data/test/functional/associations/test_many_embedded_proxy.rb +106 -0
- data/test/functional/associations/test_many_polymorphic_proxy.rb +261 -0
- data/test/functional/associations/test_many_proxy.rb +295 -0
- data/test/functional/test_associations.rb +47 -0
- data/test/functional/test_callbacks.rb +85 -0
- data/test/functional/test_document.rb +952 -0
- data/test/functional/test_pagination.rb +81 -0
- data/test/functional/test_rails_compatibility.rb +30 -0
- data/test/functional/test_validations.rb +172 -0
- data/test/models.rb +139 -0
- data/test/test_helper.rb +67 -0
- data/test/unit/serializers/test_json_serializer.rb +157 -0
- data/test/unit/test_association_base.rb +144 -0
- data/test/unit/test_document.rb +123 -0
- data/test/unit/test_embedded_document.rb +526 -0
- data/test/unit/test_finder_options.rb +183 -0
- data/test/unit/test_key.rb +247 -0
- data/test/unit/test_mongomapper.rb +28 -0
- data/test/unit/test_observing.rb +101 -0
- data/test/unit/test_pagination.rb +113 -0
- data/test/unit/test_rails_compatibility.rb +34 -0
- data/test/unit/test_serializations.rb +52 -0
- data/test/unit/test_validations.rb +500 -0
- metadata +189 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
class DynamicFinder
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def initialize(model, method)
|
6
|
+
@model = model
|
7
|
+
@options = {}
|
8
|
+
@options[:method] = method
|
9
|
+
match
|
10
|
+
end
|
11
|
+
|
12
|
+
def valid?
|
13
|
+
@options[:finder].present?
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
def match
|
18
|
+
@options[:finder] = :first
|
19
|
+
|
20
|
+
case @options[:method].to_s
|
21
|
+
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
22
|
+
@options[:finder] = :last if $1 == 'last_by'
|
23
|
+
@options[:finder] = :all if $1 == 'all_by'
|
24
|
+
names = $2
|
25
|
+
when /^find_by_([_a-zA-Z]\w*)\!$/
|
26
|
+
@options[:bang] = true
|
27
|
+
names = $1
|
28
|
+
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
29
|
+
@options[:instantiator] = $1 == 'initialize' ? :new : :create
|
30
|
+
names = $2
|
31
|
+
else
|
32
|
+
@options[:finder] = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
@options[:attribute_names] = names && names.split('_and_')
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'observer'
|
2
|
+
|
3
|
+
module MongoMapper
|
4
|
+
module EmbeddedDocument
|
5
|
+
def self.included(model)
|
6
|
+
model.class_eval do
|
7
|
+
extend ClassMethods
|
8
|
+
include InstanceMethods
|
9
|
+
|
10
|
+
extend Associations::ClassMethods
|
11
|
+
include Associations::InstanceMethods
|
12
|
+
|
13
|
+
include RailsCompatibility::EmbeddedDocument
|
14
|
+
include Validatable
|
15
|
+
include Serialization
|
16
|
+
|
17
|
+
extend Validations::Macros
|
18
|
+
|
19
|
+
key :_id, String
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module ClassMethods
|
24
|
+
def inherited(subclass)
|
25
|
+
unless subclass.embeddable?
|
26
|
+
subclass.collection(self.collection.name)
|
27
|
+
end
|
28
|
+
|
29
|
+
(@subclasses ||= []) << subclass
|
30
|
+
end
|
31
|
+
|
32
|
+
def subclasses
|
33
|
+
@subclasses
|
34
|
+
end
|
35
|
+
|
36
|
+
def keys
|
37
|
+
@keys ||= if parent = parent_model
|
38
|
+
parent.keys.dup
|
39
|
+
else
|
40
|
+
HashWithIndifferentAccess.new
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def key(*args)
|
45
|
+
key = Key.new(*args)
|
46
|
+
|
47
|
+
if keys[key.name].blank?
|
48
|
+
keys[key.name] = key
|
49
|
+
|
50
|
+
create_accessors_for(key)
|
51
|
+
add_to_subclasses(*args)
|
52
|
+
apply_validations_for(key)
|
53
|
+
create_indexes_for(key)
|
54
|
+
|
55
|
+
key
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def add_to_subclasses(*args)
|
60
|
+
return if subclasses.blank?
|
61
|
+
|
62
|
+
subclasses.each do |subclass|
|
63
|
+
subclass.key(*args)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def ensure_index(name_or_array, options={})
|
68
|
+
keys_to_index = if name_or_array.is_a?(Array)
|
69
|
+
name_or_array.map { |pair| [pair[0], pair[1]] }
|
70
|
+
else
|
71
|
+
name_or_array
|
72
|
+
end
|
73
|
+
|
74
|
+
collection.create_index(keys_to_index, options.delete(:unique))
|
75
|
+
end
|
76
|
+
|
77
|
+
def embeddable?
|
78
|
+
!self.ancestors.include?(Document)
|
79
|
+
end
|
80
|
+
|
81
|
+
def parent_model
|
82
|
+
(ancestors - [self,EmbeddedDocument]).find do |parent_class|
|
83
|
+
parent_class.ancestors.include?(EmbeddedDocument)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def accessors_module
|
89
|
+
if const_defined?('MongoMapperKeys') && constants.include?( 'MongoMapperKeys' )
|
90
|
+
const_get 'MongoMapperKeys'
|
91
|
+
else
|
92
|
+
const_set 'MongoMapperKeys', Module.new
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def create_accessors_for(key)
|
97
|
+
accessors_module.module_eval <<-end_eval
|
98
|
+
def #{key.name}
|
99
|
+
read_attribute( :'#{key.name}' )
|
100
|
+
end
|
101
|
+
|
102
|
+
def #{key.name}_before_typecast
|
103
|
+
read_attribute_before_typecast(:'#{key.name}')
|
104
|
+
end
|
105
|
+
|
106
|
+
def #{key.name}=(value)
|
107
|
+
write_attribute(:'#{key.name}', value)
|
108
|
+
end
|
109
|
+
|
110
|
+
def #{key.name}?
|
111
|
+
read_attribute(:#{key.name}).present?
|
112
|
+
end
|
113
|
+
end_eval
|
114
|
+
include accessors_module
|
115
|
+
end
|
116
|
+
|
117
|
+
def create_indexes_for(key)
|
118
|
+
ensure_index key.name if key.options[:index]
|
119
|
+
end
|
120
|
+
|
121
|
+
def apply_validations_for(key)
|
122
|
+
attribute = key.name.to_sym
|
123
|
+
|
124
|
+
if key.options[:required]
|
125
|
+
validates_presence_of(attribute)
|
126
|
+
end
|
127
|
+
|
128
|
+
if key.options[:unique]
|
129
|
+
validates_uniqueness_of(attribute)
|
130
|
+
end
|
131
|
+
|
132
|
+
if key.options[:numeric]
|
133
|
+
number_options = key.type == Integer ? {:only_integer => true} : {}
|
134
|
+
validates_numericality_of(attribute, number_options)
|
135
|
+
end
|
136
|
+
|
137
|
+
if key.options[:format]
|
138
|
+
validates_format_of(attribute, :with => key.options[:format])
|
139
|
+
end
|
140
|
+
|
141
|
+
if key.options[:length]
|
142
|
+
length_options = case key.options[:length]
|
143
|
+
when Integer
|
144
|
+
{:minimum => 0, :maximum => key.options[:length]}
|
145
|
+
when Range
|
146
|
+
{:within => key.options[:length]}
|
147
|
+
when Hash
|
148
|
+
key.options[:length]
|
149
|
+
end
|
150
|
+
validates_length_of(attribute, length_options)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
module InstanceMethods
|
156
|
+
def initialize(attrs={})
|
157
|
+
unless attrs.nil?
|
158
|
+
self.class.associations.each_pair do |name, association|
|
159
|
+
if collection = attrs.delete(name)
|
160
|
+
send("#{association.name}=", collection)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
self.attributes = attrs
|
165
|
+
end
|
166
|
+
|
167
|
+
if self.class.embeddable? && read_attribute(:_id).blank?
|
168
|
+
write_attribute :_id, XGen::Mongo::Driver::ObjectID.new.to_s
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def attributes=(attrs)
|
173
|
+
return if attrs.blank?
|
174
|
+
attrs.each_pair do |name, value|
|
175
|
+
writer_method = "#{name}="
|
176
|
+
|
177
|
+
if respond_to?(writer_method)
|
178
|
+
self.send(writer_method, value)
|
179
|
+
else
|
180
|
+
self[name.to_s] = value
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def attributes
|
186
|
+
attrs = HashWithIndifferentAccess.new
|
187
|
+
self.class.keys.each_pair do |name, key|
|
188
|
+
value =
|
189
|
+
if key.native?
|
190
|
+
read_attribute(key.name)
|
191
|
+
else
|
192
|
+
if embedded_document = read_attribute(key.name)
|
193
|
+
embedded_document.attributes
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
attrs[name] = value unless value.nil?
|
198
|
+
end
|
199
|
+
attrs.merge!(embedded_association_attributes)
|
200
|
+
end
|
201
|
+
|
202
|
+
def [](name)
|
203
|
+
read_attribute(name)
|
204
|
+
end
|
205
|
+
|
206
|
+
def []=(name, value)
|
207
|
+
ensure_key_exists(name)
|
208
|
+
write_attribute(name, value)
|
209
|
+
end
|
210
|
+
|
211
|
+
def ==(other)
|
212
|
+
other.is_a?(self.class) && id == other.id
|
213
|
+
end
|
214
|
+
|
215
|
+
def id
|
216
|
+
read_attribute(:_id)
|
217
|
+
end
|
218
|
+
|
219
|
+
def id=(value)
|
220
|
+
@using_custom_id = true
|
221
|
+
write_attribute :_id, value
|
222
|
+
end
|
223
|
+
|
224
|
+
def using_custom_id?
|
225
|
+
!!@using_custom_id
|
226
|
+
end
|
227
|
+
|
228
|
+
def inspect
|
229
|
+
attributes_as_nice_string = self.class.keys.keys.collect do |name|
|
230
|
+
"#{name}: #{read_attribute(name)}"
|
231
|
+
end.join(", ")
|
232
|
+
"#<#{self.class} #{attributes_as_nice_string}>"
|
233
|
+
end
|
234
|
+
|
235
|
+
private
|
236
|
+
def ensure_key_exists(name)
|
237
|
+
self.class.key(name) unless respond_to?("#{name}=")
|
238
|
+
end
|
239
|
+
|
240
|
+
def read_attribute(name)
|
241
|
+
value = self.class.keys[name].get(instance_variable_get("@#{name}"))
|
242
|
+
instance_variable_set "@#{name}", value if !frozen?
|
243
|
+
value
|
244
|
+
end
|
245
|
+
|
246
|
+
def read_attribute_before_typecast(name)
|
247
|
+
instance_variable_get("@#{name}_before_typecast")
|
248
|
+
end
|
249
|
+
|
250
|
+
def write_attribute(name, value)
|
251
|
+
instance_variable_set "@#{name}_before_typecast", value
|
252
|
+
instance_variable_set "@#{name}", self.class.keys[name].set(value)
|
253
|
+
end
|
254
|
+
|
255
|
+
def embedded_association_attributes
|
256
|
+
returning HashWithIndifferentAccess.new do |attrs|
|
257
|
+
self.class.associations.each_pair do |name, association|
|
258
|
+
next unless association.embeddable?
|
259
|
+
next unless documents = instance_variable_get(association.ivar)
|
260
|
+
|
261
|
+
attrs[name] = documents.collect { |doc| doc.attributes }
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end # InstanceMethods
|
266
|
+
end # EmbeddedDocument
|
267
|
+
end # MongoMapper
|
@@ -0,0 +1,85 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
class FinderOptions
|
3
|
+
attr_reader :options
|
4
|
+
|
5
|
+
def self.to_mongo_criteria(conditions, parent_key=nil)
|
6
|
+
criteria = {}
|
7
|
+
conditions.each_pair do |field, value|
|
8
|
+
case value
|
9
|
+
when Array
|
10
|
+
operator_present = field.to_s =~ /^\$/
|
11
|
+
criteria[field] = if operator_present
|
12
|
+
value
|
13
|
+
else
|
14
|
+
{'$in' => value}
|
15
|
+
end
|
16
|
+
when Hash
|
17
|
+
criteria[field] = to_mongo_criteria(value, field)
|
18
|
+
else
|
19
|
+
criteria[field] = value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
criteria
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.to_mongo_options(options)
|
27
|
+
options = options.dup
|
28
|
+
{
|
29
|
+
:fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
|
30
|
+
:offset => (options.delete(:offset) || 0).to_i,
|
31
|
+
:limit => (options.delete(:limit) || 0).to_i,
|
32
|
+
:sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
|
33
|
+
}
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize(options)
|
37
|
+
raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
|
38
|
+
@options = options.symbolize_keys
|
39
|
+
@conditions = @options.delete(:conditions) || {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def criteria
|
43
|
+
self.class.to_mongo_criteria(@conditions)
|
44
|
+
end
|
45
|
+
|
46
|
+
def options
|
47
|
+
self.class.to_mongo_options(@options)
|
48
|
+
end
|
49
|
+
|
50
|
+
def to_a
|
51
|
+
[criteria, options]
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
def self.to_mongo_fields(fields)
|
56
|
+
return if fields.blank?
|
57
|
+
|
58
|
+
if fields.is_a?(String)
|
59
|
+
fields.split(',').map { |field| field.strip }
|
60
|
+
else
|
61
|
+
fields.flatten.compact
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.to_mongo_sort(sort)
|
66
|
+
return if sort.blank?
|
67
|
+
pieces = sort.split(',')
|
68
|
+
pairs = pieces.map { |s| to_mongo_sort_piece(s) }
|
69
|
+
|
70
|
+
hash = OrderedHash.new
|
71
|
+
pairs.each do |pair|
|
72
|
+
field, sort_direction = pair
|
73
|
+
hash[field] = sort_direction
|
74
|
+
end
|
75
|
+
hash.symbolize_keys
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.to_mongo_sort_piece(str)
|
79
|
+
field, direction = str.strip.split(' ')
|
80
|
+
direction ||= 'ASC'
|
81
|
+
direction = direction.upcase == 'ASC' ? 1 : -1
|
82
|
+
[field, direction]
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module MongoMapper
|
2
|
+
class Key
|
3
|
+
# DateTime and Date are currently not supported by mongo's bson so just use Time
|
4
|
+
NativeTypes = [String, Float, Time, Integer, Boolean, Array, Hash]
|
5
|
+
|
6
|
+
attr_accessor :name, :type, :options, :default_value
|
7
|
+
|
8
|
+
def initialize(*args)
|
9
|
+
options = args.extract_options!
|
10
|
+
@name, @type = args.shift.to_s, args.shift
|
11
|
+
self.options = (options || {}).symbolize_keys
|
12
|
+
self.default_value = self.options.delete(:default)
|
13
|
+
end
|
14
|
+
|
15
|
+
def ==(other)
|
16
|
+
@name == other.name && @type == other.type
|
17
|
+
end
|
18
|
+
|
19
|
+
def set(value)
|
20
|
+
typecast(value)
|
21
|
+
end
|
22
|
+
|
23
|
+
def native?
|
24
|
+
@native ||= NativeTypes.include?(type) || type.nil?
|
25
|
+
end
|
26
|
+
|
27
|
+
def embedded_document?
|
28
|
+
type.respond_to?(:embeddable?) && type.embeddable?
|
29
|
+
end
|
30
|
+
|
31
|
+
def get(value)
|
32
|
+
return default_value if value.nil? && !default_value.nil?
|
33
|
+
if type == Array
|
34
|
+
value || []
|
35
|
+
elsif type == Hash
|
36
|
+
HashWithIndifferentAccess.new(value || {})
|
37
|
+
else
|
38
|
+
value
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def typecast(value)
|
44
|
+
return value if type.nil?
|
45
|
+
return HashWithIndifferentAccess.new(value) if value.is_a?(Hash) && type == Hash
|
46
|
+
return value.utc if type == Time && value.kind_of?(type)
|
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).utc
|
53
|
+
elsif type == Boolean then Boolean.mm_typecast(value)
|
54
|
+
elsif type == Integer
|
55
|
+
# ganked from datamapper
|
56
|
+
value_to_i = value.to_i
|
57
|
+
if value_to_i == 0
|
58
|
+
value.to_s =~ /^(0x|0b)?0+/ ? 0 : nil
|
59
|
+
else
|
60
|
+
value_to_i
|
61
|
+
end
|
62
|
+
elsif embedded_document?
|
63
|
+
typecast_embedded_document(value)
|
64
|
+
else
|
65
|
+
value
|
66
|
+
end
|
67
|
+
rescue
|
68
|
+
value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def typecast_embedded_document(value)
|
73
|
+
value.is_a?(type) ? value : type.new(value)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|