djsun-mongomapper 0.3.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 +51 -0
- data/LICENSE +20 -0
- data/README.rdoc +39 -0
- data/Rakefile +71 -0
- data/VERSION +1 -0
- data/bin/mmconsole +56 -0
- data/lib/mongomapper.rb +96 -0
- data/lib/mongomapper/associations.rb +61 -0
- data/lib/mongomapper/associations/base.rb +71 -0
- data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +32 -0
- data/lib/mongomapper/associations/belongs_to_proxy.rb +22 -0
- data/lib/mongomapper/associations/many_documents_proxy.rb +85 -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 +67 -0
- data/lib/mongomapper/callbacks.rb +106 -0
- data/lib/mongomapper/document.rb +278 -0
- data/lib/mongomapper/embedded_document.rb +237 -0
- data/lib/mongomapper/finder_options.rb +96 -0
- data/lib/mongomapper/key.rb +80 -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 +79 -0
- data/lib/mongomapper/validations.rb +47 -0
- data/mongomapper.gemspec +139 -0
- data/test/NOTE_ON_TESTING +1 -0
- data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +39 -0
- data/test/functional/associations/test_belongs_to_proxy.rb +35 -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 +267 -0
- data/test/functional/associations/test_many_proxy.rb +236 -0
- data/test/functional/test_associations.rb +40 -0
- data/test/functional/test_callbacks.rb +85 -0
- data/test/functional/test_document.rb +691 -0
- data/test/functional/test_pagination.rb +81 -0
- data/test/functional/test_rails_compatibility.rb +31 -0
- data/test/functional/test_validations.rb +172 -0
- data/test/models.rb +108 -0
- data/test/test_helper.rb +67 -0
- data/test/unit/serializers/test_json_serializer.rb +103 -0
- data/test/unit/test_association_base.rb +136 -0
- data/test/unit/test_document.rb +125 -0
- data/test/unit/test_embedded_document.rb +370 -0
- data/test/unit/test_finder_options.rb +214 -0
- data/test/unit/test_key.rb +217 -0
- data/test/unit/test_mongo_id.rb +35 -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 +259 -0
- metadata +189 -0
@@ -0,0 +1,237 @@
|
|
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
|
+
key :_id, MongoID
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def inherited(subclass)
|
23
|
+
unless subclass.embeddable?
|
24
|
+
subclass.collection(self.collection.name)
|
25
|
+
end
|
26
|
+
|
27
|
+
(@subclasses ||= []) << subclass
|
28
|
+
end
|
29
|
+
|
30
|
+
def subclasses
|
31
|
+
@subclasses
|
32
|
+
end
|
33
|
+
|
34
|
+
def keys
|
35
|
+
@keys ||= if parent = parent_model
|
36
|
+
parent.keys.dup
|
37
|
+
else
|
38
|
+
HashWithIndifferentAccess.new
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def key(name, type, options={})
|
43
|
+
key = Key.new(name, type, options)
|
44
|
+
keys[key.name] = key
|
45
|
+
|
46
|
+
create_accessors_for(key)
|
47
|
+
add_to_subclasses(name, type, options)
|
48
|
+
apply_validations_for(key)
|
49
|
+
create_indexes_for(key)
|
50
|
+
|
51
|
+
key
|
52
|
+
end
|
53
|
+
|
54
|
+
def create_accessors_for(key)
|
55
|
+
define_method(key.name) do
|
56
|
+
read_attribute(key.name)
|
57
|
+
end
|
58
|
+
|
59
|
+
define_method("#{key.name}_before_typecast") do
|
60
|
+
read_attribute_before_typecast(key.name)
|
61
|
+
end
|
62
|
+
|
63
|
+
define_method("#{key.name}=") do |value|
|
64
|
+
write_attribute(key.name, value)
|
65
|
+
end
|
66
|
+
|
67
|
+
define_method("#{key.name}?") do
|
68
|
+
read_attribute(key.name).present?
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def add_to_subclasses(name, type, options)
|
73
|
+
return if subclasses.blank?
|
74
|
+
|
75
|
+
subclasses.each do |subclass|
|
76
|
+
subclass.key name, type, options
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def ensure_index(name_or_array, options={})
|
81
|
+
keys_to_index = if name_or_array.is_a?(Array)
|
82
|
+
name_or_array.map { |pair| [pair[0], pair[1]] }
|
83
|
+
else
|
84
|
+
name_or_array
|
85
|
+
end
|
86
|
+
|
87
|
+
collection.create_index(keys_to_index, options.delete(:unique))
|
88
|
+
end
|
89
|
+
|
90
|
+
def embeddable?
|
91
|
+
!self.ancestors.include?(Document)
|
92
|
+
end
|
93
|
+
|
94
|
+
def parent_model
|
95
|
+
if parent = ancestors[1]
|
96
|
+
parent if parent.ancestors.include?(EmbeddedDocument)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
def create_indexes_for(key)
|
102
|
+
ensure_index key.name if key.options[:index]
|
103
|
+
end
|
104
|
+
|
105
|
+
def apply_validations_for(key)
|
106
|
+
attribute = key.name.to_sym
|
107
|
+
|
108
|
+
if key.options[:required]
|
109
|
+
validates_presence_of(attribute)
|
110
|
+
end
|
111
|
+
|
112
|
+
if key.options[:unique]
|
113
|
+
validates_uniqueness_of(attribute)
|
114
|
+
end
|
115
|
+
|
116
|
+
if key.options[:numeric]
|
117
|
+
number_options = key.type == Integer ? {:only_integer => true} : {}
|
118
|
+
validates_numericality_of(attribute, number_options)
|
119
|
+
end
|
120
|
+
|
121
|
+
if key.options[:format]
|
122
|
+
validates_format_of(attribute, :with => key.options[:format])
|
123
|
+
end
|
124
|
+
|
125
|
+
if key.options[:length]
|
126
|
+
length_options = case key.options[:length]
|
127
|
+
when Integer
|
128
|
+
{:minimum => 0, :maximum => key.options[:length]}
|
129
|
+
when Range
|
130
|
+
{:within => key.options[:length]}
|
131
|
+
when Hash
|
132
|
+
key.options[:length]
|
133
|
+
end
|
134
|
+
validates_length_of(attribute, length_options)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
module InstanceMethods
|
140
|
+
def initialize(attrs={})
|
141
|
+
unless attrs.nil?
|
142
|
+
self.class.associations.each_pair do |name, association|
|
143
|
+
if collection = attrs.delete(name)
|
144
|
+
send("#{association.name}=", collection)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
self.attributes = attrs
|
149
|
+
end
|
150
|
+
|
151
|
+
if self.class.embeddable? && read_attribute(:_id).blank?
|
152
|
+
write_attribute :_id, XGen::Mongo::Driver::ObjectID.new
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
def attributes=(attrs)
|
157
|
+
return if attrs.blank?
|
158
|
+
attrs.each_pair do |method, value|
|
159
|
+
self.send("#{method}=", value)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def attributes
|
164
|
+
returning HashWithIndifferentAccess.new do |attributes|
|
165
|
+
self.class.keys.each_pair do |name, key|
|
166
|
+
value = value_for_key(key)
|
167
|
+
attributes[name] = value unless value.nil?
|
168
|
+
end
|
169
|
+
|
170
|
+
attributes.merge!(embedded_association_attributes)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def [](name)
|
175
|
+
read_attribute(name)
|
176
|
+
end
|
177
|
+
|
178
|
+
def []=(name, value)
|
179
|
+
write_attribute(name, value)
|
180
|
+
end
|
181
|
+
|
182
|
+
def ==(other)
|
183
|
+
other.is_a?(self.class) && id == other.id
|
184
|
+
end
|
185
|
+
|
186
|
+
def id
|
187
|
+
self._id.to_s
|
188
|
+
end
|
189
|
+
|
190
|
+
def id=(value)
|
191
|
+
self._id = value
|
192
|
+
end
|
193
|
+
|
194
|
+
def inspect
|
195
|
+
attributes_as_nice_string = self.class.keys.keys.collect do |name|
|
196
|
+
"#{name}: #{read_attribute(name)}"
|
197
|
+
end.join(", ")
|
198
|
+
"#<#{self.class} #{attributes_as_nice_string}>"
|
199
|
+
end
|
200
|
+
|
201
|
+
private
|
202
|
+
def value_for_key(key)
|
203
|
+
if key.native?
|
204
|
+
read_attribute(key.name)
|
205
|
+
else
|
206
|
+
embedded_document = read_attribute(key.name)
|
207
|
+
embedded_document && embedded_document.attributes
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def read_attribute(name)
|
212
|
+
val = self.class.keys[name].get(instance_variable_get("@#{name}"))
|
213
|
+
instance_variable_set "@#{name}", val
|
214
|
+
end
|
215
|
+
|
216
|
+
def read_attribute_before_typecast(name)
|
217
|
+
instance_variable_get("@#{name}_before_typecast")
|
218
|
+
end
|
219
|
+
|
220
|
+
def write_attribute(name, value)
|
221
|
+
instance_variable_set "@#{name}_before_typecast", value
|
222
|
+
instance_variable_set "@#{name}", self.class.keys[name].set(value)
|
223
|
+
end
|
224
|
+
|
225
|
+
def embedded_association_attributes
|
226
|
+
returning HashWithIndifferentAccess.new do |attrs|
|
227
|
+
self.class.associations.each_pair do |name, association|
|
228
|
+
next unless association.embeddable?
|
229
|
+
next unless documents = instance_variable_get(association.ivar)
|
230
|
+
|
231
|
+
attrs[name] = documents.collect { |doc| doc.attributes }
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end # InstanceMethods
|
236
|
+
end # EmbeddedDocument
|
237
|
+
end # MongoMapper
|
@@ -0,0 +1,96 @@
|
|
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
|
+
|
12
|
+
dealing_with_ids = field.to_s == '_id' ||
|
13
|
+
(parent_key && parent_key.to_s == '_id')
|
14
|
+
|
15
|
+
criteria[field] = if dealing_with_ids
|
16
|
+
ids = value.map { |id| MongoID.mm_typecast(id) }
|
17
|
+
operator_present ? ids : {'$in' => ids}
|
18
|
+
elsif operator_present
|
19
|
+
value
|
20
|
+
else
|
21
|
+
{'$in' => value}
|
22
|
+
end
|
23
|
+
when Hash
|
24
|
+
criteria[field] = to_mongo_criteria(value, field)
|
25
|
+
else
|
26
|
+
if field.to_s == '_id'
|
27
|
+
value = MongoID.mm_typecast(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
criteria[field] = value
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
criteria
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.to_mongo_options(options)
|
38
|
+
options = options.dup
|
39
|
+
{
|
40
|
+
:fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
|
41
|
+
:offset => (options.delete(:offset) || 0).to_i,
|
42
|
+
:limit => (options.delete(:limit) || 0).to_i,
|
43
|
+
:sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize(options)
|
48
|
+
raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
|
49
|
+
@options = options.symbolize_keys
|
50
|
+
@conditions = @options.delete(:conditions) || {}
|
51
|
+
end
|
52
|
+
|
53
|
+
def criteria
|
54
|
+
self.class.to_mongo_criteria(@conditions)
|
55
|
+
end
|
56
|
+
|
57
|
+
def options
|
58
|
+
self.class.to_mongo_options(@options)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_a
|
62
|
+
[criteria, options]
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def self.to_mongo_fields(fields)
|
67
|
+
return if fields.blank?
|
68
|
+
|
69
|
+
if fields.is_a?(String)
|
70
|
+
fields.split(',').map { |field| field.strip }
|
71
|
+
else
|
72
|
+
fields.flatten.compact
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def self.to_mongo_sort(sort)
|
77
|
+
return if sort.blank?
|
78
|
+
pieces = sort.split(',')
|
79
|
+
pairs = pieces.map { |s| to_mongo_sort_piece(s) }
|
80
|
+
|
81
|
+
hash = OrderedHash.new
|
82
|
+
pairs.each do |pair|
|
83
|
+
field, sort_direction = pair
|
84
|
+
hash[field] = sort_direction
|
85
|
+
end
|
86
|
+
hash.symbolize_keys
|
87
|
+
end
|
88
|
+
|
89
|
+
def self.to_mongo_sort_piece(str)
|
90
|
+
field, direction = str.strip.split(' ')
|
91
|
+
direction ||= 'ASC'
|
92
|
+
direction = direction.upcase == 'ASC' ? 1 : -1
|
93
|
+
[field, direction]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,80 @@
|
|
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, MongoID]
|
5
|
+
|
6
|
+
attr_accessor :name, :type, :options, :default_value
|
7
|
+
|
8
|
+
def initialize(name, type, options={})
|
9
|
+
@name, @type = name.to_s, type
|
10
|
+
self.options = (options || {}).symbolize_keys
|
11
|
+
self.default_value = self.options.delete(:default)
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
@name == other.name && @type == other.type
|
16
|
+
end
|
17
|
+
|
18
|
+
def set(value)
|
19
|
+
typecast(value)
|
20
|
+
end
|
21
|
+
|
22
|
+
def native?
|
23
|
+
@native ||= NativeTypes.include?(type)
|
24
|
+
end
|
25
|
+
|
26
|
+
def embedded_document?
|
27
|
+
type.ancestors.include?(EmbeddedDocument) && !type.ancestors.include?(Document)
|
28
|
+
end
|
29
|
+
|
30
|
+
def get(value)
|
31
|
+
return default_value if value.nil? && !default_value.nil?
|
32
|
+
if type == Array
|
33
|
+
value || []
|
34
|
+
elsif type == Hash
|
35
|
+
HashWithIndifferentAccess.new(value || {})
|
36
|
+
else
|
37
|
+
value
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
def typecast(value)
|
43
|
+
return HashWithIndifferentAccess.new(value) if value.is_a?(Hash) && type == Hash
|
44
|
+
return value if value.kind_of?(type) || value.nil?
|
45
|
+
begin
|
46
|
+
if type == String then value.to_s
|
47
|
+
elsif type == Float then value.to_f
|
48
|
+
elsif type == Array then value.to_a
|
49
|
+
elsif type == Time then Time.parse(value.to_s)
|
50
|
+
elsif type == MongoID then MongoID.mm_typecast(value)
|
51
|
+
#elsif type == Date then Date.parse(value.to_s)
|
52
|
+
elsif type == Boolean then Boolean.mm_typecast(value)
|
53
|
+
elsif type == Integer
|
54
|
+
# ganked from datamapper
|
55
|
+
value_to_i = value.to_i
|
56
|
+
if value_to_i == 0 && value != '0'
|
57
|
+
value_to_s = value.to_s
|
58
|
+
begin
|
59
|
+
Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s)
|
60
|
+
rescue ArgumentError
|
61
|
+
nil
|
62
|
+
end
|
63
|
+
else
|
64
|
+
value_to_i
|
65
|
+
end
|
66
|
+
elsif embedded_document?
|
67
|
+
typecast_embedded_document(value)
|
68
|
+
else
|
69
|
+
value
|
70
|
+
end
|
71
|
+
rescue
|
72
|
+
value
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def typecast_embedded_document(value)
|
77
|
+
value.is_a?(type) ? value : type.new(value)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'observer'
|
2
|
+
require 'singleton'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module MongoMapper
|
6
|
+
module Observing #:nodoc:
|
7
|
+
def self.included(model)
|
8
|
+
model.class_eval do
|
9
|
+
extend Observable
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Observer
|
15
|
+
include Singleton
|
16
|
+
|
17
|
+
class << self
|
18
|
+
def observe(*models)
|
19
|
+
models.flatten!
|
20
|
+
models.collect! { |model| model.is_a?(Symbol) ? model.to_s.camelize.constantize : model }
|
21
|
+
define_method(:observed_classes) { Set.new(models) }
|
22
|
+
end
|
23
|
+
|
24
|
+
def observed_class
|
25
|
+
if observed_class_name = name[/(.*)Observer/, 1]
|
26
|
+
observed_class_name.constantize
|
27
|
+
else
|
28
|
+
nil
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def initialize
|
34
|
+
Set.new(observed_classes).each { |klass| add_observer! klass }
|
35
|
+
end
|
36
|
+
|
37
|
+
def update(observed_method, object) #:nodoc:
|
38
|
+
send(observed_method, object) if respond_to?(observed_method)
|
39
|
+
end
|
40
|
+
|
41
|
+
protected
|
42
|
+
def observed_classes
|
43
|
+
Set.new([self.class.observed_class].compact.flatten)
|
44
|
+
end
|
45
|
+
|
46
|
+
def add_observer!(klass)
|
47
|
+
klass.add_observer(self)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|