mongo_mapper 0.5.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 (69) hide show
  1. data/.gitignore +7 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +39 -0
  4. data/Rakefile +87 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +55 -0
  7. data/lib/mongo_mapper.rb +92 -0
  8. data/lib/mongo_mapper/associations.rb +86 -0
  9. data/lib/mongo_mapper/associations/base.rb +83 -0
  10. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
  11. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
  12. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +27 -0
  13. data/lib/mongo_mapper/associations/many_documents_proxy.rb +116 -0
  14. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  15. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +67 -0
  16. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  17. data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
  18. data/lib/mongo_mapper/associations/proxy.rb +64 -0
  19. data/lib/mongo_mapper/callbacks.rb +106 -0
  20. data/lib/mongo_mapper/document.rb +317 -0
  21. data/lib/mongo_mapper/dynamic_finder.rb +35 -0
  22. data/lib/mongo_mapper/embedded_document.rb +354 -0
  23. data/lib/mongo_mapper/finder_options.rb +94 -0
  24. data/lib/mongo_mapper/key.rb +32 -0
  25. data/lib/mongo_mapper/observing.rb +50 -0
  26. data/lib/mongo_mapper/pagination.rb +51 -0
  27. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  28. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  29. data/lib/mongo_mapper/save_with_validation.rb +19 -0
  30. data/lib/mongo_mapper/serialization.rb +55 -0
  31. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  32. data/lib/mongo_mapper/support.rb +157 -0
  33. data/lib/mongo_mapper/validations.rb +69 -0
  34. data/mongo_mapper.gemspec +156 -0
  35. data/test/NOTE_ON_TESTING +1 -0
  36. data/test/custom_matchers.rb +48 -0
  37. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +54 -0
  38. data/test/functional/associations/test_belongs_to_proxy.rb +46 -0
  39. data/test/functional/associations/test_many_documents_as_proxy.rb +244 -0
  40. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +132 -0
  41. data/test/functional/associations/test_many_embedded_proxy.rb +174 -0
  42. data/test/functional/associations/test_many_polymorphic_proxy.rb +297 -0
  43. data/test/functional/associations/test_many_proxy.rb +331 -0
  44. data/test/functional/test_associations.rb +48 -0
  45. data/test/functional/test_binary.rb +18 -0
  46. data/test/functional/test_callbacks.rb +85 -0
  47. data/test/functional/test_document.rb +951 -0
  48. data/test/functional/test_embedded_document.rb +97 -0
  49. data/test/functional/test_pagination.rb +87 -0
  50. data/test/functional/test_rails_compatibility.rb +30 -0
  51. data/test/functional/test_validations.rb +279 -0
  52. data/test/models.rb +169 -0
  53. data/test/test_helper.rb +29 -0
  54. data/test/unit/serializers/test_json_serializer.rb +189 -0
  55. data/test/unit/test_association_base.rb +144 -0
  56. data/test/unit/test_document.rb +165 -0
  57. data/test/unit/test_dynamic_finder.rb +125 -0
  58. data/test/unit/test_embedded_document.rb +645 -0
  59. data/test/unit/test_finder_options.rb +193 -0
  60. data/test/unit/test_key.rb +163 -0
  61. data/test/unit/test_mongomapper.rb +28 -0
  62. data/test/unit/test_observing.rb +101 -0
  63. data/test/unit/test_pagination.rb +109 -0
  64. data/test/unit/test_rails_compatibility.rb +39 -0
  65. data/test/unit/test_serializations.rb +52 -0
  66. data/test/unit/test_support.rb +272 -0
  67. data/test/unit/test_time_zones.rb +40 -0
  68. data/test/unit/test_validations.rb +503 -0
  69. metadata +204 -0
