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
@@ -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
|
-
|
61
|
+
unique_scope = unique_scope.where(attr_name => value)
|
60
62
|
else
|
61
|
-
|
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
|
-
|
67
|
+
unique_scope = unique_scope.where(scope => record.send(scope))
|
66
68
|
end
|
67
69
|
|
68
|
-
|
70
|
+
unique_scope = unique_scope.where(:id.ne => record.id) unless record.new_record?
|
69
71
|
|
70
|
-
if
|
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
|