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
@@ -55,19 +55,21 @@ module MongoModel
55
55
  end
56
56
 
57
57
  validates_each(attr_names, configuration) do |record, attr_name, value|
58
+ unique_scope = scoped
59
+
58
60
  if configuration[:case_sensitive] || !value.is_a?(String)
59
- conditions = { attr_name => value }
61
+ unique_scope = unique_scope.where(attr_name => value)
60
62
  else
61
- conditions = { "_lowercase_#{attr_name}" => value.downcase }
63
+ unique_scope = unique_scope.where("_lowercase_#{attr_name}" => value.downcase)
62
64
  end
63
65
 
64
66
  Array(configuration[:scope]).each do |scope|
65
- conditions[scope] = record.send(scope)
67
+ unique_scope = unique_scope.where(scope => record.send(scope))
66
68
  end
67
69
 
68
- conditions.merge!(:id.ne => record.id) unless record.new_record?
70
+ unique_scope = unique_scope.where(:id.ne => record.id) unless record.new_record?
69
71
 
70
- if exists?(conditions)
72
+ if unique_scope.any?
71
73
  record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
72
74
  end
73
75
  end
@@ -0,0 +1,68 @@
1
+ module MongoModel
2
+ class DynamicFinder
3
+ def initialize(scope, attribute_names, finder=:first, bang=false)
4
+ @scope, @attribute_names, @finder, @bang = scope, attribute_names, finder, bang
5
+ end
6
+
7
+ def execute(*args)
8
+ options = args.extract_options!
9
+ conditions = build_conditions(args)
10
+
11
+ result = @scope.where(conditions).send(instantiator? ? :first : @finder)
12
+
13
+ if result.nil?
14
+ if bang?
15
+ raise DocumentNotFound, "Couldn't find #{@scope.klass.to_s} with #{conditions.inspect}"
16
+ elsif instantiator?
17
+ return @scope.send(@finder, conditions)
18
+ end
19
+ end
20
+
21
+ result
22
+ end
23
+
24
+ def bang?
25
+ @bang
26
+ end
27
+
28
+ def instantiator?
29
+ @finder == :new || @finder == :create
30
+ end
31
+
32
+ def self.match(scope, method)
33
+ finder = :first
34
+ bang = false
35
+
36
+ case method.to_s
37
+ when /^find_(all_by|last_by|by)_([_a-zA-Z]\w*)$/
38
+ finder = :last if $1 == 'last_by'
39
+ finder = :all if $1 == 'all_by'
40
+ names = $2
41
+ when /^find_by_([_a-zA-Z]\w*)\!$/
42
+ bang = true
43
+ names = $1
44
+ when /^find_or_(initialize|create)_by_([_a-zA-Z]\w*)$/
45
+ finder = ($1 == 'initialize' ? :new : :create)
46
+ names = $2
47
+ else
48
+ return nil
49
+ end
50
+
51
+ names = names.split('_and_')
52
+ if names.all? { |n| scope.klass.properties.include?(n.to_sym) }
53
+ new(scope, names, finder, bang)
54
+ end
55
+ end
56
+
57
+ private
58
+ def build_conditions(args)
59
+ result = {}
60
+
61
+ @attribute_names.zip(args) do |attribute, value|
62
+ result[attribute.to_sym] = value
63
+ end
64
+
65
+ result
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,29 @@
1
+ module MongoModel
2
+ class MongoOperator
3
+ attr_reader :field, :operator
4
+
5
+ def initialize(field, operator)
6
+ @field, @operator = field, operator
7
+ end
8
+
9
+ def to_mongo_selector(value)
10
+ { "$#{operator}" => value }
11
+ end
12
+
13
+ def inspect
14
+ "#{field.inspect}.#{operator}"
15
+ end
16
+
17
+ def ==(other)
18
+ other.is_a?(self.class) && field == other.field && operator == other.operator
19
+ end
20
+
21
+ def hash
22
+ field.hash ^ operator.hash
23
+ end
24
+
25
+ def eql?(other)
26
+ self == other
27
+ end
28
+ end
29
+ end
@@ -75,105 +75,4 @@ module MongoModel
75
75
  end
76
76
  end
77
77
  end
