mongo_mapper-unstable 2009.10.11

Sign up to get free protection for your applications and to get access to all the features.
Files changed (73) hide show
  1. data/.gitignore +8 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +50 -0
  4. data/Rakefile +87 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +55 -0
  7. data/lib/mongo_mapper/associations/base.rb +83 -0
  8. data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
  9. data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
  10. data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +27 -0
  11. data/lib/mongo_mapper/associations/many_documents_proxy.rb +116 -0
  12. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  13. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +67 -0
  14. data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
  15. data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
  16. data/lib/mongo_mapper/associations/proxy.rb +74 -0
  17. data/lib/mongo_mapper/associations.rb +86 -0
  18. data/lib/mongo_mapper/callbacks.rb +106 -0
  19. data/lib/mongo_mapper/dirty.rb +137 -0
  20. data/lib/mongo_mapper/document.rb +340 -0
  21. data/lib/mongo_mapper/dynamic_finder.rb +35 -0
  22. data/lib/mongo_mapper/embedded_document.rb +355 -0
  23. data/lib/mongo_mapper/finder_options.rb +98 -0
  24. data/lib/mongo_mapper/key.rb +36 -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 +161 -0
  33. data/lib/mongo_mapper/validations.rb +69 -0
  34. data/lib/mongo_mapper.rb +111 -0
  35. data/mongo_mapper.gemspec +162 -0
  36. data/specs.watchr +32 -0
  37. data/test/NOTE_ON_TESTING +1 -0
  38. data/test/custom_matchers.rb +55 -0
  39. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  40. data/test/functional/associations/test_belongs_to_proxy.rb +49 -0
  41. data/test/functional/associations/test_many_documents_as_proxy.rb +244 -0
  42. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +132 -0
  43. data/test/functional/associations/test_many_embedded_proxy.rb +174 -0
  44. data/test/functional/associations/test_many_polymorphic_proxy.rb +297 -0
  45. data/test/functional/associations/test_many_proxy.rb +331 -0
  46. data/test/functional/test_associations.rb +44 -0
  47. data/test/functional/test_binary.rb +18 -0
  48. data/test/functional/test_callbacks.rb +85 -0
  49. data/test/functional/test_dirty.rb +138 -0
  50. data/test/functional/test_document.rb +1051 -0
  51. data/test/functional/test_embedded_document.rb +97 -0
  52. data/test/functional/test_logger.rb +20 -0
  53. data/test/functional/test_pagination.rb +87 -0
  54. data/test/functional/test_rails_compatibility.rb +30 -0
  55. data/test/functional/test_validations.rb +279 -0
  56. data/test/models.rb +195 -0
  57. data/test/test_helper.rb +30 -0
  58. data/test/unit/serializers/test_json_serializer.rb +189 -0
  59. data/test/unit/test_association_base.rb +144 -0
  60. data/test/unit/test_document.rb +184 -0
  61. data/test/unit/test_dynamic_finder.rb +125 -0
  62. data/test/unit/test_embedded_document.rb +656 -0
  63. data/test/unit/test_finder_options.rb +261 -0
  64. data/test/unit/test_key.rb +172 -0
  65. data/test/unit/test_mongomapper.rb +28 -0
  66. data/test/unit/test_observing.rb +101 -0
  67. data/test/unit/test_pagination.rb +109 -0
  68. data/test/unit/test_rails_compatibility.rb +39 -0
  69. data/test/unit/test_serializations.rb +52 -0
  70. data/test/unit/test_support.rb +291 -0
  71. data/test/unit/test_time_zones.rb +40 -0
  72. data/test/unit/test_validations.rb +503 -0
  73. metadata +210 -0
