magic_scopes 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +55 -0
- data/Rakefile +34 -0
- data/lib/magic_scopes.rb +28 -0
- data/lib/magic_scopes/railtie.rb +11 -0
- data/lib/magic_scopes/scopes_builder.rb +203 -0
- data/lib/magic_scopes/scopes_generators/association.rb +76 -0
- data/lib/magic_scopes/scopes_generators/base.rb +27 -0
- data/lib/magic_scopes/scopes_generators/boolean.rb +14 -0
- data/lib/magic_scopes/scopes_generators/float.rb +7 -0
- data/lib/magic_scopes/scopes_generators/integer.rb +27 -0
- data/lib/magic_scopes/scopes_generators/mixins/comparison_scopes.rb +11 -0
- data/lib/magic_scopes/scopes_generators/mixins/equality_scopes.rb +30 -0
- data/lib/magic_scopes/scopes_generators/mixins/order_scopes.rb +11 -0
- data/lib/magic_scopes/scopes_generators/mixins/presence_scopes.rb +11 -0
- data/lib/magic_scopes/scopes_generators/state.rb +35 -0
- data/lib/magic_scopes/scopes_generators/string.rb +40 -0
- data/lib/magic_scopes/standard_scopes.rb +31 -0
- data/lib/magic_scopes/version.rb +3 -0
- metadata +177 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2012 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
= MagicScopes
|
2
|
+
|
3
|
+
MagicScopes is the scopes generator for ActiveRecord.
|
4
|
+
|
5
|
+
It adds magic_scopes method (and nothing else) to ActiveRecord::Base.
|
6
|
+
|
7
|
+
It depends on ActiveRecord and ActiveSupport and can be used without Rails.
|
8
|
+
MagicScopes module should be included manually in the latter case.
|
9
|
+
|
10
|
+
It also has support for the StateMachine gem and can generate scopes for states.
|
11
|
+
|
12
|
+
|
13
|
+
== MagicScopes can generate these scopes:
|
14
|
+
|
15
|
+
* <b>with</b> and <b>without</b> options will generate scopes such as <b>with_rating</b>, <b>without_rating</b> that will look for non null and null values, respectively. For state attributes thee redefine corresponding state_machine scopes so they can be called without parameters to check against null and non null. (You can use it in the normal way too, of course). These scopes can be used with boolean, state, integer, decimal, time, date, datetime, float and string attributes. For integer, decimal, time, date, datetime and string attributes value(s) can be passed to the scope so the check will be performed against passed values (not against null/non null vakues). (ex.: with_rating(1,2,3), without_rating([5,4,3]))
|
16
|
+
* <b>is</b> and <b>not</b> options will generate scopes such as <b>published</b>, <b>not_published</b> ("is" is the one option that is not reflected in the name of the generated scope) that look for true and and false or null values for boolean attributes and for the specified state and for state that is not the specifieed one no null for state attributes.
|
17
|
+
* <b>eq</b> (stands for equal), <b>ne</b> (not equal), <b>gt</b> (greater than), <b>gte</b> (greater than or equal), <b>lt</b> (lesser than), <b>lte</b> (lesser than or equal) options generate scopes with corresponding suffixes and accept list of values or array as parameteters. Options names are self explanatory. These scopes can be used with integer, decimal, time, date and datetime attributes. lt and gt options can be used with float attributes.
|
18
|
+
* for string attributes <b>like</b> and <b>ilike</b> options can be specified which generate scopes like <b>title_like</b>, <b>title_ilike</b> which accept list or array of arguments and look for values like the specified ones, case sensitive and insensitive, respectively.
|
19
|
+
* <b>by</b> and <b>by_desc</b> options can be used with integer, decimal, time, date, datetime, float and string attributes and generate scopes like <b>by_created_at</b> and <b>by_created_at_desc</b>, which sort by created at asc and created at desc, respectively.
|
20
|
+
* <b>for</b> and <b>not_for</b> scopes can be used with belongs_to associations (including polymorphic ones) and generate scopes like <b>for_user</b>, <b>not_for_commentable</b>. They can accept list or array of associations. Also, hash(es) with id and type keys can be specified for polymorphic associations.
|
21
|
+
|
22
|
+
|
23
|
+
== Usage examples
|
24
|
+
|
25
|
+
* Call to magic_scopes without parameters will generate all possible scopes for all attributes, belongs_to associations and states. By specifying parameters you tell magic_scopes to generate only the scopes you need.
|
26
|
+
magic_scopes
|
27
|
+
|
28
|
+
* With specified attributes it will generate all possible scopes for title and bogus attributes.
|
29
|
+
magic_scopes :title, :bogus
|
30
|
+
|
31
|
+
* You can specify in (stands for include) and ex (for exclude) options, so:
|
32
|
+
* it will generate with and by scopes for all suitable attributes when in option is specified.
|
33
|
+
magic_scopes in: %w(with by)
|
34
|
+
* It will generate generate all possible scopes except with and by ones when ex option is specified.
|
35
|
+
magic_scopes ex: %w(with by)
|
36
|
+
* It will generate with and by scopes for all suitable attributes in specified list when both attributes list and in options are cpecified:
|
37
|
+
magic_scopes :title, :bogus, in: %w(with by)
|
38
|
+
* You can also override scopes that should be generated per attribute:
|
39
|
+
magic_scopes :title, :rating, bogus: :by_desc, in: %w(with by)
|
40
|
+
It will generate with and by scopes for title and rating attributes and by_desc scope for bogus attribute.
|
41
|
+
|
42
|
+
* All options accept strings, symbols and arrays as arguments.
|
43
|
+
|
44
|
+
|
45
|
+
== Standard scopes
|
46
|
+
|
47
|
+
In addition, some standard scopes can be specified using :std option, they are:
|
48
|
+
asc and sorted sort by id asc, desc and recent sort by id desc and random sorts in random order.
|
49
|
+
ex.: magic_scopes :title, :bogus, in: %w(with without), std: %w(recent rendom)
|
50
|
+
magic_scopes generate all possible standard scopes unless std option is specified.
|
51
|
+
|
52
|
+
MagicScopes is thoroughly tested and ready for production.
|
53
|
+
It works with ActiveRecord >= 3.0 and ruby 1.9. (1.8 is not supported).
|
54
|
+
|
55
|
+
This project rocks and uses MIT-LICENSE.
|
data/Rakefile
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
begin
|
3
|
+
require 'bundler/setup'
|
4
|
+
rescue LoadError
|
5
|
+
puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
|
6
|
+
end
|
7
|
+
begin
|
8
|
+
require 'rdoc/task'
|
9
|
+
rescue LoadError
|
10
|
+
require 'rdoc/rdoc'
|
11
|
+
require 'rake/rdoctask'
|
12
|
+
RDoc::Task = Rake::RDocTask
|
13
|
+
end
|
14
|
+
|
15
|
+
RDoc::Task.new(:rdoc) do |rdoc|
|
16
|
+
rdoc.rdoc_dir = 'rdoc'
|
17
|
+
rdoc.title = 'MagicScopes'
|
18
|
+
rdoc.options << '--line-numbers'
|
19
|
+
rdoc.rdoc_files.include('README.rdoc')
|
20
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
21
|
+
end
|
22
|
+
|
23
|
+
Bundler::GemHelper.install_tasks
|
24
|
+
|
25
|
+
APP_RAKEFILE = File.expand_path('../spec/dummy/Rakefile', __FILE__)
|
26
|
+
load 'rails/tasks/engine.rake'
|
27
|
+
|
28
|
+
require 'rspec/core/rake_task'
|
29
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
30
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'Default: Run all specs.'
|
34
|
+
task default: :spec
|
data/lib/magic_scopes.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'magic_scopes/standard_scopes'
|
2
|
+
require 'magic_scopes/scopes_builder'
|
3
|
+
require 'magic_scopes/scopes_generators/mixins/order_scopes'
|
4
|
+
require 'magic_scopes/scopes_generators/mixins/comparison_scopes'
|
5
|
+
require 'magic_scopes/scopes_generators/mixins/equality_scopes'
|
6
|
+
require 'magic_scopes/scopes_generators/mixins/presence_scopes'
|
7
|
+
require 'magic_scopes/scopes_generators/base'
|
8
|
+
require 'magic_scopes/scopes_generators/boolean'
|
9
|
+
require 'magic_scopes/scopes_generators/integer'
|
10
|
+
require 'magic_scopes/scopes_generators/string'
|
11
|
+
require 'magic_scopes/scopes_generators/float'
|
12
|
+
require 'magic_scopes/scopes_generators/association'
|
13
|
+
require 'magic_scopes/scopes_generators/state'
|
14
|
+
require 'magic_scopes/version'
|
15
|
+
require 'magic_scopes/railtie' if defined?(Rails)
|
16
|
+
|
17
|
+
|
18
|
+
module MagicScopes
|
19
|
+
extend ActiveSupport::Concern
|
20
|
+
|
21
|
+
module ClassMethods
|
22
|
+
def magic_scopes(*attrs)
|
23
|
+
builder = ScopesBuilder.new(self, *attrs)
|
24
|
+
builder.generate_standard_scopes
|
25
|
+
builder.generate_scopes
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,203 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
class ScopesBuilder
|
3
|
+
|
4
|
+
include MagicScopes::StandardScopes
|
5
|
+
|
6
|
+
STANDARD_OPTIONS = [:std, :in, :ex]
|
7
|
+
|
8
|
+
MAGIC_SCOPES = {
|
9
|
+
boolean: [:is, :not, :with, :without],
|
10
|
+
integer: [:with, :without, :eq, :ne, :gt, :gte, :lt, :lte, :by, :by_desc],
|
11
|
+
float: [:with, :without, :lt, :gt, :by, :by_desc],
|
12
|
+
string: [:with, :without, :eq, :ne, :by, :by_desc, :like, :not_like, :ilike, :not_ilike],
|
13
|
+
association: [:for, :not_for]
|
14
|
+
}
|
15
|
+
MAGIC_SCOPES[:state] = MAGIC_SCOPES[:boolean]
|
16
|
+
MAGIC_SCOPES[:decimal] = MAGIC_SCOPES[:time] = MAGIC_SCOPES[:datetime] = MAGIC_SCOPES[:date] = MAGIC_SCOPES[:integer]
|
17
|
+
MAGIC_SCOPES[:text] = MAGIC_SCOPES[:string]
|
18
|
+
|
19
|
+
delegate :scope, :order, :columns_hash, :reflections, :state_machines, to: :@model
|
20
|
+
|
21
|
+
def initialize(model, *attrs)
|
22
|
+
@model = model
|
23
|
+
@options = attrs.extract_options!.symbolize_keys
|
24
|
+
check_options
|
25
|
+
@attributes_with_scopes = extract_attributes_with_scopes
|
26
|
+
@attributes = make_attributes(attrs)
|
27
|
+
@is_attributes_passed = attrs.present? || @attributes_with_scopes.present?
|
28
|
+
@needed_scopes = make_needed_scopes
|
29
|
+
end
|
30
|
+
|
31
|
+
def generate_standard_scopes
|
32
|
+
(@options[:std] || STANDARD_SCOPES).each { |scope_type| send("#{scope_type}_scope") }
|
33
|
+
end
|
34
|
+
|
35
|
+
def generate_scopes
|
36
|
+
@attributes.inject({}) { |hsh, attr| hsh[attr] = generate_scopes_for_attr(attr, @needed_scopes); hsh }.merge(
|
37
|
+
@attributes_with_scopes.inject({}) { |hsh, (attr, attr_scopes)| hsh[attr] = generate_scopes_for_attr(attr, attr_scopes); hsh }
|
38
|
+
)
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def generate_scopes_for_attr(attr, scopes)
|
45
|
+
scopes.inject([]) do |ar, scope_info|
|
46
|
+
scope_info = Array.wrap(scope_info)
|
47
|
+
if has_scope_generator_for?(attr, scope_info[0])
|
48
|
+
generate_scope_for(attr, scope_info[0], scope_info[1] != scope_info[0] ? scope_info[1] : nil)
|
49
|
+
ar << scope_info[0]
|
50
|
+
end
|
51
|
+
ar
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def check_options
|
56
|
+
@options.assert_valid_keys(*valid_options)
|
57
|
+
|
58
|
+
if @options[:in] && @options[:ex]
|
59
|
+
raise ArgumentError, "In(clude) and ex(clude) options can not be specified simultaneously"
|
60
|
+
end
|
61
|
+
|
62
|
+
if @options[:std] && option = @options[:std].find { |opt| STANDARD_SCOPES.exclude?(opt.to_sym) }
|
63
|
+
raise ArgumentError, "Unknown option #{option} passed to magic_scopes#std"
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def extract_attributes_with_scopes
|
68
|
+
attributes_with_scopes = @options.inject({}) do |hsh, (option_key, options)|
|
69
|
+
unless option_key.in?(STANDARD_OPTIONS)
|
70
|
+
hsh[option_key] = unless options.is_a?(Hash)
|
71
|
+
Array.wrap(@options.delete(option_key)).inject({}) { |hsh, option| hsh[option.to_sym] = option.to_sym; hsh }
|
72
|
+
else
|
73
|
+
options
|
74
|
+
end
|
75
|
+
end
|
76
|
+
hsh
|
77
|
+
end
|
78
|
+
extract_states_from_attrs!(attributes_with_scopes)
|
79
|
+
|
80
|
+
wrong_scope = nil
|
81
|
+
if wrong_attr = attributes_with_scopes.find do |attr, scopes_hash|
|
82
|
+
wrong_scope = scopes_hash.find { |scope_type, _| scope_types(attr).exclude?(scope_type.to_sym) }
|
83
|
+
end
|
84
|
+
raise ArgumentError, "Unknown scope #{wrong_scope} for attribute #{wrong_attr[0]} passed to magic_scopes"
|
85
|
+
end
|
86
|
+
|
87
|
+
attributes_with_scopes
|
88
|
+
end
|
89
|
+
|
90
|
+
def valid_options
|
91
|
+
@valid_options ||= (STANDARD_OPTIONS + columns_hash.keys + reflections.keys).map(&:to_sym)
|
92
|
+
end
|
93
|
+
|
94
|
+
def scope_types(*attrs)
|
95
|
+
filtered_scopes = if attrs.empty?
|
96
|
+
MAGIC_SCOPES
|
97
|
+
else
|
98
|
+
needed_attr_types = attrs.map { |attr| attrs_with_types[attr] }
|
99
|
+
MAGIC_SCOPES.select { |k, _| k.in?(needed_attr_types) }
|
100
|
+
end
|
101
|
+
filtered_scopes.values.flatten.uniq
|
102
|
+
end
|
103
|
+
|
104
|
+
def make_attributes(attrs)
|
105
|
+
attributes = if attrs.empty?
|
106
|
+
all_possible_attributes - @attributes_with_scopes.keys
|
107
|
+
else
|
108
|
+
extract_states_from_attrs(attrs.map(&:to_sym))
|
109
|
+
end
|
110
|
+
|
111
|
+
if wrong_attr = attributes.find { |attr| all_possible_attributes.exclude?(attr) }
|
112
|
+
raise ActiveRecord::UnknownAttributeError, "Unknown attribute #{wrong_attr} passed to magic_scopes"
|
113
|
+
end
|
114
|
+
if attr = attributes.find { |attr| @attributes_with_scopes.keys.include?(attr) }
|
115
|
+
raise ArgumentError, "Attribute #{attr} specified using both list and hash"
|
116
|
+
end
|
117
|
+
attributes
|
118
|
+
end
|
119
|
+
|
120
|
+
def make_needed_scopes
|
121
|
+
scopes_options = @options[:in] || @options[:ex]
|
122
|
+
scopes_options = if scopes_options
|
123
|
+
scopes_options = Array.wrap(scopes_options).map(&:to_sym)
|
124
|
+
if wrong_scope = scopes_options.find { |scope_type| scope_types.exclude?(scope_type) }
|
125
|
+
raise ArgumentError, "Unknown scope #{wrong_scope} passed to magic_scopes"
|
126
|
+
end
|
127
|
+
scopes_options
|
128
|
+
end
|
129
|
+
|
130
|
+
needed_scopes = if @options[:in]
|
131
|
+
scopes_options
|
132
|
+
elsif @options[:ex]
|
133
|
+
scope_types(*@attributes) - scopes_options
|
134
|
+
else
|
135
|
+
scope_types
|
136
|
+
end
|
137
|
+
|
138
|
+
if scopes_options && @is_attributes_passed && wrong_scope = check_for_wrong_scope(needed_scopes)
|
139
|
+
raise ArgumentError, "Can not build scope #{wrong_scope} for all passed attributes"
|
140
|
+
end
|
141
|
+
|
142
|
+
needed_scopes
|
143
|
+
end
|
144
|
+
|
145
|
+
def check_for_wrong_scope(scopes)
|
146
|
+
scopes.find { |scope_type| @attributes.any? { |attr| !has_scope_generator_for?(attr, scope_type) } }
|
147
|
+
end
|
148
|
+
|
149
|
+
def generate_scope_for(attr, scope_type, scope_name = nil)
|
150
|
+
type = type_for_attr(attr)
|
151
|
+
"MagicScopes::#{type.to_s.classify}ScopesGenerator".constantize.instance(@model, attr).send(scope_type, scope_name)
|
152
|
+
end
|
153
|
+
|
154
|
+
def has_scope_generator_for?(attr, scope_type)
|
155
|
+
scope_type.in?(MAGIC_SCOPES[type_for_attr(attr)])
|
156
|
+
end
|
157
|
+
|
158
|
+
def type_for_attr(attr)
|
159
|
+
attrs_with_types[attr]
|
160
|
+
end
|
161
|
+
|
162
|
+
def all_possible_attributes
|
163
|
+
@all_possible_attributes ||= extract_states_from_attrs(columns_hash.keys.map(&:to_sym) + reflections.keys)
|
164
|
+
end
|
165
|
+
|
166
|
+
def extract_states_from_attrs(attrs)
|
167
|
+
if defined?(StateMachine)
|
168
|
+
machines = state_machines.keys
|
169
|
+
if attrs.is_a?(Array)
|
170
|
+
attrs += machines.inject([]) do |ar, sm_key|
|
171
|
+
ar += state_machines[sm_key].states.map(&:name).compact if sm_key.in?(attrs)
|
172
|
+
ar
|
173
|
+
end
|
174
|
+
attrs -= machines
|
175
|
+
else
|
176
|
+
machines_states = machines.inject(attrs) do |hsh, sm_key|
|
177
|
+
state_machines[sm_key].states.map(&:name).compact.each { |state| hsh[state] = attrs[sm_key] } if sm_key.in?(attrs)
|
178
|
+
hsh
|
179
|
+
end
|
180
|
+
attrs.except!(*machines)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
attrs
|
184
|
+
end
|
185
|
+
|
186
|
+
def extract_states_from_attrs!(attrs)
|
187
|
+
attrs = extract_states_from_attrs(attrs)
|
188
|
+
end
|
189
|
+
|
190
|
+
def attrs_with_types
|
191
|
+
@attrs_with_types ||= all_possible_attributes.inject({}) do |hsh, attr|
|
192
|
+
hsh[attr] = if reflections[attr]
|
193
|
+
:association
|
194
|
+
elsif (type = columns_hash[attr.to_s].try(:type)) && (!defined?(StateMachine) || !state_machines.keys.include?(attr))
|
195
|
+
type
|
196
|
+
else
|
197
|
+
:state
|
198
|
+
end
|
199
|
+
hsh
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
class AssociationScopesGenerator < ScopesGenerator::Base
|
3
|
+
|
4
|
+
def initialize(model, attr)
|
5
|
+
super
|
6
|
+
@model = model
|
7
|
+
@attr = attr
|
8
|
+
@key = "#{model.table_name}.#{attr.to_s.foreign_key}"
|
9
|
+
@type_key = "#{model.table_name}.#{attr}_type"
|
10
|
+
end
|
11
|
+
|
12
|
+
def for(name)
|
13
|
+
if @model.reflections[@attr].options[:polymorphic]
|
14
|
+
scope name || "for_#{@attr}", ->(*vals) {
|
15
|
+
raise ArgumentError, "No argument for for_#{@attr} scope" if vals.empty?
|
16
|
+
ids_and_types = vals.map { |v| extract_ids_and_types(v, @attr) }.flatten
|
17
|
+
conditions = ids_and_types.map { |hsh| "(#{@key} = ? AND #{@type_key} = ?)" }.join(' OR ')
|
18
|
+
where(conditions, *ids_and_types.map(&:values).flatten)
|
19
|
+
}
|
20
|
+
else
|
21
|
+
scope name || "for_#{@attr}", ->(*vals) {
|
22
|
+
raise ArgumentError, "No argument for for_#{@attr} scope" if vals.empty?
|
23
|
+
where(@key => vals.map { |v| extract_ids(v, @attr) }.flatten )
|
24
|
+
}
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def not_for(name)
|
29
|
+
if @model.reflections[@attr.to_sym].options[:polymorphic]
|
30
|
+
scope name || "not_for_#{@attr}", ->(*vals) {
|
31
|
+
raise ArgumentError, "No argument for for_#{@attr} scope" if vals.empty?
|
32
|
+
ids_and_types = vals.map { |v| extract_ids_and_types(v, @attr) }.flatten
|
33
|
+
conditions = ids_and_types.map { |hsh| "(#{@key} != ? AND #{@type_key} != ?)" }.join(' AND ')
|
34
|
+
where("#{conditions} OR (#{@key} IS NULL OR #{@type_key} IS NULL)", *ids_and_types.map(&:values).flatten)
|
35
|
+
}
|
36
|
+
else
|
37
|
+
scope name || "not_for_#{@attr}", ->(*vals) {
|
38
|
+
raise ArgumentError, "No argument for for_#{@attr} scope" if vals.empty?
|
39
|
+
ids = vals.map { |v| extract_ids(v, @attr) }.flatten
|
40
|
+
conditions = ids.size == 1 ? "!= ?" : "NOT IN (?)"
|
41
|
+
where("#{@key} #{conditions} OR #{@key} IS NULL", ids.size == 1 ? ids[0] : ids)
|
42
|
+
}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def extract_ids(val, attr)
|
50
|
+
if val.is_a?(Fixnum) || (val.is_a?(String) && val.to_i != 0)
|
51
|
+
val
|
52
|
+
elsif val.is_a?(ActiveRecord::Base)
|
53
|
+
val.id
|
54
|
+
elsif val.is_a?(Array) && val.all? { |v| v.is_a?(Fixnum) || (v.is_a?(String) && v.to_i != 0) || v.is_a?(ActiveRecord::Base) }
|
55
|
+
val.is_a?(ActiveRecord::Base) ? val.map(&:id) : val
|
56
|
+
else
|
57
|
+
raise ArgumentError, "Wrong argument type #{attr.class.name} for argument #{attr}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_ids_and_types(val, attr)
|
62
|
+
if val.is_a?(ActiveRecord::Base)
|
63
|
+
{id: val.id, type: val.class.name}
|
64
|
+
elsif val.is_a?(Hash) && val.assert_valid_keys(:id, :type)
|
65
|
+
val
|
66
|
+
elsif val.is_a?(Array) && val.all? { |v| v.is_a?(ActiveRecord::Base) }
|
67
|
+
val.map { |v| {id: v.id, type: v.class.name} }
|
68
|
+
elsif val.is_a?(Array) && val.size == 2 && id = val.find { |v| v.respond_to?(:to_i) && v.to_i != 0 }
|
69
|
+
val.delete(id)
|
70
|
+
{id: id, type: val[0]}
|
71
|
+
else
|
72
|
+
raise ArgumentError, "Wrong argument type #{attr.class.name} for argument #{attr}"
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
module ScopesGenerator
|
3
|
+
class Base
|
4
|
+
|
5
|
+
@@instances = {}
|
6
|
+
|
7
|
+
private_class_method :new
|
8
|
+
|
9
|
+
def initialize(model, attr)
|
10
|
+
@model = model
|
11
|
+
@attr = attr
|
12
|
+
@key = "#{model.table_name}.#{attr}"
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.instance(model, attr)
|
16
|
+
if @@instances[model.name].try(:has_key?, attr)
|
17
|
+
@@instances[model.name][attr]
|
18
|
+
else
|
19
|
+
@@instances[model.name] ||= {}
|
20
|
+
@@instances[model.name][attr] = new(model, attr)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
delegate :scope, :where, :order, to: :@model
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
class BooleanScopesGenerator < ScopesGenerator::Base
|
3
|
+
|
4
|
+
include PresenceScopes
|
5
|
+
|
6
|
+
def is(name)
|
7
|
+
scope name || @attr, where("#{@key}" => true)
|
8
|
+
end
|
9
|
+
|
10
|
+
def not(name)
|
11
|
+
scope name || "not_#{@attr}", where("#{@key}" => [false, nil])
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
class IntegerScopesGenerator < ScopesGenerator::Base
|
3
|
+
|
4
|
+
include EqualityScopes
|
5
|
+
include OrderScopes
|
6
|
+
include ComparisonScopes
|
7
|
+
|
8
|
+
def gte(name)
|
9
|
+
scope name || "#{@attr}_gte", ->(val) { where("#{@key} >= ?", val) }
|
10
|
+
end
|
11
|
+
|
12
|
+
def lte(name)
|
13
|
+
scope name || "#{@attr}_lte", ->(val) { where("#{@key} <= ?", val) }
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO
|
19
|
+
# make something like:
|
20
|
+
# %w(integer decimal time datetime date).each do |type|
|
21
|
+
# send("#{type.classify}=", NumScopesGenerator)
|
22
|
+
# end
|
23
|
+
DecimalScopesGenerator = IntegerScopesGenerator
|
24
|
+
TimeScopesGenerator = IntegerScopesGenerator
|
25
|
+
DatetimeScopesGenerator = IntegerScopesGenerator
|
26
|
+
DateScopesGenerator = IntegerScopesGenerator
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
module EqualityScopes
|
3
|
+
def with(name)
|
4
|
+
scope name || "with_#{@attr}", eq_scope
|
5
|
+
end
|
6
|
+
|
7
|
+
def eq(name)
|
8
|
+
scope name || "#{@attr}_eq", eq_scope
|
9
|
+
end
|
10
|
+
|
11
|
+
def without(name)
|
12
|
+
scope name || "without_#{@attr}", where("#{@key} IS NULL")
|
13
|
+
end
|
14
|
+
|
15
|
+
def ne(name)
|
16
|
+
scope name || "#{@attr}_ne", ->(*vals) {
|
17
|
+
raise ArgumentError, "No argument for for_#{@attr} scope" if vals.empty?
|
18
|
+
sql = "#{@key} " << (vals.size == 1 && !vals[0].is_a?(Array) ? '!= ?' : 'NOT IN (?)') << " OR #{@key} IS NULL"
|
19
|
+
where(sql, vals.flatten)
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def eq_scope
|
26
|
+
@eq_scope ||= ->(*vals) { vals.empty? ? where("#{@key} IS NOT NULL") : where(@key => vals.flatten) }
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
if defined?(StateMachine)
|
2
|
+
module MagicScopes
|
3
|
+
class StateScopesGenerator < ScopesGenerator::Base
|
4
|
+
def initialize(model, state)
|
5
|
+
@model = model
|
6
|
+
@state = state
|
7
|
+
@key = "#{model.table_name}.#{attr}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def is(name)
|
11
|
+
scope name || @state, where("#{@key}" => @state)
|
12
|
+
end
|
13
|
+
|
14
|
+
def not(name)
|
15
|
+
scope name || "not_#{@state}", where("#{@key} != ? OR #{@key} IS NULL", @state)
|
16
|
+
end
|
17
|
+
|
18
|
+
def with(name)
|
19
|
+
@model.instance_eval("undef :with_#{attr}")
|
20
|
+
scope name || "with_#{@attr}", ->(*vals) { where(vals.empty? ? "#{@key} IS NOT NULL" : ["#{@key} IN (?)", vals]) }
|
21
|
+
end
|
22
|
+
|
23
|
+
def without(name)
|
24
|
+
@model.instance_eval("undef :without_#{attr}")
|
25
|
+
scope name || "without_#{@attr}", ->(*vals) { where(vals.empty? ? "#{@key} IS NULL" : ["#{@key} NOT IN (?)", vals]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def attr
|
31
|
+
@attr ||= @model.state_machines.find { |_, sm| sm.states.map(&:name).include?(@state) }[0]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
class StringScopesGenerator < ScopesGenerator::Base
|
3
|
+
|
4
|
+
include EqualityScopes
|
5
|
+
include OrderScopes
|
6
|
+
|
7
|
+
def like(name)
|
8
|
+
scope name || "#{@attr}_like", ->(*vals) { where(build_query(*vals, "#{@key} LIKE ?", 'OR')) }
|
9
|
+
end
|
10
|
+
|
11
|
+
def not_like(name)
|
12
|
+
scope name || "#{@attr}_not_like", ->(*vals) { where(build_query(*vals, "#{@key} NOT LIKE ?", 'AND')) }
|
13
|
+
end
|
14
|
+
|
15
|
+
def ilike(name)
|
16
|
+
scope name || "#{@attr}_ilike", ilike_scope
|
17
|
+
end
|
18
|
+
|
19
|
+
def not_ilike(name)
|
20
|
+
scope name || "#{@attr}_not_ilike", ilike_scope('NOT')
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def build_query(*vals, condition, operator)
|
26
|
+
[Array.new(vals.size, condition).join(" #{operator} "), *vals.map { |val| "%#{val}%" }]
|
27
|
+
end
|
28
|
+
|
29
|
+
def ilike_scope(operator = nil)
|
30
|
+
conditions = ilike_supported? ? "#{@key} #{operator} ILIKE ?" : "LOWER(#{@key}) #{operator} LIKE ?"
|
31
|
+
->(*vals){ where(build_query(*vals, conditions, operator != 'NOT' ? 'OR' : 'AND')) }
|
32
|
+
end
|
33
|
+
|
34
|
+
def ilike_supported?
|
35
|
+
@model.connection.adapter_name == 'PostgreSQL'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
TextScopesGenerator = StringScopesGenerator
|
40
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module MagicScopes
|
2
|
+
module StandardScopes
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
self::STANDARD_SCOPES = [:asc, :sorted, :desc, :recent, :random]
|
7
|
+
end
|
8
|
+
|
9
|
+
private
|
10
|
+
|
11
|
+
def asc_scope
|
12
|
+
scope :asc, order(:id)
|
13
|
+
end
|
14
|
+
|
15
|
+
def sorted_scope
|
16
|
+
scope :sorted, order(:id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def desc_scope
|
20
|
+
scope :desc, order('id DESC')
|
21
|
+
end
|
22
|
+
|
23
|
+
def recent_scope
|
24
|
+
scope :recent, order('id DESC')
|
25
|
+
end
|
26
|
+
|
27
|
+
def random_scope
|
28
|
+
scope :random, order('RANDOM()')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
metadata
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: magic_scopes
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.1.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Dmitry Afanasyev
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-11-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '3.2'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '3.2'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: activesupport
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '3.2'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '3.2'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rails
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '3.2'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '3.2'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: sqlite3
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: state_machine
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
- !ruby/object:Gem::Dependency
|
95
|
+
name: rspec-rails
|
96
|
+
requirement: !ruby/object:Gem::Requirement
|
97
|
+
none: false
|
98
|
+
requirements:
|
99
|
+
- - ! '>='
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
type: :development
|
103
|
+
prerelease: false
|
104
|
+
version_requirements: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: pry-debugger
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
none: false
|
114
|
+
requirements:
|
115
|
+
- - ! '>='
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
description: ActiveRecord scopes generators
|
127
|
+
email:
|
128
|
+
- dimarzio1986@gmail.com
|
129
|
+
executables: []
|
130
|
+
extensions: []
|
131
|
+
extra_rdoc_files: []
|
132
|
+
files:
|
133
|
+
- lib/magic_scopes/railtie.rb
|
134
|
+
- lib/magic_scopes/scopes_builder.rb
|
135
|
+
- lib/magic_scopes/scopes_generators/association.rb
|
136
|
+
- lib/magic_scopes/scopes_generators/base.rb
|
137
|
+
- lib/magic_scopes/scopes_generators/boolean.rb
|
138
|
+
- lib/magic_scopes/scopes_generators/float.rb
|
139
|
+
- lib/magic_scopes/scopes_generators/integer.rb
|
140
|
+
- lib/magic_scopes/scopes_generators/mixins/comparison_scopes.rb
|
141
|
+
- lib/magic_scopes/scopes_generators/mixins/equality_scopes.rb
|
142
|
+
- lib/magic_scopes/scopes_generators/mixins/order_scopes.rb
|
143
|
+
- lib/magic_scopes/scopes_generators/mixins/presence_scopes.rb
|
144
|
+
- lib/magic_scopes/scopes_generators/state.rb
|
145
|
+
- lib/magic_scopes/scopes_generators/string.rb
|
146
|
+
- lib/magic_scopes/standard_scopes.rb
|
147
|
+
- lib/magic_scopes/version.rb
|
148
|
+
- lib/magic_scopes.rb
|
149
|
+
- MIT-LICENSE
|
150
|
+
- Rakefile
|
151
|
+
- README.rdoc
|
152
|
+
homepage: http://github.com/icrowley
|
153
|
+
licenses: []
|
154
|
+
post_install_message:
|
155
|
+
rdoc_options: []
|
156
|
+
require_paths:
|
157
|
+
- lib
|
158
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
159
|
+
none: false
|
160
|
+
requirements:
|
161
|
+
- - ! '>='
|
162
|
+
- !ruby/object:Gem::Version
|
163
|
+
version: '0'
|
164
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
165
|
+
none: false
|
166
|
+
requirements:
|
167
|
+
- - ! '>='
|
168
|
+
- !ruby/object:Gem::Version
|
169
|
+
version: '0'
|
170
|
+
requirements: []
|
171
|
+
rubyforge_project:
|
172
|
+
rubygems_version: 1.8.24
|
173
|
+
signing_key:
|
174
|
+
specification_version: 3
|
175
|
+
summary: ActiveRecord scopes generators
|
176
|
+
test_files: []
|
177
|
+
has_rdoc:
|