mongomodel 0.1.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/README.md +6 -0
  2. data/bin/console +4 -4
  3. data/lib/mongomodel.rb +4 -4
  4. data/lib/mongomodel/concerns/associations/base/definition.rb +4 -0
  5. data/lib/mongomodel/concerns/associations/has_many_by_foreign_key.rb +7 -16
  6. data/lib/mongomodel/concerns/associations/has_many_by_ids.rb +6 -12
  7. data/lib/mongomodel/concerns/attributes.rb +1 -7
  8. data/lib/mongomodel/document.rb +0 -1
  9. data/lib/mongomodel/document/dynamic_finders.rb +2 -69
  10. data/lib/mongomodel/document/indexes.rb +0 -6
  11. data/lib/mongomodel/document/persistence.rb +1 -21
  12. data/lib/mongomodel/document/scopes.rb +59 -135
  13. data/lib/mongomodel/document/validations/uniqueness.rb +7 -5
  14. data/lib/mongomodel/support/dynamic_finder.rb +68 -0
  15. data/lib/mongomodel/support/mongo_operator.rb +29 -0
  16. data/lib/mongomodel/support/mongo_options.rb +0 -101
  17. data/lib/mongomodel/support/mongo_order.rb +78 -0
  18. data/lib/mongomodel/support/scope.rb +186 -0
  19. data/lib/mongomodel/support/scope/dynamic_finders.rb +21 -0
  20. data/lib/mongomodel/support/scope/finder_methods.rb +61 -0
  21. data/lib/mongomodel/support/scope/query_methods.rb +43 -0
  22. data/lib/mongomodel/support/scope/spawn_methods.rb +35 -0
  23. data/lib/mongomodel/version.rb +1 -1
  24. data/mongomodel.gemspec +20 -3
  25. data/spec/mongomodel/concerns/associations/has_many_by_foreign_key_spec.rb +1 -1
  26. data/spec/mongomodel/concerns/associations/has_many_by_ids_spec.rb +1 -1
  27. data/spec/mongomodel/document/dynamic_finders_spec.rb +0 -1
  28. data/spec/mongomodel/document/finders_spec.rb +0 -144
  29. data/spec/mongomodel/document/indexes_spec.rb +2 -2
  30. data/spec/mongomodel/document/persistence_spec.rb +1 -15
  31. data/spec/mongomodel/document/scopes_spec.rb +64 -167
  32. data/spec/mongomodel/support/mongo_operator_spec.rb +29 -0
  33. data/spec/mongomodel/support/mongo_options_spec.rb +0 -150
  34. data/spec/mongomodel/support/mongo_order_spec.rb +127 -0
  35. data/spec/mongomodel/support/scope_spec.rb +932 -0
  36. data/spec/support/helpers/document_finder_stubs.rb +40 -0
  37. data/spec/support/matchers/find_with.rb +36 -0
  38. metadata +22 -5
  39. 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
 
@@ -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 :order => :title.asc
39
+ default_scope order(:title.asc)
40
40
 
41
- named_scope :published, :conditions => { :published => true }
42
- named_scope :latest, lambda { |num| { :limit => num, :order => 'created_at DESC' } }
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
@@ -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/mongo_options'
33
- autoload :MongoOperator, 'mongomodel/support/mongo_options'
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
@@ -38,6 +38,10 @@ module MongoModel
38
38
  options[:polymorphic]
39
39
  end
40
40
 
41
+ def scope
42
+ klass.scoped.apply_finder_options(scope_options)
43
+ end
44
+
41
45
  def scope_options
42
46
  options.slice(:conditions, :select, :offset, :limit, :order)
43
47
  end
@@ -23,17 +23,17 @@ module MongoModel
23
23
  delegate :foreign_key, :inverse_of, :to => :definition
24
24
 
25
25
  def find_target
26
- send_to_klass_with_scope(:all) + new_documents
26
+ scoped + new_documents
27
27
  end
28
28
 
29
29
  def build(*args, &block)
30
- doc = klass.new(*args, &block)
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
- klass.create(*args) do |doc|
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
- klass.create!(*args) do |doc|
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 send_to_klass_with_scope(*args, &block)
86
- scope_options = definition.scope_options
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.send_to_klass_with_scope(method_id, *args, &block)
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(klass.find(*(ids - new_document_ids))) + new_documents : []
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 = klass.new(*args, &block)
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
- klass.create(*args, &block)
42
+ scoped.create(*args, &block)
43
43
  end
44
44
 
45
- def send_to_klass_with_scope(*args, &block)
46
- scope_ids = ids
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.send_to_klass_with_scope(method_id, *args, &block)
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
- included do
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
@@ -7,7 +7,6 @@ module MongoModel
7
7
  include DocumentExtensions::Persistence
8
8
  include DocumentExtensions::OptimisticLocking
9
9
 
10
- extend DocumentExtensions::Finders
11
10
  extend DocumentExtensions::DynamicFinders
12
11
  include DocumentExtensions::Indexes
13
12
 
@@ -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(self, method_id)
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(self, method_id)
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
@@ -38,12 +38,6 @@ module MongoModel
38
38
 
39
39
  @_indexes_initialized = true
40
40
  end
41
-
42
- private
43
- def _find(*)
44
- ensure_indexes! unless indexes_initialized?
45
- super
46
- end
47
41
  end
48
42
  end
49
43
  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
- named_scope :scoped, lambda { |scope| scope }
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
- protected
22
- #
23
- def named_scope(name, options={})
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
- singleton_class.instance_eval do
27
- define_method(name) do |*args|
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
- find_without_scope(*(args << options))
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
- count_without_scope(scope_conditions.deep_merge(conditions))
26
+ def scopes
27
+ read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
71
28
  end
72
-
73
- def named_scopes
74
- read_inheritable_attribute(:named_scopes) || write_inheritable_attribute(:named_scopes, {})
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(name)
78
- Scope.new(self, named_scopes[name]) if named_scopes[name]
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
- def scopes
82
- Thread.current[:"#{self}_scopes"] ||= [ default_scope.dup ]
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
- scopes.last
68
+ current_scopes.last || unscoped
87
69
  end
88
- end
89
- end
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
- @model = model
106
- end
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
- end
133
-
134
- def options_for(action=:find)
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