zen-query 1.0.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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +3 -0
- data/.rubocop.yml +36 -0
- data/.travis.yml +4 -0
- data/CHANGELOG +21 -0
- data/Gemfile +5 -0
- data/LICENSE.txt +21 -0
- data/README.md +428 -0
- data/Rakefile +12 -0
- data/bin/console +73 -0
- data/bin/setup +8 -0
- data/lib/zen/query.rb +157 -0
- data/lib/zen/query/api_block.rb +79 -0
- data/lib/zen/query/api_methods.rb +121 -0
- data/lib/zen/query/attributes.rb +51 -0
- data/lib/zen/query/version.rb +7 -0
- data/zen-query.gemspec +40 -0
- metadata +164 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 50b08ec0ae8f0289575e0741873b57ec885f9d6c06fcd0dd6f31909127102585
|
4
|
+
data.tar.gz: 00aa3acdcbf0b33c4128bd420e90f95b260e4166963fba4d248e17508fc11a2c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 45044327d4848b4250a6f8603f19110ed1dcaf4ede3d333b5171425a6f7679f0369674f17ca1f0afec812b260583ea60ed64b39c6bf1aa5bf63c6aa60bdd53c4
|
7
|
+
data.tar.gz: bff2db15804f328d7f5e2c7f59d1b180443449ff78b7a9cf304ceb6310e794043fc0c8ec001b603b90200b9a2c827496c92f002d5927dc346c5d7feec45dd4c7
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
AllCops:
|
2
|
+
NewCops: disable
|
3
|
+
TargetRubyVersion: 2.4
|
4
|
+
|
5
|
+
Layout/MultilineMethodCallIndentation:
|
6
|
+
EnforcedStyle: indented_relative_to_receiver
|
7
|
+
|
8
|
+
Style/StringLiterals:
|
9
|
+
EnforcedStyle: double_quotes
|
10
|
+
|
11
|
+
Style/AccessModifierDeclarations:
|
12
|
+
EnforcedStyle: group
|
13
|
+
|
14
|
+
Style/Documentation:
|
15
|
+
Enabled: false
|
16
|
+
|
17
|
+
Style/LambdaCall:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/ClassAndModuleChildren:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Style/DoubleNegation:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Metrics/MethodLength:
|
27
|
+
Max: 20
|
28
|
+
|
29
|
+
Metrics/BlockLength:
|
30
|
+
Exclude:
|
31
|
+
- 'spec/**/*.rb'
|
32
|
+
- 'zen-query.gemspec'
|
33
|
+
|
34
|
+
Layout/LineLength:
|
35
|
+
Exclude:
|
36
|
+
- 'spec/**/*.rb'
|
data/.travis.yml
ADDED
data/CHANGELOG
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
=== master
|
2
|
+
|
3
|
+
Improve :index option support, add query_by! and sift_by! methods
|
4
|
+
|
5
|
+
=== 1.0.3
|
6
|
+
|
7
|
+
Explicitly apply query blocks in definition order
|
8
|
+
|
9
|
+
=== 1.0.2
|
10
|
+
|
11
|
+
Remove base_scope application on sifted instance
|
12
|
+
|
13
|
+
=== 1.0.1
|
14
|
+
|
15
|
+
Fix base scope application on sifted instance
|
16
|
+
|
17
|
+
Add [github release] badge to README
|
18
|
+
|
19
|
+
Update README, add CHANGELOG
|
20
|
+
|
21
|
+
=== 1.0.0
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Artem Kuzko
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,428 @@
|
|
1
|
+
# Zen::Query
|
2
|
+
|
3
|
+
Param-based scope (relation, dataset) generation.
|
4
|
+
|
5
|
+
[](http://travis-ci.org/akuzko/zen-query)
|
6
|
+
[](https://github.com/akuzko/zen-query/releases)
|
7
|
+
|
8
|
+
---
|
9
|
+
|
10
|
+
This gem provides a `Zen::Query` class with a declarative and convenient API
|
11
|
+
to build scopes (ActiveRecord relations or arbitrary objects) dynamically, based
|
12
|
+
on parameters passed to query object on initialization.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem 'zen-query'
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
$ bundle
|
25
|
+
|
26
|
+
Or install it yourself as:
|
27
|
+
|
28
|
+
$ gem install zen-query
|
29
|
+
|
30
|
+
## Usage
|
31
|
+
|
32
|
+
Despite the fact `zen-query` was intended to help building ActiveRecord relations
|
33
|
+
via scopes or query methods, it's usage is not limited to ActiveRecord cases and
|
34
|
+
may be used with any arbitrary classes and objects. In fact, for development and
|
35
|
+
testing, `OpenStruct` instance is used as a generic subject. However, ActiveRecord
|
36
|
+
examples should illustrate gem's usage in the best way.
|
37
|
+
|
38
|
+
For most examples in this README, `scope` method is used as accessor to
|
39
|
+
current subject value. This behavior is easily achieved via `Query.alias_subject_name(:scope)`
|
40
|
+
method call.
|
41
|
+
|
42
|
+
### API
|
43
|
+
|
44
|
+
`zen-query` provides `Zen::Query` class, descendants of which should declare
|
45
|
+
scope manipulations using `query_by`, `sift_by` and other class methods bellow.
|
46
|
+
|
47
|
+
#### Class Methods
|
48
|
+
|
49
|
+
- `query_by(*presence_fields, **value_fields, &block)` declares a scope-generation query
|
50
|
+
block that will be executed if, and only if all values of query params at the keys of
|
51
|
+
`presence_fields` are present in activesupport's definition of presence and all `value_fields`
|
52
|
+
are present in query params as is. The block is executed in context of query
|
53
|
+
object. All values of specified params are yielded to the block. If the block
|
54
|
+
returns a non-nil value, it becomes a new scope for subsequent processing. Of course,
|
55
|
+
there can be multiple `query_by` block definitions. Methods accepts additional options:
|
56
|
+
- `:index` - allows to specify order of query block applications. By default all query
|
57
|
+
blocks have index of 0. This option also accepts special values `:first` and `:last` for
|
58
|
+
more convenient usage. Queries with the same value of `:index` option are applied in
|
59
|
+
order of declaration.
|
60
|
+
- `:if` - specifies condition according to which query should be applied. If Symbol
|
61
|
+
or String is passed, calls corresponding method. If Proc is passed, it is executed
|
62
|
+
in context of query object. Note that this is optional condition, and does not
|
63
|
+
overwrite original param-based condition for a query block that should always be met.
|
64
|
+
- `:unless` - the same as `:if` option, but with reversed boolean check.
|
65
|
+
|
66
|
+
- `query_by!(*fields, &block)` declares scope-generation block that is always executed
|
67
|
+
(unless `:if` and/or `:unless` options are used). All values in params at `fields` keys are
|
68
|
+
yielded to the block. As `query_by`, accepts `:index`, `:if` and `:unless` options.
|
69
|
+
|
70
|
+
- `query(&block)` declares scope-generation block that is always executed (unless `:if`
|
71
|
+
and/or `:unless` options are used). As `query_by`, accepts `:index`, `:if` and `:unless`
|
72
|
+
options.
|
73
|
+
|
74
|
+
*Examples:*
|
75
|
+
|
76
|
+
```ruby
|
77
|
+
# executes block only when params[:department_id] is non-empty:
|
78
|
+
query_by(:department_id) { |id| scope.where(department_id: id) }
|
79
|
+
|
80
|
+
# executes block only when params[:only_active] == 'true':
|
81
|
+
query_by(only_active: 'true') { scope.active }
|
82
|
+
|
83
|
+
# executes block only when *both* params[:first_name] and params[:last_name]
|
84
|
+
# are present:
|
85
|
+
query_by(:first_name, :last_name) do |first_name, last_name|
|
86
|
+
scope.where(first_name: first_name, last_name: last_name)
|
87
|
+
end
|
88
|
+
|
89
|
+
# if query block returns nil, scope will remain intact:
|
90
|
+
query { scope.active if only_active? }
|
91
|
+
|
92
|
+
# conditional example:
|
93
|
+
query(if: :include_inactive?) { scope.with_inactive }
|
94
|
+
|
95
|
+
def include_inactive?
|
96
|
+
company.settings.include_inactive?
|
97
|
+
end
|
98
|
+
```
|
99
|
+
|
100
|
+
- `sift_by(*presence_fields, **value_fields, &block)` method is used to hoist sets of
|
101
|
+
query definitions that should be applied if, and only if, all specified values
|
102
|
+
match criteria in the same way as in `query_by` method. Just like `query_by` method,
|
103
|
+
values of specified fields are yielded to the block. Accepts the same options as
|
104
|
+
it's `query_by` counterpart. Such `sift_by` definitions may be nested in any depth.
|
105
|
+
|
106
|
+
- `sift_by!(*fields, &block)` declares a sifter block that is always applied (unless
|
107
|
+
`:if` and/or `:unless` options are used). All values in params at specified `fields`
|
108
|
+
are yielded to the block.
|
109
|
+
|
110
|
+
- `sifter` alias for `sift_by`. Results in a more readable construct when a single
|
111
|
+
presence field is passed. For example, `sifter(:paginated)`.
|
112
|
+
|
113
|
+
*Examples:*
|
114
|
+
|
115
|
+
```ruby
|
116
|
+
sift_by(:search_value, :search_type) do |value|
|
117
|
+
# definitions in this block will be applied only if *both* params[:search_value]
|
118
|
+
# and params[:search_type] are present
|
119
|
+
|
120
|
+
search_value = "%#{value}%"
|
121
|
+
|
122
|
+
query_by(search_type: 'name') { scope.name_like(value) }
|
123
|
+
query_by(search_type: 'email') { scope.where("users.email LIKE ?", search_value) }
|
124
|
+
end
|
125
|
+
|
126
|
+
sifter :paginated do
|
127
|
+
query_by(:page, :per_page) do |page, per|
|
128
|
+
scope.page(page).per(per)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def paginated_records
|
133
|
+
resolve(:paginated)
|
134
|
+
end
|
135
|
+
```
|
136
|
+
|
137
|
+
- `subject(&block)` method is used to define a base subject as a starting point
|
138
|
+
of subject-generating process. Note that `subject` will not be evaluated if
|
139
|
+
query is initialized with a given subject.
|
140
|
+
|
141
|
+
*Examples:*
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
subject { User.all }
|
145
|
+
```
|
146
|
+
|
147
|
+
- `defaults(&block)` method is used to declare default query params that are
|
148
|
+
reverse merged with params passed on query initialization. When used in `sift_by`
|
149
|
+
block, hashes are merged altogether. Accepts a `block`, it's return value
|
150
|
+
will be evaluated and merged on query object instantiation, allowing to have
|
151
|
+
dynamic default params values.
|
152
|
+
|
153
|
+
*Examples:*
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
defaults { { later_than: 1.week.ago } }
|
157
|
+
|
158
|
+
sifter :paginated do
|
159
|
+
# sifter defaults are merged with higher-level defaults:
|
160
|
+
defaults { { page: 1, per_page: 25 } }
|
161
|
+
end
|
162
|
+
```
|
163
|
+
|
164
|
+
- `guard(message = nil, &block)` defines a guard instance method block (see instance methods
|
165
|
+
bellow). All such blocks are executed before query object resolves scope via
|
166
|
+
`resolve_scope` method. Optional `message` may be supplied to provide more informative
|
167
|
+
error message.
|
168
|
+
|
169
|
+
*Examples:*
|
170
|
+
|
171
|
+
```ruby
|
172
|
+
sift_by(:sort_col, :sort_dir) do |scol, sdir|
|
173
|
+
# will raise Zen::Query::GuardViolationError on scope resolution if
|
174
|
+
# params[:sort_dir] is not 'asc' or 'desc'
|
175
|
+
guard(':sort_dir should be "asc" or "desc"') do
|
176
|
+
sdir.downcase.in?(%w(asc desc))
|
177
|
+
end
|
178
|
+
|
179
|
+
query { scope.order(scol => sdir) }
|
180
|
+
end
|
181
|
+
```
|
182
|
+
|
183
|
+
- `raise_on_guard_violation(value)` allows to specify whether or not exception should be raised
|
184
|
+
whenever any guard block is violated during scope resolution. When set to `false`, in case
|
185
|
+
of any violation, `resolve` will return `nil`, and query will have `violation` property
|
186
|
+
set with value corresponding to the message of violated block. Default option value is `true`.
|
187
|
+
|
188
|
+
*Examples:*
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
raise_on_guard_violation false
|
192
|
+
|
193
|
+
sift_by(:sort_col, :sort_dir) do |scol, sdir|
|
194
|
+
guard(':sort_dir should be "asc" or "desc"') do
|
195
|
+
sdir.downcase.in?(%w(asc desc))
|
196
|
+
end
|
197
|
+
|
198
|
+
query { scope.order(scol => sdir) }
|
199
|
+
end
|
200
|
+
```
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
query = UsersQuery.new(sort_col: 'id', sort_dir: 'there')
|
204
|
+
query.resolve # => nil
|
205
|
+
query.violation # => ":sort_dir should be \"asc\" or \"desc\""
|
206
|
+
```
|
207
|
+
|
208
|
+
- `attributes(*attribute_names)` allows to specify additional attributes that can be passed
|
209
|
+
to query object on initialization. For each given attribute name, reader method is generated.
|
210
|
+
|
211
|
+
#### Instance Methods
|
212
|
+
|
213
|
+
- `initialize(params: {}, subject: nil, **attributes)` initializes a query with
|
214
|
+
`params`, an optional subject and attributes. If subject is aliased, corresponding
|
215
|
+
key should be used instead. The rest of attributes are only accepted if they were
|
216
|
+
declared via `attributes` class method call.
|
217
|
+
|
218
|
+
*Examples:*
|
219
|
+
|
220
|
+
```ruby
|
221
|
+
query = UsersQuery.new(params: query_params, company: company)
|
222
|
+
```
|
223
|
+
|
224
|
+
- `params` returns a parameters passed in initialization, reverse merged with query
|
225
|
+
defaults.
|
226
|
+
|
227
|
+
- `subject` "current" subject of query object. For an initialized query object corresponds
|
228
|
+
to base subject. Primary usage is to call this method in `query_by` blocks and return
|
229
|
+
it's mutated version corresponding to passed `query_by` arguments.
|
230
|
+
|
231
|
+
Can be aliased to more suitable name with `Query.alias_subject_name` class method.
|
232
|
+
|
233
|
+
- `guard(&block)` executes a passed `block`. If this execution returns falsy value,
|
234
|
+
`GuardViolationError` is raised. You can use this method to ensure safety of param
|
235
|
+
values interpolation to a SQL string in a `query_by` block for example.
|
236
|
+
|
237
|
+
*Examples:*
|
238
|
+
|
239
|
+
```ruby
|
240
|
+
query_by(:sort_col, :sort_dir) do |scol, sdir|
|
241
|
+
# will raise Zen::Query::GuardViolationError on scope resolution if
|
242
|
+
# params[:sort_dir] is not 'asc' or 'desc'
|
243
|
+
guard { sdir.downcase.in?(%w(asc desc)) }
|
244
|
+
|
245
|
+
scope.order(scol => sdir)
|
246
|
+
end
|
247
|
+
```
|
248
|
+
|
249
|
+
- `resolve(*presence_keys, override_params = {})` returns a resulting scope
|
250
|
+
generated by all queries and sifted queries that fit to query params applied to
|
251
|
+
base scope. Optionally, additional params may be passed to override the ones passed on
|
252
|
+
initialization. For convinience, you may pass list of keys that should be resolved
|
253
|
+
to `true` with params (for example, `resolve(:with_projects)` instead of
|
254
|
+
`resolve(with_projects: true)`). It's the main `Query` instance method that
|
255
|
+
returns the sole purpose of it's instances.
|
256
|
+
|
257
|
+
*Examples:*
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
defaults { { only_active: true } }
|
261
|
+
|
262
|
+
subject { company.users }
|
263
|
+
|
264
|
+
query_by(:only_active) { subject.active }
|
265
|
+
|
266
|
+
sifter :with_departments do
|
267
|
+
query { subject.joins(:departments) }
|
268
|
+
|
269
|
+
query_by(:department_name) do |name|
|
270
|
+
subject.where(departments: { name: name })
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
def users
|
275
|
+
@users ||= resolve
|
276
|
+
end
|
277
|
+
|
278
|
+
# you can use options to overwrite defaults:
|
279
|
+
def all_users
|
280
|
+
resolve(only_active: false)
|
281
|
+
end
|
282
|
+
|
283
|
+
# or to apply a sifter with additional params:
|
284
|
+
def managers
|
285
|
+
resolve(:with_departments, department_name: 'managers')
|
286
|
+
end
|
287
|
+
```
|
288
|
+
|
289
|
+
### Composite usage example with ActiveRecord Relation as a subject, aliased as `:relation`
|
290
|
+
|
291
|
+
```ruby
|
292
|
+
class UserQuery < Zen::Query
|
293
|
+
alias_subject_name :relation
|
294
|
+
|
295
|
+
attributes :company
|
296
|
+
|
297
|
+
defaults { { only_active: true } }
|
298
|
+
|
299
|
+
relation { company.users }
|
300
|
+
|
301
|
+
query_by(:only_active) { relation.active }
|
302
|
+
|
303
|
+
query_by(:birthdate) { |date| relation.by_birtdate(date) }
|
304
|
+
|
305
|
+
query_by :name do |name|
|
306
|
+
relation.where("CONCAT(first_name, ' ', last_name) LIKE :name", name: "%#{name}%")
|
307
|
+
end
|
308
|
+
|
309
|
+
sift_by :sort_column, :sort_direction do |scol, sdir|
|
310
|
+
guard { sdir.to_s.downcase.in?(%w(asc desc)) }
|
311
|
+
|
312
|
+
query { relation.order(scol => sdir) }
|
313
|
+
|
314
|
+
query_by(sort_column: 'name') do
|
315
|
+
relation.reorder("CONCAT(first_name, ' ', last_name) #{sdir}")
|
316
|
+
end
|
317
|
+
end
|
318
|
+
|
319
|
+
sifter :with_projects do
|
320
|
+
query { relation.joins(:projects) }
|
321
|
+
|
322
|
+
query_by :project_name do |name|
|
323
|
+
scope.where(projects: { name: name })
|
324
|
+
end
|
325
|
+
end
|
326
|
+
|
327
|
+
def users
|
328
|
+
@users ||= resolve
|
329
|
+
end
|
330
|
+
|
331
|
+
def project_users
|
332
|
+
@project_users ||= resolve(:with_projects)
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
params = { name: 'John', sort_column: 'name', sort_direction: 'DESC', project_name: 'ExampleApp' }
|
337
|
+
|
338
|
+
query = UserQuery.new(params: params, company: some_company)
|
339
|
+
|
340
|
+
query.project_users # => this is the same as:
|
341
|
+
# some_company.users
|
342
|
+
# .active
|
343
|
+
# .joins(:projects)
|
344
|
+
# .where("CONCAT(first_name, ' ', last_name) LIKE ?", "%John%")
|
345
|
+
# .where(projects: { name: 'ExampleApp' })
|
346
|
+
# .order("CONCAT(first_name, ' ', last_name) DESC")
|
347
|
+
```
|
348
|
+
|
349
|
+
### Hints and Tips
|
350
|
+
|
351
|
+
- Keep in mind that query classes are just plain Ruby classes. All `sifter`,
|
352
|
+
`query_by` and `guard` declarations are inherited, as well as default params
|
353
|
+
declared by `defaults` method. Thus, you can define a BaseQuery with common
|
354
|
+
definitions as a base class for queries in your application. Or you can define
|
355
|
+
query API blocks in some module's `included` callback to share common definitions
|
356
|
+
via module inclusion.
|
357
|
+
|
358
|
+
- Being plain Ruby classes also means you can easily extend default functionality
|
359
|
+
for your needs. For example, if you're querying ActiveRecord relations, and your
|
360
|
+
primary use case looks like
|
361
|
+
|
362
|
+
```ruby
|
363
|
+
query_by(:some_field_id) { |id| scope.where(some_field_id: id) }
|
364
|
+
```
|
365
|
+
you can do the following to make things more DRY:
|
366
|
+
|
367
|
+
```ruby
|
368
|
+
class ApplicationQuery < Zen::Query
|
369
|
+
def self.query_by(*fields, &block)
|
370
|
+
block ||= default_query_block(fields)
|
371
|
+
super(*fields, &block)
|
372
|
+
end
|
373
|
+
|
374
|
+
def self.default_query_block(fields)
|
375
|
+
->(*values){ scope.where(Hash[fields.zip(values)]) }
|
376
|
+
end
|
377
|
+
private_class_method :default_query_block
|
378
|
+
end
|
379
|
+
```
|
380
|
+
|
381
|
+
and then you can simply call
|
382
|
+
|
383
|
+
```ruby
|
384
|
+
class UsersQuery < ApplicationQuery
|
385
|
+
base_scope { company.users }
|
386
|
+
|
387
|
+
query_by :first_name
|
388
|
+
query_by :last_name
|
389
|
+
query_by :city, :street_address
|
390
|
+
end
|
391
|
+
```
|
392
|
+
|
393
|
+
Or you can go a little further and declare a class method
|
394
|
+
|
395
|
+
```ruby
|
396
|
+
class ApplicationQuery
|
397
|
+
def self.query_by_fields(*fields)
|
398
|
+
fields.each do |field|
|
399
|
+
query_by field
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|
403
|
+
```
|
404
|
+
|
405
|
+
and then
|
406
|
+
|
407
|
+
```ruby
|
408
|
+
class UserQuery < ApplicationQuery
|
409
|
+
query_by_fields :first_name, :last_name, :department_id
|
410
|
+
end
|
411
|
+
```
|
412
|
+
|
413
|
+
## Development
|
414
|
+
|
415
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
416
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
417
|
+
prompt that will allow you to experiment.
|
418
|
+
|
419
|
+
## Contributing
|
420
|
+
|
421
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/akuzko/zen-query.
|
422
|
+
|
423
|
+
|
424
|
+
## License
|
425
|
+
|
426
|
+
The gem is available as open source under the terms of the
|
427
|
+
[MIT License](http://opensource.org/licenses/MIT).
|
428
|
+
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require "bundler/setup"
|
5
|
+
require "zen/query"
|
6
|
+
|
7
|
+
require "ostruct"
|
8
|
+
|
9
|
+
class Query < Zen::Query
|
10
|
+
raise_on_guard_violation(false)
|
11
|
+
|
12
|
+
alias_subject_name(:scope)
|
13
|
+
|
14
|
+
subject { OpenStruct.new }
|
15
|
+
|
16
|
+
defaults { { time: Time.now, barbak: "barbak" } }
|
17
|
+
|
18
|
+
query_by :foo do |foo|
|
19
|
+
guard('wrong value for :foo param. try "foo"') { foo == "foo" }
|
20
|
+
|
21
|
+
scope.tap { scope.foo = foo }
|
22
|
+
end
|
23
|
+
|
24
|
+
query_by bar: "bar" do
|
25
|
+
scope.tap { scope.bar = "bar" }
|
26
|
+
end
|
27
|
+
|
28
|
+
sift_by baz: "baz" do |baz|
|
29
|
+
defaults { { bakbar: "bakbar" } }
|
30
|
+
|
31
|
+
guard { upcase(baz) == "BAZ" }
|
32
|
+
|
33
|
+
query { scope.tap { scope.baz = "baz" } }
|
34
|
+
|
35
|
+
query_by :bak do |bak|
|
36
|
+
scope.tap { scope.bak = bak }
|
37
|
+
end
|
38
|
+
|
39
|
+
query_by :barbak do |barbak|
|
40
|
+
scope.tap { scope.barbak = barbak }
|
41
|
+
end
|
42
|
+
|
43
|
+
sift_by :nested_baz do |nested_baz|
|
44
|
+
query_by :bakbar do |bakbar|
|
45
|
+
scope.tap { scope.bakbar = [baz, nested_baz, bakbar].join("-") }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
sift_by :other_baz do |other|
|
51
|
+
query { scope.tap { scope.other_baz = other } }
|
52
|
+
end
|
53
|
+
|
54
|
+
query(if: :condition?) { scope.tap { scope.by_instance_condition = true } }
|
55
|
+
query(unless: :bad_condition?) { scope.tap { scope.not_bad_condition = true } }
|
56
|
+
|
57
|
+
def condition?
|
58
|
+
!!params[:condition]
|
59
|
+
end
|
60
|
+
|
61
|
+
def bad_condition?
|
62
|
+
!condition?
|
63
|
+
end
|
64
|
+
|
65
|
+
def upcase(str)
|
66
|
+
str.upcase
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# q = Query.new(params: { foo: "foo", bar: "bar", baz: "baz", bak: "bak", nested_baz: "nb", other_baz: "ob" })
|
71
|
+
|
72
|
+
require "pry"
|
73
|
+
Pry.start
|
data/bin/setup
ADDED
data/lib/zen/query.rb
ADDED
@@ -0,0 +1,157 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "query/api_block"
|
4
|
+
require_relative "query/api_methods"
|
5
|
+
require_relative "query/attributes"
|
6
|
+
|
7
|
+
module Zen
|
8
|
+
class Query # rubocop:disable Metrics/ClassLength
|
9
|
+
UndefinedSubjectError = Class.new(StandardError)
|
10
|
+
GuardViolationError = Class.new(ArgumentError)
|
11
|
+
|
12
|
+
extend ApiMethods
|
13
|
+
include Attributes
|
14
|
+
|
15
|
+
raise_on_guard_violation(true)
|
16
|
+
|
17
|
+
attr_reader :params, :violation
|
18
|
+
|
19
|
+
def self.inherited(subclass) # rubocop:disable Metrics/AbcSize
|
20
|
+
subclass.raise_on_guard_violation(raise_on_guard_violation?)
|
21
|
+
subclass.query_blocks.replace(query_blocks.dup)
|
22
|
+
subclass.sift_blocks.replace(sift_blocks.dup)
|
23
|
+
subclass.guard_blocks.replace(guard_blocks.dup)
|
24
|
+
subclass.subject(&subject)
|
25
|
+
subclass.defaults(&defaults) unless defaults.nil?
|
26
|
+
super
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize(params: {}, **attrs)
|
30
|
+
@params = klass.fetch_defaults.merge(params)
|
31
|
+
@subject = attrs.delete(self.class.subject_name)
|
32
|
+
@base_params = @params
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
def subject
|
37
|
+
@subject ||= base_subject
|
38
|
+
end
|
39
|
+
|
40
|
+
def base_subject
|
41
|
+
subject =
|
42
|
+
klass
|
43
|
+
.ancestors
|
44
|
+
.select { |mod| mod.respond_to?(:subject) }
|
45
|
+
.map(&:subject)
|
46
|
+
.compact
|
47
|
+
.first
|
48
|
+
&.call
|
49
|
+
|
50
|
+
raise UndefinedSubjectError, "failed to build subject. Have you missed subject definition?" if subject.nil?
|
51
|
+
|
52
|
+
subject
|
53
|
+
end
|
54
|
+
|
55
|
+
def resolve(*args)
|
56
|
+
@violation = nil
|
57
|
+
arg_params = args.pop if args.last.is_a?(Hash)
|
58
|
+
return sifted_instance.resolve! if arg_params.nil? && args.empty?
|
59
|
+
|
60
|
+
clone_with_params(trues(args).merge(arg_params || {})).resolve
|
61
|
+
rescue GuardViolationError => e
|
62
|
+
@violation = e.message
|
63
|
+
raise if self.class.raise_on_guard_violation?
|
64
|
+
end
|
65
|
+
|
66
|
+
def klass
|
67
|
+
sifted? ? singleton_class : self.class
|
68
|
+
end
|
69
|
+
|
70
|
+
protected
|
71
|
+
|
72
|
+
attr_writer :subject, :params
|
73
|
+
attr_accessor :block
|
74
|
+
attr_reader :attrs
|
75
|
+
|
76
|
+
def sifted_instance
|
77
|
+
blocks = klass.sift_blocks.select { |block| block.fits?(self) }
|
78
|
+
|
79
|
+
blocks.empty? ? self : sifted_instance_for(blocks)
|
80
|
+
end
|
81
|
+
|
82
|
+
def resolve!
|
83
|
+
guard_all
|
84
|
+
klass.sorted_query_blocks.reduce(subject) do |subject, block|
|
85
|
+
clone_with_subject(subject, block).apply_block!.subject
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def apply_block!
|
90
|
+
if block&.fits?(self)
|
91
|
+
subject = instance_exec(*block.values_for(params), &block.block)
|
92
|
+
@subject = subject unless subject.nil?
|
93
|
+
end
|
94
|
+
self
|
95
|
+
end
|
96
|
+
|
97
|
+
def sifted!(query, blocks) # rubocop:disable Metrics/AbcSize
|
98
|
+
singleton_class.query_blocks.replace(query.klass.query_blocks.dup)
|
99
|
+
singleton_class.guard_blocks.replace(query.klass.guard_blocks.dup)
|
100
|
+
singleton_class.subject(&query.klass.subject)
|
101
|
+
blocks.each do |block|
|
102
|
+
singleton_class.instance_exec(*block.values_for(params), &block.block)
|
103
|
+
end
|
104
|
+
params.replace(singleton_class.fetch_defaults.merge(params))
|
105
|
+
@sifted = true
|
106
|
+
end
|
107
|
+
|
108
|
+
def sifted?
|
109
|
+
!!@sifted
|
110
|
+
end
|
111
|
+
|
112
|
+
def clone_with_subject(subject, block = nil)
|
113
|
+
clone.tap do |query|
|
114
|
+
query.subject = subject
|
115
|
+
query.block = block
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def clone_with_params(other_params)
|
120
|
+
dup.tap do |query|
|
121
|
+
query.params = @base_params.merge(other_params)
|
122
|
+
query.remove_instance_variable("@sifted") if query.instance_variable_defined?("@sifted")
|
123
|
+
query.remove_instance_variable("@subject") if query.instance_variable_defined?("@subject")
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def clone_sifted_with(blocks)
|
128
|
+
dup.tap do |query|
|
129
|
+
query.sifted!(self, blocks)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def guard_all
|
136
|
+
klass.guard_blocks.each { |message, block| guard(message, &block) }
|
137
|
+
end
|
138
|
+
|
139
|
+
def guard(message = nil, &block)
|
140
|
+
return if instance_exec(&block)
|
141
|
+
|
142
|
+
violation = message || "guard block violated on #{block.source_location.join(':')}"
|
143
|
+
|
144
|
+
raise GuardViolationError, violation
|
145
|
+
end
|
146
|
+
|
147
|
+
def sifted_instance_for(blocks)
|
148
|
+
clone_sifted_with(blocks).sifted_instance
|
149
|
+
end
|
150
|
+
|
151
|
+
def trues(keys)
|
152
|
+
keys.each_with_object({}) do |key, hash|
|
153
|
+
hash[key] = true
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zen
|
4
|
+
class Query
|
5
|
+
class ApiBlock
|
6
|
+
OPTION_KEYS = %i[index if unless].freeze
|
7
|
+
private_constant :OPTION_KEYS
|
8
|
+
|
9
|
+
attr_reader :presence_fields, :value_fields, :block, :force, :options
|
10
|
+
|
11
|
+
def initialize(presence_fields:, value_fields:, block:, force: false)
|
12
|
+
@options = extract_options!(value_fields)
|
13
|
+
|
14
|
+
@presence_fields = presence_fields
|
15
|
+
@value_fields = value_fields
|
16
|
+
@block = block
|
17
|
+
@force = force
|
18
|
+
end
|
19
|
+
|
20
|
+
def fits?(query)
|
21
|
+
return false unless conditions_met_by?(query)
|
22
|
+
return true if force
|
23
|
+
|
24
|
+
(presence_fields.empty? && value_fields.empty?) ||
|
25
|
+
values_for(query.params).all? { |value| present?(value) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def values_for(params)
|
29
|
+
params.values_at(*presence_fields) + valued_values_for(params)
|
30
|
+
end
|
31
|
+
|
32
|
+
def present?(value)
|
33
|
+
value.respond_to?(:empty?) ? !value.empty? : !!value
|
34
|
+
end
|
35
|
+
|
36
|
+
def index
|
37
|
+
case options[:index]
|
38
|
+
when :first then -Float::INFINITY
|
39
|
+
when :last then Float::INFINITY
|
40
|
+
when Numeric then options[:index]
|
41
|
+
else 0
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
|
47
|
+
def extract_options!(fields)
|
48
|
+
fields.keys.each_with_object({}) do |key, options|
|
49
|
+
options[key] = fields.delete(key) if OPTION_KEYS.include?(key)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def valued_values_for(params)
|
54
|
+
value_fields.map do |field, required_value|
|
55
|
+
params[field] == required_value && required_value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def conditions_met_by?(query)
|
60
|
+
condition_met?(query, :if) && condition_met?(query, :unless)
|
61
|
+
end
|
62
|
+
|
63
|
+
def condition_met?(query, key)
|
64
|
+
return true unless options.key?(key)
|
65
|
+
|
66
|
+
condition = options[key]
|
67
|
+
|
68
|
+
value =
|
69
|
+
case condition
|
70
|
+
when String, Symbol then query.send(condition)
|
71
|
+
when Proc then query.instance_exec(&condition)
|
72
|
+
else condition
|
73
|
+
end
|
74
|
+
|
75
|
+
key == :if ? value : !value
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zen
|
4
|
+
class Query
|
5
|
+
module ApiMethods
|
6
|
+
def alias_subject_name(name)
|
7
|
+
@subject_name = name
|
8
|
+
|
9
|
+
define_singleton_method(name) { |&block| subject(&block) }
|
10
|
+
|
11
|
+
alias_method(name, :subject)
|
12
|
+
end
|
13
|
+
|
14
|
+
def subject_name
|
15
|
+
@subject_name || :subject
|
16
|
+
end
|
17
|
+
|
18
|
+
def raise_on_guard_violation(value)
|
19
|
+
@raise_on_guard_violation = !!value
|
20
|
+
end
|
21
|
+
|
22
|
+
def raise_on_guard_violation?
|
23
|
+
@raise_on_guard_violation
|
24
|
+
end
|
25
|
+
|
26
|
+
def subject(&block)
|
27
|
+
return @subject_block unless block_given?
|
28
|
+
|
29
|
+
@subject_block = block
|
30
|
+
end
|
31
|
+
|
32
|
+
def defaults(&block)
|
33
|
+
return @defaults_block unless block_given?
|
34
|
+
|
35
|
+
@defaults_block = block
|
36
|
+
end
|
37
|
+
|
38
|
+
def fetch_defaults
|
39
|
+
ancestors
|
40
|
+
.select { |mod| mod.respond_to?(:defaults) }
|
41
|
+
.map(&:defaults)
|
42
|
+
.compact
|
43
|
+
.reduce({}) { |result, block| result.merge!(block.call) }
|
44
|
+
end
|
45
|
+
|
46
|
+
def sift_by(*presence_fields, **value_fields, &block)
|
47
|
+
sift_blocks.push(
|
48
|
+
Query::ApiBlock.new(
|
49
|
+
presence_fields: presence_fields,
|
50
|
+
value_fields: value_fields,
|
51
|
+
block: block
|
52
|
+
)
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
def query_by(*presence_fields, **value_fields, &block)
|
57
|
+
query_blocks.push(
|
58
|
+
Query::ApiBlock.new(
|
59
|
+
presence_fields: presence_fields,
|
60
|
+
value_fields: value_fields,
|
61
|
+
block: block
|
62
|
+
)
|
63
|
+
)
|
64
|
+
end
|
65
|
+
|
66
|
+
alias sifter sift_by
|
67
|
+
alias query query_by
|
68
|
+
|
69
|
+
def sift_by!(*presence_fields, &block)
|
70
|
+
sift_blocks.push(
|
71
|
+
Query::ApiBlock.new(
|
72
|
+
presence_fields: presence_fields,
|
73
|
+
value_fields: {},
|
74
|
+
block: block,
|
75
|
+
force: true
|
76
|
+
)
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
def query_by!(*presence_fields, &block)
|
81
|
+
query_blocks.push(
|
82
|
+
Query::ApiBlock.new(
|
83
|
+
presence_fields: presence_fields,
|
84
|
+
value_fields: {},
|
85
|
+
block: block,
|
86
|
+
force: true
|
87
|
+
)
|
88
|
+
)
|
89
|
+
end
|
90
|
+
|
91
|
+
alias sifter! sift_by!
|
92
|
+
alias query! query_by!
|
93
|
+
|
94
|
+
def guard(message = nil, &block)
|
95
|
+
guard_blocks.push([message, block])
|
96
|
+
end
|
97
|
+
|
98
|
+
def sift_blocks
|
99
|
+
@sift_blocks ||= []
|
100
|
+
end
|
101
|
+
|
102
|
+
def query_blocks
|
103
|
+
@query_blocks ||= []
|
104
|
+
end
|
105
|
+
|
106
|
+
def guard_blocks
|
107
|
+
@guard_blocks ||= []
|
108
|
+
end
|
109
|
+
|
110
|
+
def sorted_query_blocks
|
111
|
+
query_blocks.sort do |a, b|
|
112
|
+
if a.index == b.index
|
113
|
+
query_blocks.index(a) <=> query_blocks.index(b)
|
114
|
+
else
|
115
|
+
a.index <=> b.index
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Zen
|
4
|
+
class Query
|
5
|
+
module Attributes
|
6
|
+
module ClassMethods
|
7
|
+
def inherited(query_class)
|
8
|
+
query_class.const_set(:AttributeMethods, Module.new)
|
9
|
+
query_class.send(:include, query_class::AttributeMethods)
|
10
|
+
query_class.attributes_list.replace(attributes_list.dup)
|
11
|
+
super
|
12
|
+
end
|
13
|
+
|
14
|
+
def attribute_methods
|
15
|
+
const_get(:AttributeMethods)
|
16
|
+
end
|
17
|
+
|
18
|
+
def attributes(*attrs)
|
19
|
+
attributes_list.concat(attrs)
|
20
|
+
|
21
|
+
attrs.each do |name|
|
22
|
+
attribute_methods.send(:define_method, name) { @attributes[name] }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def attributes_list
|
27
|
+
@attributes_list ||= []
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.included(target)
|
32
|
+
target.extend(ClassMethods)
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(**attrs)
|
36
|
+
attributes = attrs.dup
|
37
|
+
attributes.delete(:params)
|
38
|
+
assert_valid_attributes!(attributes)
|
39
|
+
@attributes = attributes
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def assert_valid_attributes!(attrs)
|
45
|
+
unknown_attrs = attrs.keys - self.class.attributes_list
|
46
|
+
|
47
|
+
raise(ArgumentError, "Unknown attributes #{unknown_attrs.inspect}") if unknown_attrs.any?
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/zen-query.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/zen/query/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "zen-query"
|
7
|
+
spec.version = Zen::Query::VERSION
|
8
|
+
spec.authors = ["Artem Kuzko"]
|
9
|
+
spec.email = ["a.kuzko@gmail.com"]
|
10
|
+
|
11
|
+
spec.summary = "Builds a params-sifted scope"
|
12
|
+
spec.description = 'Zen::Query class provides a way to dynamically
|
13
|
+
apply scopes or ActiveRecord (or any other ORM) query methods based on passed
|
14
|
+
params with a declarative and convenient API'
|
15
|
+
spec.homepage = "https://github.com/akuzko/zen-query"
|
16
|
+
spec.license = "MIT"
|
17
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.4.0")
|
18
|
+
|
19
|
+
spec.metadata["allowed_push_host"] = "https://rubygems.org/"
|
20
|
+
|
21
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
22
|
+
spec.metadata["source_code_uri"] = "https://github.com/akuzko/zen-query.git"
|
23
|
+
|
24
|
+
# Specify which files should be added to the gem when it is released.
|
25
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
26
|
+
spec.files = Dir.chdir(File.expand_path(__dir__)) do
|
27
|
+
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
|
28
|
+
end
|
29
|
+
spec.bindir = "exe"
|
30
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
31
|
+
spec.require_paths = ["lib"]
|
32
|
+
|
33
|
+
spec.add_development_dependency "bundler", ">= 2.1.0"
|
34
|
+
spec.add_development_dependency "pry"
|
35
|
+
spec.add_development_dependency "pry-nav"
|
36
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
37
|
+
spec.add_development_dependency "rspec", "~> 3.0"
|
38
|
+
spec.add_development_dependency "rspec-its", "~> 1.2"
|
39
|
+
spec.add_development_dependency "rubocop", "~> 0.80"
|
40
|
+
end
|
metadata
ADDED
@@ -0,0 +1,164 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: zen-query
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Artem Kuzko
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-05-11 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: 2.1.0
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: 2.1.0
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: pry
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry-nav
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '13.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '13.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rspec
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec-its
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '1.2'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '1.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.80'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.80'
|
111
|
+
description: |-
|
112
|
+
Zen::Query class provides a way to dynamically
|
113
|
+
apply scopes or ActiveRecord (or any other ORM) query methods based on passed
|
114
|
+
params with a declarative and convenient API
|
115
|
+
email:
|
116
|
+
- a.kuzko@gmail.com
|
117
|
+
executables: []
|
118
|
+
extensions: []
|
119
|
+
extra_rdoc_files: []
|
120
|
+
files:
|
121
|
+
- ".gitignore"
|
122
|
+
- ".rspec"
|
123
|
+
- ".rubocop.yml"
|
124
|
+
- ".travis.yml"
|
125
|
+
- CHANGELOG
|
126
|
+
- Gemfile
|
127
|
+
- LICENSE.txt
|
128
|
+
- README.md
|
129
|
+
- Rakefile
|
130
|
+
- bin/console
|
131
|
+
- bin/setup
|
132
|
+
- lib/zen/query.rb
|
133
|
+
- lib/zen/query/api_block.rb
|
134
|
+
- lib/zen/query/api_methods.rb
|
135
|
+
- lib/zen/query/attributes.rb
|
136
|
+
- lib/zen/query/version.rb
|
137
|
+
- zen-query.gemspec
|
138
|
+
homepage: https://github.com/akuzko/zen-query
|
139
|
+
licenses:
|
140
|
+
- MIT
|
141
|
+
metadata:
|
142
|
+
allowed_push_host: https://rubygems.org/
|
143
|
+
homepage_uri: https://github.com/akuzko/zen-query
|
144
|
+
source_code_uri: https://github.com/akuzko/zen-query.git
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: 2.4.0
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
requirements: []
|
160
|
+
rubygems_version: 3.1.6
|
161
|
+
signing_key:
|
162
|
+
specification_version: 4
|
163
|
+
summary: Builds a params-sifted scope
|
164
|
+
test_files: []
|