danielharan-mongo_mapper 0.6.5

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 (74) hide show
  1. data/.gitignore +10 -0
  2. data/LICENSE +20 -0
  3. data/README.rdoc +53 -0
  4. data/Rakefile +55 -0
  5. data/VERSION +1 -0
  6. data/bin/mmconsole +60 -0
  7. data/lib/mongo_mapper.rb +134 -0
  8. data/lib/mongo_mapper/associations.rb +183 -0
  9. data/lib/mongo_mapper/associations/base.rb +110 -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 +25 -0
  13. data/lib/mongo_mapper/associations/many_documents_proxy.rb +127 -0
  14. data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
  15. data/lib/mongo_mapper/associations/many_embedded_proxy.rb +53 -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 +80 -0
  19. data/lib/mongo_mapper/callbacks.rb +109 -0
  20. data/lib/mongo_mapper/dirty.rb +136 -0
  21. data/lib/mongo_mapper/document.rb +481 -0
  22. data/lib/mongo_mapper/dynamic_finder.rb +35 -0
  23. data/lib/mongo_mapper/embedded_document.rb +386 -0
  24. data/lib/mongo_mapper/finder_options.rb +133 -0
  25. data/lib/mongo_mapper/key.rb +36 -0
  26. data/lib/mongo_mapper/observing.rb +50 -0
  27. data/lib/mongo_mapper/pagination.rb +53 -0
  28. data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
  29. data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
  30. data/lib/mongo_mapper/serialization.rb +54 -0
  31. data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
  32. data/lib/mongo_mapper/support.rb +193 -0
  33. data/lib/mongo_mapper/validations.rb +41 -0
  34. data/mongo_mapper.gemspec +171 -0
  35. data/specs.watchr +32 -0
  36. data/test/NOTE_ON_TESTING +1 -0
  37. data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
  38. data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
  39. data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
  40. data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
  41. data/test/functional/associations/test_many_embedded_proxy.rb +196 -0
  42. data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
  43. data/test/functional/associations/test_many_proxy.rb +384 -0
  44. data/test/functional/test_associations.rb +44 -0
  45. data/test/functional/test_binary.rb +18 -0
  46. data/test/functional/test_callbacks.rb +85 -0
  47. data/test/functional/test_dirty.rb +159 -0
  48. data/test/functional/test_document.rb +1180 -0
  49. data/test/functional/test_embedded_document.rb +125 -0
  50. data/test/functional/test_logger.rb +20 -0
  51. data/test/functional/test_pagination.rb +95 -0
  52. data/test/functional/test_rails_compatibility.rb +25 -0
  53. data/test/functional/test_string_id_compatibility.rb +72 -0
  54. data/test/functional/test_validations.rb +369 -0
  55. data/test/models.rb +271 -0
  56. data/test/support/custom_matchers.rb +55 -0
  57. data/test/support/timing.rb +16 -0
  58. data/test/test_helper.rb +27 -0
  59. data/test/unit/serializers/test_json_serializer.rb +189 -0
  60. data/test/unit/test_association_base.rb +166 -0
  61. data/test/unit/test_document.rb +204 -0
  62. data/test/unit/test_dynamic_finder.rb +125 -0
  63. data/test/unit/test_embedded_document.rb +718 -0
  64. data/test/unit/test_finder_options.rb +296 -0
  65. data/test/unit/test_key.rb +172 -0
  66. data/test/unit/test_mongo_mapper.rb +65 -0
  67. data/test/unit/test_observing.rb +101 -0
  68. data/test/unit/test_pagination.rb +113 -0
  69. data/test/unit/test_rails_compatibility.rb +49 -0
  70. data/test/unit/test_serializations.rb +52 -0
  71. data/test/unit/test_support.rb +342 -0
  72. data/test/unit/test_time_zones.rb +40 -0
  73. data/test/unit/test_validations.rb +503 -0
  74. metadata +233 -0
