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.
Files changed (62) hide show
  1. data/.gitignore +7 -0
  2. data/History +70 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +39 -0
  5. data/Rakefile +73 -0
  6. data/VERSION +1 -0
  7. data/bin/mmconsole +56 -0
  8. data/lib/mongomapper.rb +70 -0
  9. data/lib/mongomapper/associations.rb +84 -0
  10. data/lib/mongomapper/associations/base.rb +69 -0
  11. data/lib/mongomapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
  12. data/lib/mongomapper/associations/belongs_to_proxy.rb +22 -0
  13. data/lib/mongomapper/associations/many_documents_proxy.rb +103 -0
  14. data/lib/mongomapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  15. data/lib/mongomapper/associations/many_embedded_proxy.rb +17 -0
  16. data/lib/mongomapper/associations/many_polymorphic_proxy.rb +11 -0
  17. data/lib/mongomapper/associations/many_proxy.rb +6 -0
  18. data/lib/mongomapper/associations/proxy.rb +63 -0
  19. data/lib/mongomapper/callbacks.rb +106 -0
  20. data/lib/mongomapper/document.rb +337 -0
  21. data/lib/mongomapper/dynamic_finder.rb +38 -0
  22. data/lib/mongomapper/embedded_document.rb +267 -0
  23. data/lib/mongomapper/finder_options.rb +85 -0
  24. data/lib/mongomapper/key.rb +76 -0
  25. data/lib/mongomapper/observing.rb +50 -0
  26. data/lib/mongomapper/pagination.rb +52 -0
  27. data/lib/mongomapper/rails_compatibility/document.rb +15 -0
  28. data/lib/mongomapper/rails_compatibility/embedded_document.rb +25 -0
  29. data/lib/mongomapper/save_with_validation.rb +19 -0
  30. data/lib/mongomapper/serialization.rb +55 -0
  31. data/lib/mongomapper/serializers/json_serializer.rb +92 -0
  32. data/lib/mongomapper/support.rb +30 -0
  33. data/lib/mongomapper/validations.rb +61 -0
  34. data/mongomapper.gemspec +142 -0
  35. data/test/NOTE_ON_TESTING +1 -0
  36. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +53 -0
  37. data/test/functional/associations/test_belongs_to_proxy.rb +45 -0
  38. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +131 -0
  39. data/test/functional/associations/test_many_embedded_proxy.rb +106 -0
  40. data/test/functional/associations/test_many_polymorphic_proxy.rb +261 -0
  41. data/test/functional/associations/test_many_proxy.rb +295 -0
  42. data/test/functional/test_associations.rb +47 -0
  43. data/test/functional/test_callbacks.rb +85 -0
  44. data/test/functional/test_document.rb +952 -0
  45. data/test/functional/test_pagination.rb +81 -0
  46. data/test/functional/test_rails_compatibility.rb +30 -0
  47. data/test/functional/test_validations.rb +172 -0
  48. data/test/models.rb +139 -0
  49. data/test/test_helper.rb +67 -0
  50. data/test/unit/serializers/test_json_serializer.rb +157 -0
  51. data/test/unit/test_association_base.rb +144 -0
  52. data/test/unit/test_document.rb +123 -0
  53. data/test/unit/test_embedded_document.rb +526 -0
  54. data/test/unit/test_finder_options.rb +183 -0
  55. data/test/unit/test_key.rb +247 -0
  56. data/test/unit/test_mongomapper.rb +28 -0
  57. data/test/unit/test_observing.rb +101 -0
  58. data/test/unit/test_pagination.rb +113 -0
  59. data/test/unit/test_rails_compatibility.rb +34 -0
  60. data/test/unit/test_serializations.rb +52 -0
  61. data/test/unit/test_validations.rb +500 -0
  62. 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