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