@@ -0,0 +1,98 @@
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
+ OptionKeys = [:fields, :select, :skip, :offset, :limit, :sort, :order]
46
+
47
+ def initialize(options)
48
+ raise ArgumentError, "FinderOptions must be a hash" unless options.is_a?(Hash)
49
+
50
+ options = options.symbolize_keys
51
+ @options, @conditions = {}, options.delete(:conditions) || {}
52
+
53
+ options.each_pair do |key, value|
54
+ if OptionKeys.include?(key)
55
+ @options[key] = value
56
+ else
57
+ @conditions[key] = value
58
+ end
59
+ end
60
+ end
61
+
62
+ def criteria
63
+ self.class.to_mongo_criteria(@conditions)
64
+ end
65
+
66
+ def options
67
+ self.class.to_mongo_options(@options)
68
+ end
69
+
70
+ def to_a
71
+ [criteria, options]
72
+ end
73
+
74
+ private
75
+ def self.to_mongo_fields(fields)
76
+ return if fields.blank?
77
+
78
+ if fields.is_a?(String)
79
+ fields.split(',').map { |field| field.strip }
80
+ else
81
+ fields.flatten.compact
82
+ end
83
+ end
84
+
85
+ def self.to_mongo_sort(sort)
86
+ return if sort.blank?
87
+ pieces = sort.split(',')
88
+ pieces.map { |s| to_mongo_sort_piece(s) }
89
+ end
90
+
91
+ def self.to_mongo_sort_piece(str)
92
+ field, direction = str.strip.split(' ')
93
+ direction ||= 'ASC'
94
+ direction = direction.upcase == 'ASC' ? 1 : -1
95
+ [field, direction]
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,36 @@
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 number?
25
+ [Integer, Float].include?(type)
26
+ end
27
+
28
+ def get(value)
29
+ if value.nil? && !default_value.nil?
30
+ return default_value
31
+ end
32
+
33
+ type.from_mongo(value)
34
+ end
35
+ end
36
+ 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,161 @@
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
+ if value.nil? || value == ''
135
+ nil
136
+ else
137
+ to_utc_time(value)
138
+ end
139
+ end
140
+
141
+ def self.from_mongo(value)
142
+ if Time.respond_to?(:zone) && Time.zone && value.present?
143
+ value.in_time_zone(Time.zone)
144
+ else
145
+ value
146
+ end
147
+ end
148
+
149
+ def self.to_utc_time(value)
150
+ to_local_time(value).try(:utc)
151
+ end
152
+
153
+ # make sure we have a time and that it is local
154
+ def self.to_local_time(value)
155
+ if Time.respond_to?(:zone) && Time.zone
156
+ Time.zone.parse(value.to_s)
157
+ else
158
+ Time.parse(value.to_s)
159
+ end
160
+ end
161
+ end
@@ -0,0 +1,69 @@
1
+ module MongoMapper
2
+ module Validations
3
+ module Macros
4
+ def validates_uniqueness_of(*args)
5
+ add_validations(args, MongoMapper::Validations::ValidatesUniquenessOf)
6
+ end
7
+
8
+ def validates_exclusion_of(*args)
9
+ add_validations(args, MongoMapper::Validations::ValidatesExclusionOf)
10
+ end
11
+
12
+ def validates_inclusion_of(*args)
13
+ add_validations(args, MongoMapper::Validations::ValidatesInclusionOf)
14
+ end
15
+ end
16
+
17
+ class ValidatesUniquenessOf < Validatable::ValidationBase
18
+ option :scope
19
+
20
+ def valid?(instance)
21
+ doc = instance.class.find(:first, :conditions => {self.attribute => instance[attribute]}.merge(scope_conditions(instance)), :limit => 1)
22
+ doc.nil? || instance.id == doc.id
23
+ end
24
+
25
+ def message(instance)
26
+ super || "has already been taken"
27
+ end
28
+
29
+ def scope_conditions(instance)
30
+ return {} unless scope
31
+ Array(scope).inject({}) do |conditions, key|
32
+ conditions.merge(key => instance[key])
33
+ end
34
+ end
35
+ end
36
+
37
+ class ValidatesExclusionOf < Validatable::ValidationBase
38
+ required_option :within
39
+
40
+ def valid?(instance)
41
+ value = instance[attribute]
42
+ return true if allow_nil && value.nil?
43
+ return true if allow_blank && value.blank?
44
+
45
+ !within.include?(instance[attribute])
46
+ end
47
+
48
+ def message(instance)
49
+ super || "is reserved"
50
+ end
51
+ end
52
+
53
+ class ValidatesInclusionOf < Validatable::ValidationBase
54
+ required_option :within
55
+
56
+ def valid?(instance)
57
+ value = instance[attribute]
58
+ return true if allow_nil && value.nil?
59
+ return true if allow_blank && value.blank?
60
+
61
+ within.include?(value)
62
+ end
63
+
64
+ def message(instance)
65
+ super || "is not in the list"
66
+ end
67
+ end
68
+ end
69
+ end