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.
Files changed (42) hide show
  1. data/.gitignore +7 -0
  2. data/History +30 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +71 -0
  6. data/VERSION +1 -0
  7. data/lib/mongomapper.rb +70 -0
  8. data/lib/mongomapper/associations.rb +69 -0
  9. data/lib/mongomapper/associations/array_proxy.rb +6 -0
  10. data/lib/mongomapper/associations/base.rb +54 -0
  11. data/lib/mongomapper/associations/belongs_to_proxy.rb +26 -0
  12. data/lib/mongomapper/associations/has_many_embedded_proxy.rb +19 -0
  13. data/lib/mongomapper/associations/has_many_proxy.rb +29 -0
  14. data/lib/mongomapper/associations/polymorphic_belongs_to_proxy.rb +31 -0
  15. data/lib/mongomapper/associations/proxy.rb +66 -0
  16. data/lib/mongomapper/callbacks.rb +106 -0
  17. data/lib/mongomapper/document.rb +276 -0
  18. data/lib/mongomapper/document_rails_compatibility.rb +13 -0
  19. data/lib/mongomapper/embedded_document.rb +248 -0
  20. data/lib/mongomapper/embedded_document_rails_compatibility.rb +22 -0
  21. data/lib/mongomapper/finder_options.rb +81 -0
  22. data/lib/mongomapper/key.rb +82 -0
  23. data/lib/mongomapper/observing.rb +50 -0
  24. data/lib/mongomapper/save_with_validation.rb +19 -0
  25. data/lib/mongomapper/serialization.rb +55 -0
  26. data/lib/mongomapper/serializers/json_serializer.rb +77 -0
  27. data/lib/mongomapper/validations.rb +47 -0
  28. data/mongomapper.gemspec +105 -0
  29. data/test/serializers/test_json_serializer.rb +104 -0
  30. data/test/test_associations.rb +444 -0
  31. data/test/test_callbacks.rb +84 -0
  32. data/test/test_document.rb +1002 -0
  33. data/test/test_embedded_document.rb +253 -0
  34. data/test/test_finder_options.rb +148 -0
  35. data/test/test_helper.rb +62 -0
  36. data/test/test_key.rb +200 -0
  37. data/test/test_mongomapper.rb +28 -0
  38. data/test/test_observing.rb +101 -0
  39. data/test/test_rails_compatibility.rb +73 -0
  40. data/test/test_serializations.rb +54 -0
  41. data/test/test_validations.rb +409 -0
  42. metadata +155 -0
@@ -0,0 +1,13 @@
1
+ module MongoMapper
2
+ module DocumentRailsCompatibility
3
+ def self.included(model)
4
+ model.class_eval do
5
+ alias_method :new_record?, :new?
6
+ end
7
+ end
8
+
9
+ def to_param
10
+ id
11
+ end
12
+ end
13
+ end
@@ -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