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.
- data/.gitignore +10 -0
- data/LICENSE +20 -0
- data/README.rdoc +53 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/bin/mmconsole +60 -0
- data/lib/mongo_mapper.rb +134 -0
- data/lib/mongo_mapper/associations.rb +183 -0
- data/lib/mongo_mapper/associations/base.rb +110 -0
- data/lib/mongo_mapper/associations/belongs_to_polymorphic_proxy.rb +34 -0
- data/lib/mongo_mapper/associations/belongs_to_proxy.rb +22 -0
- data/lib/mongo_mapper/associations/many_documents_as_proxy.rb +25 -0
- data/lib/mongo_mapper/associations/many_documents_proxy.rb +127 -0
- data/lib/mongo_mapper/associations/many_embedded_polymorphic_proxy.rb +33 -0
- data/lib/mongo_mapper/associations/many_embedded_proxy.rb +53 -0
- data/lib/mongo_mapper/associations/many_polymorphic_proxy.rb +11 -0
- data/lib/mongo_mapper/associations/many_proxy.rb +6 -0
- data/lib/mongo_mapper/associations/proxy.rb +80 -0
- data/lib/mongo_mapper/callbacks.rb +109 -0
- data/lib/mongo_mapper/dirty.rb +136 -0
- data/lib/mongo_mapper/document.rb +481 -0
- data/lib/mongo_mapper/dynamic_finder.rb +35 -0
- data/lib/mongo_mapper/embedded_document.rb +386 -0
- data/lib/mongo_mapper/finder_options.rb +133 -0
- data/lib/mongo_mapper/key.rb +36 -0
- data/lib/mongo_mapper/observing.rb +50 -0
- data/lib/mongo_mapper/pagination.rb +53 -0
- data/lib/mongo_mapper/rails_compatibility/document.rb +15 -0
- data/lib/mongo_mapper/rails_compatibility/embedded_document.rb +27 -0
- data/lib/mongo_mapper/serialization.rb +54 -0
- data/lib/mongo_mapper/serializers/json_serializer.rb +92 -0
- data/lib/mongo_mapper/support.rb +193 -0
- data/lib/mongo_mapper/validations.rb +41 -0
- data/mongo_mapper.gemspec +171 -0
- data/specs.watchr +32 -0
- data/test/NOTE_ON_TESTING +1 -0
- data/test/functional/associations/test_belongs_to_polymorphic_proxy.rb +55 -0
- data/test/functional/associations/test_belongs_to_proxy.rb +48 -0
- data/test/functional/associations/test_many_documents_as_proxy.rb +246 -0
- data/test/functional/associations/test_many_embedded_polymorphic_proxy.rb +156 -0
- data/test/functional/associations/test_many_embedded_proxy.rb +196 -0
- data/test/functional/associations/test_many_polymorphic_proxy.rb +339 -0
- data/test/functional/associations/test_many_proxy.rb +384 -0
- data/test/functional/test_associations.rb +44 -0
- data/test/functional/test_binary.rb +18 -0
- data/test/functional/test_callbacks.rb +85 -0
- data/test/functional/test_dirty.rb +159 -0
- data/test/functional/test_document.rb +1180 -0
- data/test/functional/test_embedded_document.rb +125 -0
- data/test/functional/test_logger.rb +20 -0
- data/test/functional/test_pagination.rb +95 -0
- data/test/functional/test_rails_compatibility.rb +25 -0
- data/test/functional/test_string_id_compatibility.rb +72 -0
- data/test/functional/test_validations.rb +369 -0
- data/test/models.rb +271 -0
- data/test/support/custom_matchers.rb +55 -0
- data/test/support/timing.rb +16 -0
- data/test/test_helper.rb +27 -0
- data/test/unit/serializers/test_json_serializer.rb +189 -0
- data/test/unit/test_association_base.rb +166 -0
- data/test/unit/test_document.rb +204 -0
- data/test/unit/test_dynamic_finder.rb +125 -0
- data/test/unit/test_embedded_document.rb +718 -0
- data/test/unit/test_finder_options.rb +296 -0
- data/test/unit/test_key.rb +172 -0
- data/test/unit/test_mongo_mapper.rb +65 -0
- data/test/unit/test_observing.rb +101 -0
- data/test/unit/test_pagination.rb +113 -0
- data/test/unit/test_rails_compatibility.rb +49 -0
- data/test/unit/test_serializations.rb +52 -0
- data/test/unit/test_support.rb +342 -0
- data/test/unit/test_time_zones.rb +40 -0
- data/test/unit/test_validations.rb +503 -0
- 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,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
|