parascope 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://secure.travis-ci.org/akuzko/parascope.png)](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
|