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