78
-
79
- class MongoOrder
80
- attr_reader :clauses
81
-
82
- def initialize(*clauses)
83
- @clauses = clauses
84
- end
85
-
86
- def to_s
87
- clauses.map { |c| c.to_s }.join(', ')
88
- end
89
-
90
- def to_sort(model)
91
- clauses.map { |c| c.to_sort(model.properties[c.field]) }
92
- end
93
-
94
- def ==(other)
95
- other.is_a?(self.class) && clauses == other.clauses
96
- end
97
-
98
- def reverse
99
- self.class.new(*clauses.map { |c| c.reverse })
100
- end
101
-
102
- def self.parse(order)
103
- case order
104
- when MongoOrder
105
- order
106
- when Clause
107
- new(order)
108
- when Symbol
109
- new(Clause.new(order))
110
- when String
111
- new(*order.split(',').map { |c| Clause.parse(c) })
112
- when Array
113
- new(*order.map { |c| Clause.parse(c) })
114
- end
115
- end
116
-
117
- class Clause
118
- attr_reader :field, :order
119
-
120
- def initialize(field, order=:ascending)
121
- @field, @order = field.to_sym, order.to_sym
122
- end
123
-
124
- def to_s
125
- "#{field} #{order}"
126
- end
127
-
128
- def to_sort(property)
129
- [property ? property.as : field.to_s, order]
130
- end
131
-
132
- def reverse
133
- self.class.new(field, order == :ascending ? :descending : :ascending)
134
- end
135
-
136
- def ==(other)
137
- other.is_a?(self.class) && field == other.field && order == other.order
138
- end
139
-
140
- def self.parse(clause)
141
- case clause
142
- when Clause
143
- clause
144
- when String, Symbol
145
- field, order = clause.to_s.strip.split(/ /)
146
- new(field, order =~ /^desc/i ? :descending : :ascending)
147
- end
148
- end
149
- end
150
- end
151
-
152
- class MongoOperator
153
- attr_reader :field, :operator
154
-
155
- def initialize(field, operator)
156
- @field, @operator = field, operator
157
- end
158
-
159
- def to_mongo_selector(value)
160
- { "$#{operator}" => value }
161
- end
162
-
163
- def inspect
164
- "#{field.inspect}.#{operator}"
165
- end
166
-
167
- def ==(other)
168
- other.is_a?(self.class) && field == other.field && operator == other.operator
169
- end
170
-
171
- def hash
172
- field.hash ^ operator.hash
173
- end
174
-
175
- def eql?(other)
176
- self == other
177
- end
178
- end
179
78
  end