@@ -0,0 +1,94 @@
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
+ field = field_normalized(field)
9
+ case value
10
+ when Array
11
+ operator_present = field.to_s =~ /^\$/
12
+ criteria[field] = if operator_present
13
+ value
14
+ else
15
+ {'$in' => value}
16
+ end
17
+ when Hash
18
+ criteria[field] = to_mongo_criteria(value, field)
19
+ else
20
+ criteria[field] = value
21
+ end
22
+ end
23
+
24
+ criteria
25
+ end
26
+
27
+ def self.to_mongo_options(options)
28
+ options = options.dup
29
+ {
30
+ :fields => to_mongo_fields(options.delete(:fields) || options.delete(:select)),
31
+ :skip => (options.delete(:skip) || options.delete(:offset) || 0).to_i,
32
+ :limit => (options.delete(:limit) || 0).to_i,
33
+ :sort => options.delete(:sort) || to_mongo_sort(options.delete(:order))
34
+ }
35
+ end
36
+
37
+ def self.field_normalized(field)
38
+ if field.to_s == 'id'
39
+ :_id
40
+ else
41
+ field
42
+ end
43
+ end
44
+
45
+ def initialize(options)
46
+ raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
47
+ @options = options.symbolize_keys
48
+ @conditions = @options.delete(:conditions) || {}
49
+ end
50
+
51
+ def criteria
52
+ self.class.to_mongo_criteria(@conditions)
53
+ end
54
+
55
+ def options
56
+ self.class.to_mongo_options(@options)
57
+ end
58
+
59
+ def to_a
60
+ [criteria, options]
61
+ end
62
+
63
+ private
64
+ def self.to_mongo_fields(fields)
65
+ return if fields.blank?
66
+
67
+ if fields.is_a?(String)
68
+ fields.split(',').map { |field| field.strip }
69
+ else
70
+ fields.flatten.compact
71
+ end
72
+ end
73
+
74
+ def self.to_mongo_sort(sort)
75
+ return if sort.blank?
76
+ pieces = sort.split(',')
77
+ pairs = pieces.map { |s| to_mongo_sort_piece(s) }
78
+
79
+ hash = OrderedHash.new
80
+ pairs.each do |pair|
81
+ field, sort_direction = pair
82
+ hash[field] = sort_direction
83
+ end
84
+ hash.symbolize_keys
85
+ end
86
+
87
+ def self.to_mongo_sort_piece(str)
88
+ field, direction = str.strip.split(' ')
89
+ direction ||= 'ASC'
90
+ direction = direction.upcase == 'ASC' ? 1 : -1
91
+ [field, direction]
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,32 @@
1
+ module MongoMapper
2
+ class Key
3
+ attr_accessor :name, :type, :options, :default_value
4
+
5
+ def initialize(*args)
6
+ options = args.extract_options!
7
+ @name, @type = args.shift.to_s, args.shift
8
+ self.options = (options || {}).symbolize_keys
9
+ self.default_value = self.options.delete(:default)
10
+ end
11
+
12
+ def ==(other)
13
+ @name == other.name && @type == other.type
14
+ end
15
+
16
+ def set(value)
17
+ type.to_mongo(value)
18
+ end
19
+
20
+ def embeddable?
21
+ type.respond_to?(:embeddable?) && type.embeddable? ? true : false
22
+ end
23
+
24
+ def get(value)
25
+ if value.nil? && !default_value.nil?
26
+ return default_value
27
+ end
28
+
29
+ type.from_mongo(value)
30
+ end
31
+ end
32
+ 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
@@ -0,0 +1,51 @@
1
+ module MongoMapper
2
+ module Pagination
3
+ class PaginationProxy < BasicObject
4
+ attr_accessor :subject
5
+ attr_reader :total_entries, :per_page, :current_page
6
+ alias limit per_page
7
+
8
+ def initialize(total_entries, current_page, per_page=nil)
9
+ @total_entries = total_entries.to_i
10
+ self.per_page = per_page
11
+ self.current_page = current_page
12
+ end
13
+
14
+ def total_pages
15
+ (total_entries / per_page.to_f).ceil
16
+ end
17
+
18
+ def out_of_bounds?
19
+ current_page > total_pages
20
+ end
21
+
22
+ def previous_page
23
+ current_page > 1 ? (current_page - 1) : nil
24
+ end
25
+
26
+ def next_page
27
+ current_page < total_pages ? (current_page + 1) : nil
28
+ end
29
+
30
+ def skip
31
+ (current_page - 1) * per_page
32
+ end
33
+
34
+ def method_missing(name, *args, &block)
35
+ @subject.send(name, *args, &block)
36
+ end
37
+
38
+ private
39
+ def per_page=(value)
40
+ value = 25 if value.blank?
41
+ @per_page = value.to_i
42
+ end
43
+
44
+ def current_page=(value)
45
+ value = value.to_i
46
+ value = 1 if value < 1
47
+ @current_page = value
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,15 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module Document
4
+ def self.included(model)
5
+ model.class_eval do
6
+ alias_method :new_record?, :new?
7
+ end
8
+ end
9
+
10
+ def to_param
11
+ id
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,27 @@
1
+ module MongoMapper
2
+ module RailsCompatibility
3
+ module EmbeddedDocument
4
+ def self.included(model)
5
+ model.class_eval do
6
+ extend ClassMethods
7
+
8
+ alias_method :new_record?, :new?
9
+ end
10
+
11
+ class << model
12
+ alias has_many many
13
+ end
14
+ end
15
+
16
+ module ClassMethods
17
+ def column_names
18
+ keys.keys
19
+ end
20
+ end
21
+
22
+ def to_param
23
+ raise "Missing to_param method in #{self.class}. You should implement it to return the unique identifier of this embedded document within a document."
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,19 @@
1
+ module MongoMapper
2
+ module SaveWithValidation
3
+ def self.included(base)
4
+ base.class_eval do
5
+ alias_method_chain :save, :validation
6
+ alias_method_chain :save!, :validation
7
+ end
8
+ end
9
+
10
+ private
11
+ def save_with_validation
12
+ valid? ? save_without_validation : false
13
+ end
14
+
15
+ def save_with_validation!
16
+ valid? ? save_without_validation! : raise(DocumentNotValid.new(self))
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,55 @@
1
+ require 'active_support/json'
2
+
3
+ module MongoMapper #:nodoc:
4
+ module Serialization
5
+ class Serializer #:nodoc:
6
+ attr_reader :options
7
+
8
+ def initialize(record, options = {})
9
+ @record, @options = record, options.dup
10
+ end
11
+
12
+ def serializable_key_names
13
+ key_names = @record.attributes.keys
14
+
15
+ if options[:only]
16
+ options.delete(:except)
17
+ key_names = key_names & Array(options[:only]).collect { |n| n.to_s }
18
+ else
19
+ options[:except] = Array(options[:except])
20
+ key_names = key_names - options[:except].collect { |n| n.to_s }
21
+ end
22
+
23
+ key_names
24
+ end
25
+
26
+ def serializable_method_names
27
+ Array(options[:methods]).inject([]) do |method_attributes, name|
28
+ method_attributes << name if @record.respond_to?(name.to_s)
29
+ method_attributes
30
+ end
31
+ end
32
+
33
+ def serializable_names
34
+ serializable_key_names + serializable_method_names
35
+ end
36
+
37
+ def serializable_record
38
+ returning(serializable_record = {}) do
39
+ serializable_names.each { |name| serializable_record[name] = @record.send(name) }
40
+ end
41
+ end
42
+
43
+ def serialize
44
+ # overwrite to implement
45
+ end
46
+
47
+ def to_s(&block)
48
+ serialize(&block)
49
+ end
50
+ end
51
+ end
52
+ end
53
+
54
+ dir = Pathname(__FILE__).dirname.expand_path + 'serializers'
55
+ require dir + 'json_serializer'
@@ -0,0 +1,92 @@
1
+ module MongoMapper #:nodoc:
2
+ module Serialization
3
+ def self.included(base)
4
+ base.cattr_accessor :include_root_in_json, :instance_writer => false
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ # Returns a JSON string representing the model. Some configuration is
9
+ # available through +options+.
10
+ #
11
+ # The option <tt>include_root_in_json</tt> controls the top-level behavior of
12
+ # to_json. When it is <tt>true</tt>, to_json will emit a single root node named
13
+ # after the object's type. For example:
14
+ #
15
+ # konata = User.find(1)
16
+ # User.include_root_in_json = true
17
+ # konata.to_json
18
+ # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
19
+ # "created_at": "2006/08/01", "awesome": true} }
20
+ #
21
+ # User.include_root_in_json = false
22
+ # konata.to_json
23
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
24
+ # "created_at": "2006/08/01", "awesome": true}
25
+ #
26
+ # The remainder of the examples in this section assume include_root_in_json is set to
27
+ # <tt>false</tt>.
28
+ #
29
+ # Without any +options+, the returned JSON string will include all
30
+ # the model's attributes. For example:
31
+ #
32
+ # konata = User.find(1)
33
+ # konata.to_json
34
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
35
+ # "created_at": "2006/08/01", "awesome": true}
36
+ #
37
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
38
+ # included, and work similar to the +attributes+ method. For example:
39
+ #
40
+ # konata.to_json(:only => [ :id, :name ])
41
+ # # => {"id": 1, "name": "Konata Izumi"}
42
+ #
43
+ # konata.to_json(:except => [ :id, :created_at, :age ])
44
+ # # => {"name": "Konata Izumi", "awesome": true}
45
+ #
46
+ # To include any methods on the model, use <tt>:methods</tt>.
47
+ #
48
+ # konata.to_json(:methods => :permalink)
49
+ # # => {"id": 1, "name": "Konata Izumi", "age": 16,
50
+ # "created_at": "2006/08/01", "awesome": true,
51
+ # "permalink": "1-konata-izumi"}
52
+ def to_json(options = {})
53
+ apply_to_json_defaults(options)
54
+
55
+ if include_root_in_json
56
+ "{#{self.class.json_class_name}: #{JsonSerializer.new(self, options).to_s}}"
57
+ else
58
+ JsonSerializer.new(self, options).to_s
59
+ end
60
+ end
61
+
62
+ def from_json(json)
63
+ self.attributes = ActiveSupport::JSON.decode(json)
64
+ self
65
+ end
66
+
67
+ class JsonSerializer < MongoMapper::Serialization::Serializer #:nodoc:
68
+ def serialize
69
+ serializable_record.to_json
70
+ end
71
+ end
72
+
73
+ module ClassMethods
74
+ def json_class_name
75
+ @json_class_name ||= name.demodulize.underscore.inspect
76
+ end
77
+ end
78
+
79
+ private
80
+ def apply_to_json_defaults(options)
81
+ unless options[:only]
82
+ methods = [options.delete(:methods)].flatten.compact
83
+ methods << :id
84
+ options[:methods] = methods.uniq
85
+ end
86
+
87
+ except = [options.delete(:except)].flatten.compact
88
+ except << :_id
89
+ options[:except] = except
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,157 @@
1
+ class BasicObject #:nodoc:
2
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|^methods$|instance_eval|proxy_|^object_id$)/ }
3
+ end unless defined?(BasicObject)
4
+
5
+ class Array
6
+ def self.to_mongo(value)
7
+ value.to_a
8
+ end
9
+
10
+ def self.from_mongo(value)
11
+ value || []
12
+ end
13
+ end
14
+
15
+ class Binary
16
+ def self.to_mongo(value)
17
+ if value.is_a?(ByteBuffer)
18
+ value
19
+ else
20
+ value.nil? ? nil : ByteBuffer.new(value)
21
+ end
22
+ end
23
+
24
+ def self.from_mongo(value)
25
+ value
26
+ end
27
+ end
28
+
29
+ class Boolean
30
+ def self.to_mongo(value)
31
+ if value.is_a?(Boolean)
32
+ value
33
+ else
34
+ ['true', 't', '1'].include?(value.to_s.downcase)
35
+ end
36
+ end
37
+
38
+ def self.from_mongo(value)
39
+ !!value
40
+ end
41
+ end
42
+
43
+ class Date
44
+ def self.to_mongo(value)
45
+ date = Date.parse(value.to_s)
46
+ Time.utc(date.year, date.month, date.day)
47
+ rescue
48
+ nil
49
+ end
50
+
51
+ def self.from_mongo(value)
52
+ value.to_date if value.present?
53
+ end
54
+ end
55
+
56
+ class Float
57
+ def self.to_mongo(value)
58
+ value.to_f
59
+ end
60
+ end
61
+
62
+ class Hash
63
+ def self.from_mongo(value)
64
+ HashWithIndifferentAccess.new(value || {})
65
+ end
66
+
67
+ def to_mongo
68
+ self
69
+ end
70
+ end
71
+
72
+ class Integer
73
+ def self.to_mongo(value)
74
+ value_to_i = value.to_i
75
+ if value_to_i == 0
76
+ value.to_s =~ /^(0x|0b)?0+/ ? 0 : nil
77
+ else
78
+ value_to_i
79
+ end
80
+ end
81
+ end
82
+
83
+ class NilClass
84
+ def to_mongo(value)
85
+ value
86
+ end
87
+
88
+ def from_mongo(value)
89
+ value
90
+ end
91
+ end
92
+
93
+ class Object
94
+ # The hidden singleton lurks behind everyone
95
+ def metaclass
96
+ class << self; self end
97
+ end
98
+
99
+ def meta_eval(&blk)
100
+ metaclass.instance_eval(&blk)
101
+ end
102
+
103
+ # Adds methods to a metaclass
104
+ def meta_def(name, &blk)
105
+ meta_eval { define_method(name, &blk) }
106
+ end
107
+
108
+ # Defines an instance method within a class
109
+ def class_def(name, &blk)
110
+ class_eval { define_method(name, &blk) }
111
+ end
112
+
113
+ def self.to_mongo(value)
114
+ value
115
+ end
116
+
117
+ def self.from_mongo(value)
118
+ value
119
+ end
120
+ end
121
+
122
+ class String
123
+ def self.to_mongo(value)
124
+ value.nil? ? nil : value.to_s
125
+ end
126
+
127
+ def self.from_mongo(value)
128
+ value.nil? ? nil : value.to_s
129
+ end
130
+ end
131
+
132
+ class Time
133
+ def self.to_mongo(value)
134
+ to_utc_time(value)
135
+ end
136
+
137
+ def self.from_mongo(value)
138
+ if Time.respond_to?(:zone) && Time.zone && value.present?
139
+ value.in_time_zone(Time.zone)
140
+ else
141
+ value
142
+ end
143
+ end
144
+
145
+ def self.to_utc_time(value)
146
+ to_local_time(value).try(:utc)
147
+ end
148
+
149
+ # make sure we have a time and that it is local
150
+ def self.to_local_time(value)
151
+ if Time.respond_to?(:zone) && Time.zone
152
+ Time.zone.parse(value.to_s)
153
+ else
154
+ Time.parse(value.to_s)
155
+ end
156
+ end
157
+ end