@@ -0,0 +1,110 @@
1
+ module MongoMapper
2
+ module Associations
3
+ # Base class for keeping track of associations.
4
+ #
5
+ # @private
6
+ class Base
7
+ attr_reader :type, :name, :options, :finder_options
8
+
9
+ # Options that should not be considered MongoDB query options/criteria
10
+ AssociationOptions = [:as, :class, :class_name, :dependent, :extend, :foreign_key, :polymorphic]
11
+
12
+ def initialize(type, name, options={}, &extension)
13
+ @type, @name, @options, @finder_options = type, name, {}, {}
14
+ options.symbolize_keys!
15
+
16
+ options[:extend] = modulized_extensions(extension, options[:extend])
17
+
18
+ options.each_pair do |key, value|
19
+ if AssociationOptions.include?(key)
20
+ @options[key] = value
21
+ else
22
+ @finder_options[key] = value
23
+ end
24
+ end
25
+ end
26
+
27
+ def class_name
28
+ @class_name ||= begin
29
+ if cn = options[:class_name]
30
+ cn
31
+ elsif many?
32
+ name.to_s.singularize.camelize
33
+ else
34
+ name.to_s.camelize
35
+ end
36
+ end
37
+ end
38
+
39
+ def klass
40
+ @klass ||= options[:class] || class_name.constantize
41
+ end
42
+
43
+ def many?
44
+ @many_type ||= @type == :many
45
+ end
46
+
47
+ def belongs_to?
48
+ @belongs_to_type ||= @type == :belongs_to
49
+ end
50
+
51
+ def polymorphic?
52
+ !!@options[:polymorphic]
53
+ end
54
+
55
+ def as?
56
+ !!@options[:as]
57
+ end
58
+
59
+ def type_key_name
60
+ @type_key_name ||= many? ? '_type' : "#{as}_type"
61
+ end
62
+
63
+ def as
64
+ @options[:as] || self.name
65
+ end
66
+
67
+ def foreign_key
68
+ @options[:foreign_key] || "#{name}_id"
69
+ end
70
+
71
+ def ivar
72
+ @ivar ||= "@_#{name}"
73
+ end
74
+
75
+ def embeddable?
76
+ many? && klass.embeddable?
77
+ end
78
+
79
+ def proxy_class
80
+ @proxy_class ||= begin
81
+ if many?
82
+ if self.klass.embeddable?
83
+ polymorphic? ? ManyEmbeddedPolymorphicProxy : ManyEmbeddedProxy
84
+ else
85
+ if polymorphic?
86
+ ManyPolymorphicProxy
87
+ elsif as?
88
+ ManyDocumentsAsProxy
89
+ else
90
+ ManyProxy
91
+ end
92
+ end
93
+ else
94
+ polymorphic? ? BelongsToPolymorphicProxy : BelongsToProxy
95
+ end
96
+ end # end begin
97
+ end # end proxy_class
98
+
99
+ private
100
+
101
+ # @param [Array<Module, Proc>] extensions a collection of Modules or
102
+ # Procs that extend the behaviour of this association.
103
+ def modulized_extensions(*extensions)
104
+ extensions.flatten.compact.map do |extension|
105
+ Proc === extension ? Module.new(&extension) : extension
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,34 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class BelongsToPolymorphicProxy < Proxy
4
+ def replace(doc)
5
+ if doc
6
+ doc.save if doc.new?
7
+ id, type = doc._id, doc.class.name
8
+ end
9
+
10
+ @owner.send("#{@association.foreign_key}=", id)
11
+ @owner.send("#{@association.type_key_name}=", type)
12
+ reset
13
+ end
14
+
15
+ protected
16
+ def find_target
17
+ if proxy_id && proxy_class
18
+ proxy_class.find_by_id(proxy_id)
19
+ end
20
+ end
21
+
22
+ def proxy_id
23
+ @proxy_id ||= @owner.send(@association.foreign_key)
24
+ end
25
+
26
+ def proxy_class
27
+ @proxy_class ||= begin
28
+ klass = @owner.send(@association.type_key_name)
29
+ klass && klass.constantize
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,22 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class BelongsToProxy < Proxy
4
+ def replace(doc)
5
+ if doc
6
+ doc.save if doc.new?
7
+ id = doc._id
8
+ end
9
+
10
+ @owner.send("#{@association.foreign_key}=", id)
11
+ reset
12
+ end
13
+
14
+ protected
15
+ def find_target
16
+ if association_id = @owner.send(@association.foreign_key)
17
+ @association.klass.find_by_id(association_id)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyDocumentsAsProxy < ManyDocumentsProxy
4
+ protected
5
+ def scoped_conditions
6
+ {as_type_name => @owner.class.name, as_id_name => @owner._id}
7
+ end
8
+
9
+ def apply_scope(doc)
10
+ ensure_owner_saved
11
+ doc.send("#{as_type_name}=", @owner.class.name)
12
+ doc.send("#{as_id_name}=", @owner._id)
13
+ doc
14
+ end
15
+
16
+ def as_type_name
17
+ @as_type_name ||= @association.options[:as].to_s + "_type"
18
+ end
19
+
20
+ def as_id_name
21
+ @as_id_name ||= @association.options[:as].to_s + "_id"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,127 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyDocumentsProxy < Proxy
4
+ delegate :klass, :to => :@association
5
+ delegate :collection, :to => :klass
6
+
7
+ include ::MongoMapper::Finders
8
+
9
+ def find(*args)
10
+ options = args.extract_options!
11
+ klass.find(*args << scoped_options(options))
12
+ end
13
+
14
+ def find!(*args)
15
+ options = args.extract_options!
16
+ klass.find!(*args << scoped_options(options))
17
+ end
18
+
19
+ def paginate(options)
20
+ klass.paginate(scoped_options(options))
21
+ end
22
+
23
+ def all(options={})
24
+ klass.all(scoped_options(options))
25
+ end
26
+
27
+ def first(options={})
28
+ klass.first(scoped_options(options))
29
+ end
30
+
31
+ def last(options={})
32
+ klass.last(scoped_options(options))
33
+ end
34
+
35
+ def count(options={})
36
+ klass.count(scoped_options(options))
37
+ end
38
+
39
+ def replace(docs)
40
+ @target.map(&:destroy) if load_target
41
+ docs.each { |doc| apply_scope(doc).save }
42
+ reset
43
+ end
44
+
45
+ def <<(*docs)
46
+ ensure_owner_saved
47
+ flatten_deeper(docs).each { |doc| apply_scope(doc).save }
48
+ reset
49
+ end
50
+ alias_method :push, :<<
51
+ alias_method :concat, :<<
52
+
53
+ def build(attrs={})
54
+ doc = klass.new(attrs)
55
+ apply_scope(doc)
56
+ doc
57
+ end
58
+
59
+ def create(attrs={})
60
+ doc = klass.new(attrs)
61
+ apply_scope(doc).save
62
+ doc
63
+ end
64
+
65
+ def create!(attrs={})
66
+ doc = klass.new(attrs)
67
+ apply_scope(doc).save!
68
+ doc
69
+ end
70
+
71
+ def destroy_all(options={})
72
+ all(options).map(&:destroy)
73
+ reset
74
+ end
75
+
76
+ def delete_all(options={})
77
+ klass.delete_all(options.merge(scoped_conditions))
78
+ reset
79
+ end
80
+
81
+ def nullify
82
+ criteria = FinderOptions.new(klass, scoped_conditions).criteria
83
+ all(criteria).each do |doc|
84
+ doc.update_attributes(self.foreign_key => nil)
85
+ end
86
+ reset
87
+ end
88
+
89
+ def method_missing(method, *args)
90
+ finder = DynamicFinder.new(method)
91
+
92
+ if finder.found?
93
+ dynamic_find(finder, args)
94
+ else
95
+ super
96
+ end
97
+ end
98
+
99
+ protected
100
+ def scoped_conditions
101
+ {self.foreign_key => @owner._id}
102
+ end
103
+
104
+ def scoped_options(options)
105
+ @association.finder_options.merge(options).merge(scoped_conditions)
106
+ end
107
+
108
+ def find_target
109
+ all
110
+ end
111
+
112
+ def ensure_owner_saved
113
+ @owner.save if @owner.new?
114
+ end
115
+
116
+ def apply_scope(doc)
117
+ ensure_owner_saved
118
+ doc.send("#{self.foreign_key}=", @owner._id)
119
+ doc
120
+ end
121
+
122
+ def foreign_key
123
+ @association.options[:foreign_key] || @owner.class.name.underscore.gsub("/", "_") + "_id"
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,33 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyEmbeddedPolymorphicProxy < Proxy
4
+ def replace(v)
5
+ @_values = v.map do |doc_or_hash|
6
+ if doc_or_hash.kind_of?(EmbeddedDocument)
7
+ doc = doc_or_hash
8
+ {@association.type_key_name => doc.class.name}.merge(doc.attributes)
9
+ else
10
+ doc_or_hash
11
+ end
12
+ end
13
+
14
+ reset
15
+ end
16
+
17
+ protected
18
+ def find_target
19
+ (@_values || []).map do |hash|
20
+ polymorphic_class(hash).new(hash)
21
+ end
22
+ end
23
+
24
+ def polymorphic_class(doc)
25
+ if class_name = doc[@association.type_key_name]
26
+ class_name.constantize
27
+ else
28
+ @association.klass
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,53 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyEmbeddedProxy < Proxy
4
+ def replace(v)
5
+ @_values = v.map { |e| e.kind_of?(EmbeddedDocument) ? e.attributes : e }
6
+ reset
7
+ end
8
+
9
+ def build(attributes={})
10
+ doc = @association.klass.new(attributes)
11
+ assign_root_document(doc)
12
+ self << doc
13
+ doc
14
+ end
15
+
16
+ # TODO: test that both string and oid version work
17
+ def find(id)
18
+ load_target
19
+ @target.detect { |item| item.id.to_s == id || item.id == id }
20
+ end
21
+
22
+ def <<(*docs)
23
+ if load_target
24
+ docs.each do |doc|
25
+ assign_root_document(doc)
26
+ @target << doc
27
+ end
28
+ end
29
+ end
30
+ alias_method :push, :<<
31
+ alias_method :concat, :<<
32
+
33
+ private
34
+ def find_target
35
+ (@_values || []).map do |e|
36
+ child = @association.klass.new(e)
37
+ assign_root_document(child)
38
+ child
39
+ end
40
+ end
41
+
42
+ def root_document
43
+ @owner._root_document || @owner
44
+ end
45
+
46
+ def assign_root_document(*docs)
47
+ docs.each do |doc|
48
+ doc._root_document = root_document
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,11 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyPolymorphicProxy < ManyDocumentsProxy
4
+ private
5
+ def apply_scope(doc)
6
+ doc.send("#{@association.type_key_name}=", doc.class.name)
7
+ super
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,6 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class ManyProxy < ManyDocumentsProxy
4
+ end
5
+ end
6
+ end
@@ -0,0 +1,80 @@
1
+ module MongoMapper
2
+ module Associations
3
+ class Proxy < BasicObject
4
+ attr_reader :owner, :association
5
+
6
+ def initialize(owner, association)
7
+ @owner = owner
8
+ @association = association
9
+ @association.options[:extend].each { |ext| class << self; self; end.instance_eval { include ext } }
10
+ reset
11
+ end
12
+
13
+ def respond_to?(*methods)
14
+ (load_target && @target.respond_to?(*methods))
15
+ end
16
+
17
+ def reset
18
+ @target = nil
19
+ end
20
+
21
+ def reload_target
22
+ reset
23
+ load_target
24
+ self
25
+ end
26
+
27
+ def send(method, *args)
28
+ metaclass_instance_methods = class << self; self; end.instance_methods
29
+
30
+ if metaclass_instance_methods.any? { |m| m.to_s == method.to_s }
31
+ return __send__(method, *args)
32
+ end
33
+
34
+ load_target
35
+ @target.send(method, *args)
36
+ end
37
+
38
+ def replace(v)
39
+ raise NotImplementedError
40
+ end
41
+
42
+ def inspect
43
+ load_target
44
+ @target.inspect
45
+ end
46
+
47
+ def nil?
48
+ load_target
49
+ @target.nil?
50
+ end
51
+
52
+ protected
53
+ def method_missing(method, *args, &block)
54
+ if load_target
55
+ if block.nil?
56
+ @target.send(method, *args)
57
+ else
58
+ @target.send(method, *args) { |*block_args| block.call(*block_args) }
59
+ end
60
+ end
61
+ end
62
+
63
+ def load_target
64
+ @target ||= find_target
65
+ end
66
+
67
+ def find_target
68
+ raise NotImplementedError
69
+ end
70
+
71
+ # Array#flatten has problems with recursive arrays. Going one level
72
+ # deeper solves the majority of the problems.
73
+ def flatten_deeper(array)
74
+ array.collect do |element|
75
+ (element.respond_to?(:flatten) && !element.is_a?(Hash)) ? element.flatten : element
76
+ end.flatten
77
+ end
78
+ end
79
+ end
80
+ end