parascope 0.2.0 → 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 +4 -4
- data/README.md +202 -10
- data/bin/console +0 -6
- data/lib/parascope.rb +3 -0
- data/lib/parascope/query.rb +4 -7
- data/lib/parascope/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fc98be2a5b2713d248126d205faadbc007153c7a
|
4
|
+
data.tar.gz: 8f0dadd459a841defd277566b14740f9d7c7dc52
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 02fe0357a9573f7e6d9f9e02506d6c30d50c724050a5786c20ddf5981a8a304cf19a9e7b8c971998977cf97a4ee5e3f2bf33b1173cdfe42513c7024f6d8e2482
|
7
|
+
data.tar.gz: 630a5784a2c1e339945639a45e8a3cdf03e4749c5714f725a8e126788f1ec0c84064c68e79b9e2535fbb6108afd5b91e3fea50fcf33fdf1ad39abc5479bbc2b8
|
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
[](http://travis-ci.org/akuzko/parascope)
|
4
4
|
|
5
|
-
|
5
|
+
Param-based scope generation.
|
6
6
|
|
7
7
|
--
|
8
8
|
|
@@ -60,6 +60,30 @@ scope manipulations using `query_by`, `sift_by` and other class methods bellow.
|
|
60
60
|
- `query(&block)` declares scope-generation block that is always executed. As `query_by`,
|
61
61
|
accepts `:index`, `:if` and `:unless` options.
|
62
62
|
|
63
|
+
*Examples:*
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# executes block only when params[:department_id] is non-empty:
|
67
|
+
query_by(:department_id) { |id| scope.where(department_id: id) }
|
68
|
+
|
69
|
+
# executes block only when params[:only_active] == 'true':
|
70
|
+
query_by(only_active: 'true') { scope.active }
|
71
|
+
|
72
|
+
# executes block only when *both* params[:first_name] and params[:last_name]
|
73
|
+
# are present:
|
74
|
+
query_by(:first_name, :last_name) { |first_name, last_name| /* ... */ }
|
75
|
+
|
76
|
+
# if query block returns nil, scope will remain intact:
|
77
|
+
query { scope.active if only_active? }
|
78
|
+
|
79
|
+
# conditional example:
|
80
|
+
query(if: :include_inactive?) { scope.with_inactive }
|
81
|
+
|
82
|
+
def include_inactive?
|
83
|
+
company.settings.include_inactive?
|
84
|
+
end
|
85
|
+
```
|
86
|
+
|
63
87
|
- `sift_by(*presence_fields, **value_fields, &block)` method is used to hoist sets of
|
64
88
|
query definitions that should be applied if, and only if, all specified values
|
65
89
|
match criteria in the same way as in `query_by` method. Just like `query_by` method,
|
@@ -69,30 +93,96 @@ scope manipulations using `query_by`, `sift_by` and other class methods bellow.
|
|
69
93
|
- `sifter` alias for `sift_by`. Results in a more readable construct when a single
|
70
94
|
presence field is passed. For example, `sifter(:paginated)`.
|
71
95
|
|
96
|
+
*Examples:*
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
sift_by(:search_value, :search_type) do |value|
|
100
|
+
# definitions in this block will be applied only if *both* params[:search_value]
|
101
|
+
# and params[:search_type] are present
|
102
|
+
|
103
|
+
search_value = "%#{value}%"
|
104
|
+
|
105
|
+
query_by(search_type: 'name') { scope.name_like(value) }
|
106
|
+
query_by(search_type: 'email') { scope.where("users.email LIKE ?", search_value) }
|
107
|
+
end
|
108
|
+
|
109
|
+
sifter :paginated do
|
110
|
+
query_by(:page, :per_page) do |page, per|
|
111
|
+
scope.page(page).per(per)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def paginated_records
|
116
|
+
resolved_scope(:paginated)
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
72
120
|
- `base_scope(&block)` method is used to define a base scope as a starting point
|
73
121
|
of scope-generating process. If this method is called from `sift_by` block,
|
74
122
|
top-level base scope is yielded to the method block. Note that `base_scope` will
|
75
123
|
not be called if query is initialized with a given scope.
|
76
124
|
|
125
|
+
*Examples:*
|
126
|
+
|
127
|
+
```ruby
|
128
|
+
base_scope { company.users }
|
129
|
+
|
130
|
+
sifter :with_department do
|
131
|
+
base_scope { |scope| scope.joins(:department) }
|
132
|
+
end
|
133
|
+
```
|
134
|
+
|
77
135
|
- `defaults(hash)` method is used to declare default query params that are reverse
|
78
136
|
merged with params passed on query initialization. When used in `sift_by` block,
|
79
137
|
hashes are merged altogether.
|
80
138
|
|
139
|
+
*Examples:*
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
defaults only_active: true
|
143
|
+
|
144
|
+
sifter :paginated do
|
145
|
+
# sifter defaults are merged with higher-level defaults:
|
146
|
+
defaults page: 1, per_page: 25
|
147
|
+
end
|
148
|
+
```
|
149
|
+
|
81
150
|
- `guard(&block)` defines a guard instance method block (see instance methods
|
82
151
|
bellow). All such blocks are executed before query object resolves scope via
|
83
152
|
`resolve_scope` method.
|
84
153
|
|
154
|
+
*Examples:*
|
155
|
+
|
156
|
+
```ruby
|
157
|
+
sift_by(:sort_col, :sort_dir) do |scol, sdir|
|
158
|
+
# will raise Parascope::GuardViolationError on scope resolution if
|
159
|
+
# params[:sort_dir] is not 'asc' or 'desc'
|
160
|
+
guard { sdir.downcase.in?(%w(asc desc)) }
|
161
|
+
|
162
|
+
base_scope { |scope| scope.order(scol => sdir) }
|
163
|
+
end
|
164
|
+
```
|
165
|
+
|
85
166
|
#### Instance Methods
|
86
167
|
|
87
168
|
- `initialize(params, scope: nil, **attributes)` initializes a query with `params`,
|
88
169
|
an optional scope (that if passed, is used instead of `base_scope`). All additionally
|
89
170
|
passed options are accessible via reader methods in query blocks and elsewhere.
|
90
171
|
|
172
|
+
- `build(scope: nil, **attributes)` initializes a query with empty params. Handy when
|
173
|
+
query depends only passed attributes and internal logic. Also useful in specs.
|
174
|
+
|
175
|
+
*Examples:*
|
176
|
+
|
177
|
+
```ruby
|
178
|
+
query = UsersQuery.new(query_params, company: company)
|
179
|
+
|
180
|
+
query = UsersQuery.build(scope: users_scope)
|
181
|
+
```
|
182
|
+
|
91
183
|
- `params` returns a parameters passed in initialization. Is a `Hashie::Mash` instance,
|
92
184
|
thus, values can be accessible via reader methods.
|
93
185
|
|
94
|
-
- `[](key)` delegates to query `params` for slightly easier values access.
|
95
|
-
|
96
186
|
- `scope` "current" scope of query object. For an initialized query object corresponds
|
97
187
|
to base scope. Primary usage is to call this method in `query_by` blocks and return
|
98
188
|
it's mutated version corresponding to passed `query_by` arguments.
|
@@ -101,6 +191,18 @@ scope manipulations using `query_by`, `sift_by` and other class methods bellow.
|
|
101
191
|
`GuardViolationError` is raised. You can use this method to ensure safety of param
|
102
192
|
values interpolation to a SQL string in a `query_by` block for example.
|
103
193
|
|
194
|
+
*Examples:*
|
195
|
+
|
196
|
+
```ruby
|
197
|
+
query_by(:sort_col, :sort_dir) do |scol, sdir|
|
198
|
+
# will raise Parascope::GuardViolationError on scope resolution if
|
199
|
+
# params[:sort_dir] is not 'asc' or 'desc'
|
200
|
+
guard { sdir.downcase.in?(%w(asc desc)) }
|
201
|
+
|
202
|
+
scope.order(scol => sdir)
|
203
|
+
end
|
204
|
+
```
|
205
|
+
|
104
206
|
- `resolved_scope(*presence_keys, override_params = {})` returns a resulting scope
|
105
207
|
generated by all queries and sifted queries that fit to query params applied to
|
106
208
|
base scope. Optionally, additional params may be passed to override the ones passed on
|
@@ -109,7 +211,37 @@ scope manipulations using `query_by`, `sift_by` and other class methods bellow.
|
|
109
211
|
`resolved_scope(with_projects: true)`). It's the main `Query` instance method that
|
110
212
|
returns the sole purpose of it's instances.
|
111
213
|
|
112
|
-
|
214
|
+
*Examples:*
|
215
|
+
|
216
|
+
```ruby
|
217
|
+
defaults only_active: true
|
218
|
+
|
219
|
+
base_scope { company.users }
|
220
|
+
|
221
|
+
query_by(:only_active) { scope.active }
|
222
|
+
|
223
|
+
sifter :with_departments do
|
224
|
+
scope.joins(:departments)
|
225
|
+
|
226
|
+
query_by(:department_name) { |name| scope.where(departments: {name: name}) }
|
227
|
+
end
|
228
|
+
|
229
|
+
def users
|
230
|
+
@users ||= resolved_scope
|
231
|
+
end
|
232
|
+
|
233
|
+
# you can use options to overwrite defaults:
|
234
|
+
def all_users
|
235
|
+
resolved_scope(only_active: false)
|
236
|
+
end
|
237
|
+
|
238
|
+
# or to apply a sifter with additional params:
|
239
|
+
def managers
|
240
|
+
resolved_scope(:with_departments, department_name: 'managers')
|
241
|
+
end
|
242
|
+
```
|
243
|
+
|
244
|
+
### Composite usage example with ActiveRecord Relation as a scope
|
113
245
|
|
114
246
|
```ruby
|
115
247
|
class UserQuery < Parascope::Query
|
@@ -117,13 +249,9 @@ class UserQuery < Parascope::Query
|
|
117
249
|
|
118
250
|
base_scope { company.users }
|
119
251
|
|
120
|
-
query_by
|
121
|
-
scope.active
|
122
|
-
end
|
252
|
+
query_by(:only_active) { scope.active }
|
123
253
|
|
124
|
-
query_by
|
125
|
-
scope.by_birtdate(date)
|
126
|
-
end
|
254
|
+
query_by(:birthdate) { |date| scope.by_birtdate(date) }
|
127
255
|
|
128
256
|
query_by :name do |name|
|
129
257
|
scope.where("CONCAT(first_name, ' ', last_name) LIKE ?", "%#{name}%")
|
@@ -169,6 +297,70 @@ query.project_users # => this is the same as:
|
|
169
297
|
# .order("CONCAT(first_name, ' ', last_name) DESC")
|
170
298
|
```
|
171
299
|
|
300
|
+
### Hints and Tips
|
301
|
+
|
302
|
+
- Keep in mind that query classes are just plain Ruby classes. All `sifter`,
|
303
|
+
`query_by` and `guard` declarations are inherited, as well as default params
|
304
|
+
declared by `defaults` method. Thus, you can define a BaseQuery with common
|
305
|
+
definitions as a base class for queries in your application. Or you can define
|
306
|
+
query API blocks in some module's `included` callback to share common definitions
|
307
|
+
via module inclusion.
|
308
|
+
|
309
|
+
- Being plain Ruby classes also means you can easily extend default functionality
|
310
|
+
for your needs. For example, if you're querying ActiveRecord relations, and your
|
311
|
+
primary use case looks like
|
312
|
+
|
313
|
+
```ruby
|
314
|
+
query_by(:some_field_id) { |id| scope.where(some_field_id: id) }
|
315
|
+
```
|
316
|
+
you can do the following to make things more DRY:
|
317
|
+
|
318
|
+
```ruby
|
319
|
+
class ApplicationQuery < Parascope::Query
|
320
|
+
def self.query_by(*fields, &block)
|
321
|
+
block ||= default_query_block(fields)
|
322
|
+
super(*fields, &block)
|
323
|
+
end
|
324
|
+
|
325
|
+
def self.default_query_block(fields)
|
326
|
+
->(*values){ scope.where(Hash[fields.zip(values)]) }
|
327
|
+
end
|
328
|
+
private_class_method :default_query_block
|
329
|
+
end
|
330
|
+
```
|
331
|
+
|
332
|
+
and then you can simply call
|
333
|
+
|
334
|
+
```ruby
|
335
|
+
class UsersQuery < ApplicationQuery
|
336
|
+
base_scope { company.users }
|
337
|
+
|
338
|
+
query_by :first_name
|
339
|
+
query_by :last_name
|
340
|
+
query_by :city, :street_address
|
341
|
+
end
|
342
|
+
```
|
343
|
+
|
344
|
+
Or you can go a little further and declare a class method
|
345
|
+
|
346
|
+
```ruby
|
347
|
+
class ApplicationQuery
|
348
|
+
def self.query_by_fields(*fields)
|
349
|
+
fields.each do |field|
|
350
|
+
query_by field
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
```
|
355
|
+
|
356
|
+
and then
|
357
|
+
|
358
|
+
```ruby
|
359
|
+
class UserQuery < ApplicationQuery
|
360
|
+
query_by_fields :first_name, :last_name, :department_id
|
361
|
+
end
|
362
|
+
```
|
363
|
+
|
172
364
|
## Development
|
173
365
|
|
174
366
|
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
data/bin/console
CHANGED
@@ -3,9 +3,6 @@
|
|
3
3
|
require "bundler/setup"
|
4
4
|
require "parascope"
|
5
5
|
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
6
|
require 'ostruct'
|
10
7
|
|
11
8
|
class Query < Parascope::Query
|
@@ -65,6 +62,3 @@ q = Query.new(foo: 'foo', bar: 'bar', baz: 'baz', bak: 'bak', nested_baz: 'nb',
|
|
65
62
|
|
66
63
|
require "pry"
|
67
64
|
Pry.start
|
68
|
-
|
69
|
-
# require "irb"
|
70
|
-
# IRB.start
|
data/lib/parascope.rb
CHANGED
data/lib/parascope/query.rb
CHANGED
@@ -5,16 +5,9 @@ module Parascope
|
|
5
5
|
autoload :ApiMethods, "parascope/query/api_methods"
|
6
6
|
autoload :ApiBlock, "parascope/query/api_block"
|
7
7
|
|
8
|
-
extend Forwardable
|
9
8
|
extend ApiMethods
|
10
9
|
|
11
|
-
UndefinedScopeError = Class.new(StandardError)
|
12
|
-
GuardViolationError = Class.new(ArgumentError)
|
13
|
-
# for backward-compatability
|
14
|
-
UnpermittedError = GuardViolationError
|
15
|
-
|
16
10
|
attr_reader :params
|
17
|
-
def_delegator :params, :[]
|
18
11
|
|
19
12
|
def self.inherited(subclass)
|
20
13
|
subclass.query_blocks.replace query_blocks.dup
|
@@ -24,6 +17,10 @@ module Parascope
|
|
24
17
|
subclass.defaults defaults
|
25
18
|
end
|
26
19
|
|
20
|
+
def self.build(**attrs)
|
21
|
+
new({}, **attrs)
|
22
|
+
end
|
23
|
+
|
27
24
|
def initialize(params, scope: nil, **attrs)
|
28
25
|
@params = Hashie::Mash.new(klass.defaults).merge(params || {})
|
29
26
|
@scope = scope unless scope.nil?
|
data/lib/parascope/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: parascope
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Artem Kuzko
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-09-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: hashie
|