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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d24fcd353f9b0a8dc2091b654e35c4f6b3f973c4
4
- data.tar.gz: 23a9825594b4ab16faa6deb34b4da2b5232fdd8c
3
+ metadata.gz: fc98be2a5b2713d248126d205faadbc007153c7a
4
+ data.tar.gz: 8f0dadd459a841defd277566b14740f9d7c7dc52
5
5
  SHA512:
6
- metadata.gz: c27284dc7a29103d72502a5db3232eca92fb99341df633e0f77c7c905a269e85bdc75ecf4a45e8a5945b94e0dbb72a289aa76fe7b24f41341d64421fec547e44
7
- data.tar.gz: f5ae369d56e2ae924a358e950e8196127c1c31bc8a9c13d833fe479a6dcf36489b678502f3cebfee398f04d5281625afbef2c92ace9b24e2f37cafacb35fce73
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
- Because `periscope` is already taken.
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
- ### Usage example with ActiveRecord Relation as a scope
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 :only_active do
121
- scope.active
122
- end
252
+ query_by(:only_active) { scope.active }
123
253
 
124
- query_by :birthdate do |date|
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
@@ -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
@@ -1,5 +1,8 @@
1
1
  require "parascope/version"
2
2
 
3
3
  module Parascope
4
+ UndefinedScopeError = Class.new(StandardError)
5
+ GuardViolationError = Class.new(ArgumentError)
6
+
4
7
  autoload :Query, "parascope/query"
5
8
  end
@@ -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?
@@ -1,3 +1,3 @@
1
1
  module Parascope
2
- VERSION = "0.2.0"
2
+ VERSION = "1.0.0"
3
3
  end
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.2.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-05-29 00:00:00.000000000 Z
11
+ date: 2016-09-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: hashie