mongomodel 0.1.6 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +6 -0
- data/bin/console +4 -4
- data/lib/mongomodel.rb +4 -4
- data/lib/mongomodel/concerns/associations/base/definition.rb +4 -0
- data/lib/mongomodel/concerns/associations/has_many_by_foreign_key.rb +7 -16
- data/lib/mongomodel/concerns/associations/has_many_by_ids.rb +6 -12
- data/lib/mongomodel/concerns/attributes.rb +1 -7
- data/lib/mongomodel/document.rb +0 -1
- data/lib/mongomodel/document/dynamic_finders.rb +2 -69
- data/lib/mongomodel/document/indexes.rb +0 -6
- data/lib/mongomodel/document/persistence.rb +1 -21
- data/lib/mongomodel/document/scopes.rb +59 -135
- data/lib/mongomodel/document/validations/uniqueness.rb +7 -5
- data/lib/mongomodel/support/dynamic_finder.rb +68 -0
- data/lib/mongomodel/support/mongo_operator.rb +29 -0
- data/lib/mongomodel/support/mongo_options.rb +0 -101
- data/lib/mongomodel/support/mongo_order.rb +78 -0
- data/lib/mongomodel/support/scope.rb +186 -0
- data/lib/mongomodel/support/scope/dynamic_finders.rb +21 -0
- data/lib/mongomodel/support/scope/finder_methods.rb +61 -0
- data/lib/mongomodel/support/scope/query_methods.rb +43 -0
- data/lib/mongomodel/support/scope/spawn_methods.rb +35 -0
- data/lib/mongomodel/version.rb +1 -1
- data/mongomodel.gemspec +20 -3
- data/spec/mongomodel/concerns/associations/has_many_by_foreign_key_spec.rb +1 -1
- data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +1 -1
- data/spec/mongomodel/document/dynamic_finders_spec.rb +0 -1
- data/spec/mongomodel/document/finders_spec.rb +0 -144
- data/spec/mongomodel/document/indexes_spec.rb +2 -2
- data/spec/mongomodel/document/persistence_spec.rb +1 -15
- data/spec/mongomodel/document/scopes_spec.rb +64 -167
- data/spec/mongomodel/support/mongo_operator_spec.rb +29 -0
- data/spec/mongomodel/support/mongo_options_spec.rb +0 -150
- data/spec/mongomodel/support/mongo_order_spec.rb +127 -0
- data/spec/mongomodel/support/scope_spec.rb +932 -0
- data/spec/support/helpers/document_finder_stubs.rb +40 -0
- data/spec/support/matchers/find_with.rb +36 -0
- metadata +22 -5
- data/lib/mongomodel/document/finders.rb +0 -82
data/README.md
CHANGED
@@ -11,6 +11,10 @@ MongoModel is distributed as a gem. Install with:
|
|
11
11
|
|
12
12
|
gem install mongomodel
|
13
13
|
|
14
|
+
For performance, you should probably also install the BSON C extensions:
|
15
|
+
|
16
|
+
gem install bson_ext
|
17
|
+
|
14
18
|
|
15
19
|
Sample Usage
|
16
20
|
============
|
@@ -30,5 +34,7 @@ Sample Usage
|
|
30
34
|
validates_presence_of :title, :body
|
31
35
|
|
32
36
|
belongs_to :author, :class => User
|
37
|
+
|
38
|
+
scope :published, where(:published_at.ne => nil)
|
33
39
|
end
|
34
40
|
|
data/bin/console
CHANGED
@@ -31,15 +31,15 @@ IRB.conf[:MAIN_CONTEXT] = irb.context
|
|
31
31
|
examples = <<-EXAMPLES
|
32
32
|
|
33
33
|
class Article < MongoModel::Document
|
34
|
-
property :title, String
|
34
|
+
property :title, String, :required => true
|
35
35
|
property :published, Boolean, :default => false
|
36
36
|
|
37
37
|
timestamps!
|
38
38
|
|
39
|
-
default_scope
|
39
|
+
default_scope order(:title.asc)
|
40
40
|
|
41
|
-
|
42
|
-
|
41
|
+
scope :published, where(:published => true)
|
42
|
+
scope :latest, lambda { |num| order(:created_at.desc).limit(num) }
|
43
43
|
end
|
44
44
|
|
45
45
|
EXAMPLES
|
data/lib/mongomodel.rb
CHANGED
@@ -29,10 +29,12 @@ module MongoModel
|
|
29
29
|
autoload :ActiveModelCompatibility, 'mongomodel/concerns/activemodel'
|
30
30
|
|
31
31
|
autoload :MongoOptions, 'mongomodel/support/mongo_options'
|
32
|
-
autoload :MongoOrder, 'mongomodel/support/
|
33
|
-
autoload :MongoOperator, 'mongomodel/support/
|
32
|
+
autoload :MongoOrder, 'mongomodel/support/mongo_order'
|
33
|
+
autoload :MongoOperator, 'mongomodel/support/mongo_operator'
|
34
|
+
autoload :Scope, 'mongomodel/support/scope'
|
34
35
|
autoload :Types, 'mongomodel/support/types'
|
35
36
|
autoload :Configuration, 'mongomodel/support/configuration'
|
37
|
+
autoload :DynamicFinder, 'mongomodel/support/dynamic_finder'
|
36
38
|
|
37
39
|
autoload :Collection, 'mongomodel/support/collection'
|
38
40
|
|
@@ -66,11 +68,9 @@ module MongoModel
|
|
66
68
|
module DocumentExtensions
|
67
69
|
autoload :Persistence, 'mongomodel/document/persistence'
|
68
70
|
autoload :OptimisticLocking, 'mongomodel/document/optimistic_locking'
|
69
|
-
autoload :Finders, 'mongomodel/document/finders'
|
70
71
|
autoload :DynamicFinders, 'mongomodel/document/dynamic_finders'
|
71
72
|
autoload :Indexes, 'mongomodel/document/indexes'
|
72
73
|
autoload :Scopes, 'mongomodel/document/scopes'
|
73
|
-
autoload :Scope, 'mongomodel/document/scopes'
|
74
74
|
autoload :Validations, 'mongomodel/document/validations'
|
75
75
|
autoload :Callbacks, 'mongomodel/document/callbacks'
|
76
76
|
end
|
@@ -23,17 +23,17 @@ module MongoModel
|
|
23
23
|
delegate :foreign_key, :inverse_of, :to => :definition
|
24
24
|
|
25
25
|
def find_target
|
26
|
-
|
26
|
+
scoped + new_documents
|
27
27
|
end
|
28
28
|
|
29
29
|
def build(*args, &block)
|
30
|
-
doc =
|
30
|
+
doc = scoped.new(*args, &block)
|
31
31
|
new_documents << doc
|
32
32
|
doc
|
33
33
|
end
|
34
34
|
|
35
35
|
def create(*args, &block)
|
36
|
-
|
36
|
+
scoped.create(*args) do |doc|
|
37
37
|
if doc.respond_to?("#{inverse_of}=")
|
38
38
|
doc.send("#{inverse_of}=", instance)
|
39
39
|
else
|
@@ -45,7 +45,7 @@ module MongoModel
|
|
45
45
|
end
|
46
46
|
|
47
47
|
def create!(*args, &block)
|
48
|
-
|
48
|
+
scoped.create!(*args) do |doc|
|
49
49
|
if doc.respond_to?("#{inverse_of}=")
|
50
50
|
doc.send("#{inverse_of}=", instance)
|
51
51
|
else
|
@@ -82,17 +82,8 @@ module MongoModel
|
|
82
82
|
doc.save(false) unless doc.new_record?
|
83
83
|
end
|
84
84
|
|
85
|
-
def
|
86
|
-
|
87
|
-
fk_conditions = { foreign_key => instance.id }
|
88
|
-
|
89
|
-
klass.instance_eval do
|
90
|
-
with_scope(:find => { :conditions => fk_conditions }) do
|
91
|
-
with_scope(:find => scope_options) do
|
92
|
-
send(*args, &block)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
end
|
85
|
+
def scoped
|
86
|
+
definition.scope.where(foreign_key => instance.id)
|
96
87
|
end
|
97
88
|
|
98
89
|
protected
|
@@ -183,7 +174,7 @@ module MongoModel
|
|
183
174
|
if target.respond_to?(method_id) && !OVERRIDE_METHODS.include?(method_id.to_sym)
|
184
175
|
super(method_id, *args, &block)
|
185
176
|
else
|
186
|
-
association.
|
177
|
+
association.scoped.send(method_id, *args, &block)
|
187
178
|
end
|
188
179
|
end
|
189
180
|
end
|
@@ -29,27 +29,21 @@ module MongoModel
|
|
29
29
|
end
|
30
30
|
|
31
31
|
def find_target
|
32
|
-
ids.any? ? Array(
|
32
|
+
ids.any? ? Array.wrap(definition.scope.find(ids - new_document_ids)) + new_documents : []
|
33
33
|
end
|
34
34
|
|
35
35
|
def build(*args, &block)
|
36
|
-
doc =
|
36
|
+
doc = scoped.new(*args, &block)
|
37
37
|
new_documents << doc
|
38
38
|
doc
|
39
39
|
end
|
40
40
|
|
41
41
|
def create(*args, &block)
|
42
|
-
|
42
|
+
scoped.create(*args, &block)
|
43
43
|
end
|
44
44
|
|
45
|
-
def
|
46
|
-
|
47
|
-
|
48
|
-
klass.instance_eval do
|
49
|
-
with_scope(:find => { :conditions => { :id.in => scope_ids } }) do
|
50
|
-
send(*args, &block)
|
51
|
-
end
|
52
|
-
end
|
45
|
+
def scoped
|
46
|
+
definition.scope.where(:id.in => ids)
|
53
47
|
end
|
54
48
|
|
55
49
|
protected
|
@@ -166,7 +160,7 @@ module MongoModel
|
|
166
160
|
if target.respond_to?(method_id) && !OVERRIDE_METHODS.include?(method_id.to_sym)
|
167
161
|
super(method_id, *args, &block)
|
168
162
|
else
|
169
|
-
association.
|
163
|
+
association.scoped.send(method_id, *args, &block)
|
170
164
|
end
|
171
165
|
end
|
172
166
|
end
|
@@ -4,13 +4,7 @@ module MongoModel
|
|
4
4
|
module Attributes
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
|
-
|
8
|
-
alias_method_chain :initialize, :attributes
|
9
|
-
end
|
10
|
-
|
11
|
-
def initialize_with_attributes(attrs={})
|
12
|
-
initialize_without_attributes
|
13
|
-
|
7
|
+
def initialize(attrs={})
|
14
8
|
self.attributes = attrs
|
15
9
|
yield self if block_given?
|
16
10
|
end
|
data/lib/mongomodel/document.rb
CHANGED
@@ -2,7 +2,7 @@ module MongoModel
|
|
2
2
|
module DocumentExtensions
|
3
3
|
module DynamicFinders
|
4
4
|
def respond_to?(method_id, include_private = false)
|
5
|
-
if DynamicFinder.match(
|
5
|
+
if DynamicFinder.match(scoped, method_id)
|
6
6
|
true
|
7
7
|
else
|
8
8
|
super
|
@@ -10,79 +10,12 @@ module MongoModel
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def method_missing(method_id, *args, &block)
|
13
|
-
if finder = DynamicFinder.match(
|
13
|
+
if finder = DynamicFinder.match(scoped, method_id)
|
14
14
|
finder.execute(*args)
|
15
15
|
else
|
16
16
|
super
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
20
|
-
|
21
|
-
class DynamicFinder
|
22
|
-
def initialize(model, attribute_names, finder=:first, bang=false)
|
23
|
-
@model, @attribute_names, @finder, @bang = model, attribute_names, finder, bang
|
24
|
-
end
|
25
|
-
|
26
|
-
def execute(*args)
|
27
|
-
options = args.extract_options!
|
28
|
-
conditions = build_conditions(args)
|
29
|
-
|
30
|
-
result = @model.send(instantiator? ? :first : @finder, options.deep_merge(:conditions => conditions))
|
31
|
-
|
32
|
-
if result.nil?
|
33
|
-
if bang?
|
34
|
-
raise DocumentNotFound, "Couldn't find #{@model.to_s} with #{conditions.inspect}"
|
35
|
-
elsif instantiator?
|
36
|
-
return @model.send(@finder, conditions)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
result
|
41
|
-
end
|
42
|
-
|
43
|
-
def bang?
|
44
|
-
@bang
|
45
|
-
end
|
46
|
-
|
47
|
-
def instantiator?
|
48
|
-
@finder == :new || @finder == :create
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.match(model, method)
|
52
|
-
finder = :first
|
53
|
-
bang = false
|
54
|
-
|
55
|
-
case method.to_s
|
56
|
-
when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
|
57
|
-
finder = :last if $1 == 'last_by'
|
58
|
-
finder = :all if $1 == 'all_by'
|
59
|
-
names = $2
|
60
|
-
when /^find_by_([_a-zA-Z]\w*)\!$/
|
61
|
-
bang = true
|
62
|
-
names = $1
|
63
|
-
when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
|
64
|
-
finder = ($1 == 'initialize' ? :new : :create)
|
65
|
-
names = $2
|
66
|
-
else
|
67
|
-
return nil
|
68
|
-
end
|
69
|
-
|
70
|
-
names = names.split('_and_')
|
71
|
-
if names.all? { |n| model.properties.include?(n.to_sym) }
|
72
|
-
new(model, names, finder, bang)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
private
|
77
|
-
def build_conditions(args)
|
78
|
-
result = {}
|
79
|
-
|
80
|
-
@attribute_names.zip(args) do |attribute, value|
|
81
|
-
result[attribute.to_sym] = value
|
82
|
-
end
|
83
|
-
|
84
|
-
result
|
85
|
-
end
|
86
|
-
end
|
87
20
|
end
|
88
21
|
end
|
@@ -19,7 +19,7 @@ module MongoModel
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def delete
|
22
|
-
self.class.delete(id)
|
22
|
+
self.class.unscoped.delete(id)
|
23
23
|
set_destroyed(true)
|
24
24
|
freeze
|
25
25
|
end
|
@@ -65,14 +65,6 @@ module MongoModel
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
def delete(id_or_conditions)
|
69
|
-
collection.remove(MongoOptions.new(self, :conditions => id_to_conditions(id_or_conditions)).selector)
|
70
|
-
end
|
71
|
-
|
72
|
-
def destroy(id_or_conditions)
|
73
|
-
find(:all, :conditions => id_to_conditions(id_or_conditions)).each { |instance| instance.destroy }
|
74
|
-
end
|
75
|
-
|
76
68
|
def from_mongo(document)
|
77
69
|
instance = super
|
78
70
|
instance.send(:instantiate, document)
|
@@ -102,18 +94,6 @@ module MongoModel
|
|
102
94
|
def save_safely=(val)
|
103
95
|
@save_safely = val
|
104
96
|
end
|
105
|
-
|
106
|
-
private
|
107
|
-
def id_to_conditions(id_or_conditions)
|
108
|
-
case id_or_conditions
|
109
|
-
when String
|
110
|
-
{ :id => id_or_conditions }
|
111
|
-
when Array
|
112
|
-
{ :id.in => id_or_conditions }
|
113
|
-
else
|
114
|
-
id_or_conditions
|
115
|
-
end
|
116
|
-
end
|
117
97
|
end
|
118
98
|
|
119
99
|
private
|
@@ -1,161 +1,85 @@
|
|
1
|
-
require 'active_support/core_ext/module/aliasing'
|
2
|
-
require 'active_support/core_ext/module/delegation'
|
3
|
-
require 'active_support/core_ext/kernel/singleton_class'
|
4
|
-
|
5
1
|
module MongoModel
|
6
2
|
module DocumentExtensions
|
7
3
|
module Scopes
|
8
4
|
extend ActiveSupport::Concern
|
9
|
-
|
10
|
-
included do
|
11
|
-
class << self
|
12
|
-
[:find, :count].each do |method|
|
13
|
-
alias_method_chain method, :scope
|
14
|
-
end
|
15
|
-
end
|
16
5
|
|
17
|
-
|
6
|
+
delegate :current_scope, :to => "self.class"
|
7
|
+
|
8
|
+
def initialize(*)
|
9
|
+
self.attributes = current_scope.options_for_create
|
10
|
+
super
|
18
11
|
end
|
19
|
-
|
12
|
+
|
20
13
|
module ClassMethods
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
named_scopes[name] = options
|
14
|
+
delegate :find, :first, :last, :all, :exists?, :count, :to => :scoped
|
15
|
+
delegate :delete, :delete_all, :destroy, :destroy_all, :to => :scoped
|
16
|
+
delegate :select, :order, :where, :limit, :offset, :from, :to => :scoped
|
25
17
|
|
26
|
-
|
27
|
-
|
28
|
-
scope(name).apply(*args)
|
29
|
-
end
|
30
|
-
end
|
31
|
-
end
|
32
|
-
|
33
|
-
#
|
34
|
-
def default_scope(options={})
|
35
|
-
if options.empty?
|
36
|
-
read_inheritable_attribute(:default_scope) || write_inheritable_attribute(:default_scope, Scope.new(self))
|
37
|
-
else
|
38
|
-
write_inheritable_attribute(:default_scope, default_scope.merge(:find => options))
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
#
|
43
|
-
def with_scope(options={}, &block)
|
44
|
-
push_scope(current_scope.merge(options), &block)
|
18
|
+
def unscoped
|
19
|
+
@unscoped ||= MongoModel::Scope.new(self)
|
45
20
|
end
|
46
|
-
|
47
|
-
#
|
48
|
-
def with_exclusive_scope(options={}, &block)
|
49
|
-
push_scope(Scope.new(self, options), &block)
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
def push_scope(scope, &block)
|
54
|
-
scopes << scope
|
55
|
-
yield
|
56
|
-
ensure
|
57
|
-
scopes.pop
|
58
|
-
end
|
59
|
-
|
60
|
-
def find_with_scope(*args)
|
61
|
-
options = args.extract_options!
|
62
|
-
options = current_scope.options_for(:find).deep_merge(options)
|
63
21
|
|
64
|
-
|
22
|
+
def scoped
|
23
|
+
current_scope.clone
|
65
24
|
end
|
66
|
-
|
67
|
-
def count_with_scope(conditions={})
|
68
|
-
scope_conditions = current_scope.options_for(:find)[:conditions] || {}
|
69
25
|
|
70
|
-
|
26
|
+
def scopes
|
27
|
+
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
|
71
28
|
end
|
72
|
-
|
73
|
-
def
|
74
|
-
|
29
|
+
|
30
|
+
def scope(name, scope)
|
31
|
+
name = name.to_sym
|
32
|
+
|
33
|
+
if !scopes[name] && respond_to?(name, true)
|
34
|
+
logger.warn "Creating scope :#{name}. " \
|
35
|
+
"Overwriting existing method #{self.name}.#{name}."
|
36
|
+
end
|
37
|
+
|
38
|
+
scopes[name] = lambda do |*args|
|
39
|
+
scope.is_a?(Proc) ? scope.call(*args) : scope
|
40
|
+
end
|
41
|
+
|
42
|
+
singleton_class.class_eval do
|
43
|
+
define_method(name) do |*args|
|
44
|
+
scopes[name].call(*args)
|
45
|
+
end
|
46
|
+
end
|
75
47
|
end
|
76
|
-
|
77
|
-
def scope
|
78
|
-
|
48
|
+
|
49
|
+
def default_scope(scope)
|
50
|
+
reset_current_scopes
|
51
|
+
previous_scope = default_scoping.last || unscoped
|
52
|
+
default_scoping << previous_scope.merge(scope)
|
79
53
|
end
|
80
54
|
|
81
|
-
|
82
|
-
|
55
|
+
protected
|
56
|
+
def with_scope(scope, &block)
|
57
|
+
current_scopes << current_scope.merge(scope)
|
58
|
+
|
59
|
+
begin
|
60
|
+
yield
|
61
|
+
ensure
|
62
|
+
current_scopes.pop
|
63
|
+
end
|
83
64
|
end
|
84
|
-
|
65
|
+
|
66
|
+
private
|
85
67
|
def current_scope
|
86
|
-
|
68
|
+
current_scopes.last || unscoped
|
87
69
|
end
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
class Scope
|
92
|
-
attr_reader :model, :options
|
93
|
-
|
94
|
-
delegate :with_scope, :with_exclusive_scope, :scope, :to => :model
|
95
|
-
delegate :inspect, :to => :proxy_found
|
96
|
-
|
97
|
-
def initialize(model, options={})
|
98
|
-
if options.is_a?(Proc) || options.has_key?(:find) || options.has_key?(:create)
|
99
|
-
@options = options
|
100
|
-
else
|
101
|
-
@exclusive = options.delete(:exclusive)
|
102
|
-
@options = { :find => options }
|
70
|
+
|
71
|
+
def current_scopes
|
72
|
+
Thread.current[:"#{self}_scopes"] ||= default_scoping.dup
|
103
73
|
end
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
def merge(scope)
|
109
|
-
raise ArgumentError, "Scope must be applied before it can be merged" unless options.is_a?(Hash)
|
110
|
-
options_to_merge = scope.is_a?(self.class) ? scope.options : scope
|
111
|
-
self.class.new(model, options.deep_merge(options_to_merge))
|
112
|
-
end
|
113
|
-
|
114
|
-
def merge!(scope)
|
115
|
-
raise ArgumentError, "Scope must be applied before it can be merged" unless options.is_a?(Hash)
|
116
|
-
options_to_merge = scope.is_a?(self.class) ? scope.options : scope
|
117
|
-
options.deep_merge!(options_to_merge)
|
118
|
-
self
|
119
|
-
end
|
120
|
-
|
121
|
-
def reload
|
122
|
-
@found = nil
|
123
|
-
self
|
124
|
-
end
|
125
|
-
|
126
|
-
def apply(*args)
|
127
|
-
if options.is_a?(Hash)
|
128
|
-
reload
|
129
|
-
else
|
130
|
-
self.class.new(model, options.call(*args))
|
74
|
+
|
75
|
+
def reset_current_scopes
|
76
|
+
Thread.current[:"#{self}_scopes"] = nil
|
131
77
|
end
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
options[action] || {}
|
136
|
-
end
|
137
|
-
|
138
|
-
def exclusive?
|
139
|
-
@exclusive == true
|
140
|
-
end
|
141
|
-
|
142
|
-
protected
|
143
|
-
def proxy_found
|
144
|
-
@found ||= find(:all)
|
145
|
-
end
|
146
|
-
|
147
|
-
private
|
148
|
-
def method_missing(method, *args, &block)
|
149
|
-
if scope = scope(method)
|
150
|
-
scope.exclusive? ? scope : merge(scope.apply(*args))
|
151
|
-
else
|
152
|
-
send(scope_type, options) { model.send(method, *args, &block) }
|
78
|
+
|
79
|
+
def default_scoping
|
80
|
+
read_inheritable_attribute(:default_scoping) || write_inheritable_attribute(:default_scoping, [])
|
153
81
|
end
|
154
82
|
end
|
155
|
-
|
156
|
-
def scope_type
|
157
|
-
exclusive? ? :with_exclusive_scope : :with_scope
|
158
|
-
end
|
159
83
|
end
|
160
84
|
end
|
161
85
|
end
|