@@ -0,0 +1,78 @@
1
+ module MongoModel
2
+ class MongoOrder
3
+ attr_reader :clauses
4
+
5
+ def initialize(*clauses)
6
+ @clauses = clauses
7
+ end
8
+
9
+ def to_a
10
+ clauses
11
+ end
12
+
13
+ def to_s
14
+ clauses.map { |c| c.to_s }.join(', ')
15
+ end
16
+
17
+ def to_sort(model)
18
+ clauses.map { |c| c.to_sort(model.properties[c.field]) }
19
+ end
20
+
21
+ def ==(other)
22
+ other.is_a?(self.class) && clauses == other.clauses
23
+ end
24
+
25
+ def reverse
26
+ self.class.new(*clauses.map { |c| c.reverse })
27
+ end
28
+
29
+ def self.parse(order)
30
+ case order
31
+ when MongoOrder
32
+ order
33
+ when Clause
34
+ new(order)
35
+ when Symbol
36
+ new(Clause.new(order))
37
+ when String
38
+ new(*order.split(',').map { |c| Clause.parse(c) })
39
+ when Array
40
+ new(*order.map { |c| Clause.parse(c) })
41
+ end
42
+ end
43
+
44
+ class Clause
45
+ attr_reader :field, :order
46
+
47
+ def initialize(field, order=:ascending)
48
+ @field, @order = field.to_sym, order.to_sym
49
+ end
50
+
51
+ def to_s
52
+ "#{field} #{order}"
53
+ end
54
+
55
+ def to_sort(property)
56
+ [property ? property.as : field.to_s, order]
57
+ end
58
+
59
+ def reverse
60
+ self.class.new(field, order == :ascending ? :descending : :ascending)
61
+ end
62
+
63
+ def ==(other)
64
+ other.is_a?(self.class) && field == other.field && order == other.order
65
+ end
66
+
67
+ def self.parse(clause)
68
+ case clause
69
+ when Clause
70
+ clause
71
+ when String, Symbol
72
+ field, order = clause.to_s.strip.split(/ /)
73
+ new(field, order =~ /^desc/i ? :descending : :ascending)
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,186 @@
1
+ require 'active_support/core_ext/module/delegation'
2
+
3
+ module MongoModel
4
+ class Scope
5
+ MULTI_VALUE_METHODS = [ :select, :order, :where ]
6
+ SINGLE_VALUE_METHODS = [ :limit, :offset, :from ]
7
+
8
+ autoload :SpawnMethods, 'mongomodel/support/scope/spawn_methods'
9
+ autoload :QueryMethods, 'mongomodel/support/scope/query_methods'
10
+ autoload :FinderMethods, 'mongomodel/support/scope/finder_methods'
11
+ autoload :DynamicFinders, 'mongomodel/support/scope/dynamic_finders'
12
+
13
+ include DynamicFinders, FinderMethods, QueryMethods, SpawnMethods
14
+
15
+ delegate :inspect, :to => :to_a
16
+
17
+ attr_reader :klass
18
+
19
+ def initialize(klass)
20
+ super
21
+
22
+ @klass = klass
23
+
24
+ @loaded = false
25
+ @documents = []
26
+ end
27
+
28
+ def initialize_copy(other)
29
+ reset
30
+ end
31
+
32
+ def build(*args, &block)
33
+ new(*args, &block)
34
+ end
35
+
36
+ def to_a
37
+ return @documents if loaded?
38
+
39
+ @documents = _find_and_instantiate
40
+ @loaded = true
41
+
42
+ @documents
43
+ end
44
+
45
+ def size
46
+ loaded? ? @documents.size : count
47
+ end
48
+
49
+ def empty?
50
+ loaded? ? @documents.empty? : count.zero?
51
+ end
52
+
53
+ def any?(&block)
54
+ if block_given?
55
+ to_a.any?(&block)
56
+ else
57
+ !empty?
58
+ end
59
+ end
60
+
61
+ def count
62
+ _find.count
63
+ end
64
+
65
+ def destroy_all
66
+ to_a.each { |doc| doc.destroy }
67
+ reset
68
+ end
69
+
70
+ def destroy(*ids)
71
+ where(ids_to_conditions(ids)).destroy_all
72
+ reset
73
+ end
74
+
75
+ def delete_all
76
+ selector = MongoOptions.new(klass, :conditions => finder_conditions).selector
77
+ collection.remove(selector)
78
+ reset
79
+ end
80
+
81
+ def delete(*ids)
82
+ where(ids_to_conditions(ids)).delete_all
83
+ reset
84
+ end
85
+
86
+ def loaded?
87
+ @loaded
88
+ end
89
+
90
+ def reload
91
+ reset
92
+ to_a
93
+ self
94
+ end
95
+
96
+ def reset
97
+ @loaded = nil
98
+ @documents = []
99
+ self
100
+ end
101
+
102
+ def ==(other)
103
+ case other
104
+ when Scope
105
+ klass == other.klass &&
106
+ collection == other.collection &&
107
+ finder_options == other.finder_options
108
+ when Array
109
+ to_a == other.to_a
110
+ end
111
+ end
112
+
113
+ def collection
114
+ from_value || klass.collection
115
+ end
116
+
117
+ def finder_options
118
+ @finder_options ||= begin
119
+ result = {}
120
+
121
+ result[:conditions] = finder_conditions if where_values.any?
122
+ result[:select] = select_values if select_values.any?
123
+ result[:order] = order_values if order_values.any?
124
+ result[:limit] = limit_value if limit_value.present?
125
+ result[:offset] = offset_value if offset_value.present?
126
+
127
+ result
128
+ end
129
+ end
130
+
131
+ def options_for_create
132
+ @options_for_create ||= begin
133
+ result = {}
134
+
135
+ finder_conditions.each do |k, v|
136
+ result[k] = v unless k.is_a?(MongoModel::MongoOperator)
137
+ end
138
+
139
+ result
140
+ end
141
+ end
142
+
143
+ protected
144
+ def method_missing(method, *args, &block)
145
+ if Array.method_defined?(method)
146
+ to_a.send(method, *args, &block)
147
+ elsif klass.scopes[method]
148
+ merge(klass.send(method, *args, &block))
149
+ elsif klass.respond_to?(method)
150
+ with_scope { klass.send(method, *args, &block) }
151
+ else
152
+ super
153
+ end
154
+ end
155
+
156
+ private
157
+ def _find
158
+ klass.ensure_indexes! unless klass.indexes_initialized?
159
+
160
+ selector, options = MongoOptions.new(klass, finder_options).to_a
161
+ collection.find(selector, options)
162
+ end
163
+
164
+ def _find_and_instantiate
165
+ _find.to_a.map { |doc| klass.from_mongo(doc) }
166
+ end
167
+
168
+ def finder_conditions
169
+ where_values.inject({}) { |conditions, v| conditions.merge(v) }
170
+ end
171
+
172
+ def with_scope(&block)
173
+ klass.send(:with_scope, self, &block)
174
+ end
175
+
176
+ def ids_to_conditions(ids)
177
+ ids.flatten!
178
+
179
+ if ids.size == 1
180
+ { :id => ids.first.to_s }
181
+ else
182
+ { :id.in => ids.map { |id| id.to_s } }
183
+ end
184
+ end
185
+ end
186
+ end