mongomodel 0.1.6 → 0.2.0

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 (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