rubanok 0.1.1 โ†’ 0.4.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
  SHA256:
3
- metadata.gz: 5b5cdbefb98c951e26be6bfcd8029250f8b314c9b7bb86e5d5d778bc340b4d40
4
- data.tar.gz: abd725823622bdf1ff062cc1496dfd121522e6b866ec01ce2bc9ea421a56785e
3
+ metadata.gz: 90f6ca9dfd61a6f143eff1f3ab3c2d7e41502c992879af785dcf414af15249fd
4
+ data.tar.gz: 8807cbb2a680e9fbc739a9f0214fb4753e24ee55e69da72d9b45c7f121900dd9
5
5
  SHA512:
6
- metadata.gz: aeec1ebbf07db8ec31046cf1442ffac9e6cae698e979768e8560d189f7ab23bfc13d9ef2979dbd45fd00490a5101e3f3b3900d4ff2425b2112880915e8446ba6
7
- data.tar.gz: e66af487b2f7b82626d790bc0746559102de30ec0220e5d12de1ce0f29c79c98ed0bad6744ab90df5c5a931b421d90d1b4b4d082e70d15f714efb49fb56b2418
6
+ metadata.gz: 2e015d7c3c517e42e5fe42037035db950cc87f9bd0099b38580c03ad156c36d0133681bd659451e8bdb58f15254799ce8d7b3158a1dc998210e3a425867a521d
7
+ data.tar.gz: 6c22e9a876e86f144726e8dbbe79b42f9d3052feb6f9c31688f7259872a5eed1e5ac66b031b890e0d1b4eceba7d3903331a073f2e13321b9f3d45c9fe53229d2
data/CHANGELOG.md CHANGED
@@ -2,6 +2,64 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.4.0 (2021-03-05)
6
+
7
+ - Ruby 3.0 compatibility. ([@palkan][])
8
+
9
+ - Add RBS. ([@palkan][])
10
+
11
+ ## 0.3.0 (2020-10-21)
12
+
13
+ - Add `filter_with: Symbol | Proc` option to `.map` to allowing filtering the input value. ([@palkan][])
14
+
15
+ - Allow specifying `ignore_empty_values: *` per rule. ([@palkan][])
16
+
17
+ - Add `prepare` DSL method to transform the input once before the first rule is activated. ([@palkan][])
18
+
19
+ When no rules match, the method is not called.
20
+ Useful when you want to perform some default transformations.
21
+
22
+ ## 0.2.1 (2019-08-24)
23
+
24
+ - Fix bug with trying to add a helper for API controller. ([@palkan][])
25
+
26
+ Fixes [#10](https://github.com/palkan/rubanok/issues/10).
27
+
28
+ ## 0.2.0 (2019-08-23)
29
+
30
+ - Add `Process.project` and `rubanok_scope` methods to get the Hash of recognized params. ([@palkan][])
31
+
32
+ ```ruby
33
+ class PostsProcessor < Rubanok::Processor
34
+ map(:q) { block }
35
+ match(:page, :per_page, activate_on: :page) { block }
36
+ end
37
+
38
+ PostsProcessor.project(q: "search_me", filter: "smth", page: 2)
39
+ # => { q: "search_me", page: 2 }
40
+
41
+ class PostsController < ApplicationController
42
+ def index
43
+ @filter_params = rubanok_scope
44
+ # or
45
+ @filter_params = rubanok_scope params.require(:filter), with: PostsProcessor
46
+ # ...
47
+ end
48
+ end
49
+ ```
50
+
51
+ - Improve naming by using "processor" instead of "plane". ([@palkan][])
52
+
53
+ See [the discussion](https://github.com/palkan/rubanok/issues/3).
54
+
55
+ **NOTE**: Older API is still available without deprecation.
56
+
57
+ - Add `fail_when_no_matches` parameter to `match` method. ([@Earendil95][])
58
+
59
+ ## 0.1.3 (2019-03-05)
60
+
61
+ - Fix using `activate_always: true` with `default` matching clause. ([@palkan][])
62
+
5
63
  ## 0.1.1 (2019-01-16)
6
64
 
7
65
  - Fix RSpec matcher to call original implementation instead of returning `nil`. ([@palkan][])
@@ -15,3 +73,4 @@ Initial implementation.
15
73
  Proposal added.
16
74
 
17
75
  [@palkan]: https://github.com/palkan
76
+ [@Earendil95]: https://github.com/Earendil95
data/LICENSE.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2018 Vladimir Dementyev
3
+ Copyright (c) 2018-2020 Vladimir Dementyev
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,9 +1,12 @@
1
- [![Gem Version](https://badge.fury.io/rb/rubanok.svg)](https://rubygems.org/gems/rubanok) [![Build Status](https://travis-ci.org/palkan/rubanok.svg?branch=master)](https://travis-ci.org/palkan/rubanok)
1
+ [![Gem Version](https://badge.fury.io/rb/rubanok.svg)](https://rubygems.org/gems/rubanok)
2
+ ![Build](https://github.com/palkan/rubanok/workflows/Build/badge.svg)
2
3
 
3
4
  # Rubanok
4
5
 
5
6
  Rubanok provides a DSL to build parameters-based data transformers.
6
7
 
8
+ ๐Ÿ“– Read the introduction post: ["Carve your controllers like Papa Carlo"](https://evilmartians.com/chronicles/rubanok-carve-your-rails-controllers-like-papa-carlo)
9
+
7
10
  The typical usage is to describe all the possible collection manipulation for REST `index` action, e.g. filtering, sorting, searching, pagination, etc..
8
11
 
9
12
  So, instead of:
@@ -11,12 +14,12 @@ So, instead of:
11
14
  ```ruby
12
15
  class CourseSessionController < ApplicationController
13
16
  def index
14
- @sessions = CourseSession.
15
- search(params[:q]).
16
- by_course_type(params[:course_type_id]).
17
- by_role(params[:role_id]).
18
- paginate(page_params).
19
- order(ordering_params)
17
+ @sessions = CourseSession
18
+ .search(params[:q])
19
+ .by_course_type(params[:course_type_id])
20
+ .by_role(params[:role_id])
21
+ .paginate(page_params)
22
+ .order(ordering_params)
20
23
  end
21
24
  end
22
25
  ```
@@ -26,13 +29,13 @@ You have:
26
29
  ```ruby
27
30
  class CourseSessionController < ApplicationController
28
31
  def index
29
- @sessions = planish(
32
+ @sessions = rubanok_process(
30
33
  # pass input
31
34
  CourseSession.all,
32
35
  # pass params
33
36
  params,
34
- # provide a plane to use
35
- with: CourseSessionsPlane
37
+ # provide a processor to use
38
+ with: CourseSessionsProcessor
36
39
  )
37
40
  end
38
41
  end
@@ -40,34 +43,42 @@ end
40
43
 
41
44
  Or we can try to infer all the configuration for you:
42
45
 
43
-
44
46
  ```ruby
45
47
  class CourseSessionController < ApplicationController
46
48
  def index
47
- @sessions = planish(CourseSession.all)
49
+ @sessions = rubanok_process(CourseSession.all)
48
50
  end
49
51
  end
50
52
  ```
51
53
 
52
54
  Requirements:
55
+
53
56
  - Ruby ~> 2.5
54
- - Rails >= 4.2 (only for using with Rails)
57
+ - (optional\*) Rails >= 5.2 (Rails 4.2 should work but we don't test against it anymore)
58
+
59
+ \* This gem has no dependency on Rails.
55
60
 
56
61
  <a href="https://evilmartians.com/">
57
62
  <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
58
63
 
59
64
  ## Installation
60
65
 
61
- **This gem hasn't been released (and even built) yet.**
66
+ Add to your `Gemfile`:
67
+
68
+ ```ruby
69
+ gem "rubanok"
70
+ ```
71
+
72
+ And run `bundle install`.
62
73
 
63
74
  ## Usage
64
75
 
65
- The core concept of this library is a _plane_ (or _hand plane_, or "ั€ัƒะฑะฐะฝะพะบ" in Russian). Plane is responsible for mapping parameters to transformrations.
76
+ The core concept of this library is a processor (previously called _plane_ or _hand plane_, or "ั€ัƒะฑะฐะฝะพะบ" in Russian). Processor is responsible for mapping parameters to transformations.
66
77
 
67
78
  From the example above:
68
79
 
69
80
  ```ruby
70
- class CourseSessionsPlane < Rubanok::Plane
81
+ class CourseSessionsProcessor < Rubanok::Processor
71
82
  # You can map keys
72
83
  map :q do |q:|
73
84
  # `raw` is an accessor for input data
@@ -76,7 +87,7 @@ class CourseSessionsPlane < Rubanok::Plane
76
87
  end
77
88
 
78
89
  # The following code
79
- CourseSessionsPlane.call(CourseSession.all, q: "xyz")
90
+ CourseSessionsProcessor.call(CourseSession.all, q: "xyz")
80
91
 
81
92
  # is equal to
82
93
  CourseSession.all.search("xyz")
@@ -85,7 +96,7 @@ CourseSession.all.search("xyz")
85
96
  You can map multiple keys at once:
86
97
 
87
98
  ```ruby
88
- class CourseSessionsPlane < Rubanok::Plane
99
+ class CourseSessionsProcessor < Rubanok::Processor
89
100
  DEFAULT_PAGE_SIZE = 25
90
101
 
91
102
  map :page, :per_page do |page:, per_page: DEFAULT_PAGE_SIZE|
@@ -97,9 +108,9 @@ end
97
108
  There is also `match` method to handle values:
98
109
 
99
110
  ```ruby
100
- class CourseSessionsPlane < Rubanok::Plane
101
- SORT_ORDERS = %w(asc desc).freeze
102
- SORTABLE_FIELDS = %w(id name created_at).freeze
111
+ class CourseSessionsProcessor < Rubanok::Processor
112
+ SORT_ORDERS = %w[asc desc].freeze
113
+ SORTABLE_FIELDS = %w[id name created_at].freeze
103
114
 
104
115
  match :sort_by, :sort do
105
116
  having "course_id", "desc" do
@@ -124,16 +135,94 @@ class CourseSessionsPlane < Rubanok::Plane
124
135
  raw.order(sort_by => sort)
125
136
  end
126
137
  end
138
+
139
+ # strict matching; if Processor will not match parameter, it will raise Rubanok::UnexpectedInputError
140
+ # You can handle it in controller, for example, with sending 422 Unprocessable Entity to client
141
+ match :filter, fail_when_no_matches: true do
142
+ having "active" do
143
+ raw.active
144
+ end
145
+
146
+ having "finished" do
147
+ raw.finished
148
+ end
149
+ end
150
+ end
151
+ ```
152
+
153
+ By default, Rubanok will not fail if no matches found in `match` rule. You can change it by setting: `Rubanok.fail_when_no_matches = true`.
154
+ If in example above you will call `CourseSessionsProcessor.call(CourseSession, filter: 'acitve')`, you will get `Rubanok::UnexpectedInputError: Unexpected input: {:filter=>'acitve'}`.
155
+
156
+ **NOTE:** Rubanok only matches exact values; more complex matching could be added in the future.
157
+
158
+ ### Default transformation
159
+
160
+ Sometimes it's useful to perform some transformations before **any** rule is activated.
161
+
162
+ There is a special `prepare` method which allows you to define the default transformation:
163
+
164
+ ```ruby
165
+ class CourseSearchQueryProcessor < Rubanok::Processor
166
+ prepare do
167
+ next if raw&.dig(:query, :bool)
168
+
169
+ {query: {bool: {filters: []}}}
170
+ end
171
+
172
+ map :ids do |ids:|
173
+ raw.dig(:query, :bool, :filters) << {terms: {id: ids}}
174
+ raw
175
+ end
127
176
  end
128
177
  ```
129
178
 
130
- **NOTE:** matching only match the exact values; more complex matching could be added in the future.
179
+ The block should return a new initial value for the _raw_ input or `nil` (no transformation required).
180
+
181
+ The `prepare` callback is not executed if no params match, e.g.:
182
+
183
+ ```ruby
184
+ CourseSearchQueryProcessor.call(nil, {}) #=> nil
185
+
186
+ # But
187
+ CourseSearchQueryProcessor.call(nil, {ids: [1]}) #=> {query {bool: {filters: [{terms: {ids: [1]}}]}}}
188
+
189
+ # Note that we can omit the first argument altogether
190
+ CourseSearchQueryProcessor.call({ids: [1]})
191
+ ```
192
+
193
+ ### Getting the matching params
194
+
195
+ Sometimes it could be useful to get the params that were used to process the data by Rubanok processor (e.g., you can use this data in views to display the actual filters state).
196
+
197
+ In Rails, you can use the `#rubanok_scope` method for that:
198
+
199
+ ```ruby
200
+ class CourseSessionController < ApplicationController
201
+ def index
202
+ @sessions = rubanok_process(CourseSession.all)
203
+ # Returns the Hash of params recognized by the CourseSessionProcessor.
204
+ # For example:
205
+ #
206
+ # params == {q: "search", role_id: 2, date: "2019-08-22"}
207
+ # @session_filter == {q: "search", role_id: 2}
208
+ @sessions_filter = rubanok_scope(
209
+ params.permit(:q, :role_id),
210
+ with: CourseSessionProcessor
211
+ )
212
+
213
+ # You can omit all the arguments
214
+ @sessions_filter = rubanok_scope #=> equals to rubanok_scope(params, with: implicit_rubanok_class)
215
+ end
216
+ end
217
+ ```
218
+
219
+ You can also accesss `rubanok_scope` in views (it's a helper method).
131
220
 
132
221
  ### Rule activation
133
222
 
134
223
  Rubanok _activates_ a rule by checking whether the corresponding keys are present in the params object. All the fields must be present to apply the rule.
135
224
 
136
- Sometimes you might want to make some fields optional (or event all of them). You can use `activate_on` and `activate_always` options for that:
225
+ Some fields may be optional, or perhaps even all of them. You can use `activate_on` and `activate_always` options to mark something as an optional key instead of a required one:
137
226
 
138
227
  ```ruby
139
228
  # Always apply the rule; use default values for keyword args
@@ -147,18 +236,52 @@ match :sort_by, :sort, activate_on: :sort_by do
147
236
  end
148
237
  ```
149
238
 
150
- By default, Rubanok ignores empty param values (using `#empty?` under the hood) and do not activate the matching rules (i.e. `{ q: "" }` or `{ q: nil }` won't activate the `map :q` rule).
239
+ By default, Rubanok ignores empty param values (using `#empty?` under the hood) and will not run matching rules on those values. For example: `{ q: "" }` and `{ q: nil }` won't activate the `map :q` rule.
240
+
241
+ You can change this behaviour by specifying `ignore_empty_values: true` option for a particular rule or enabling this behaviour globally via `Rubanok.ignore_empty_values = true` (enabled by default).
242
+
243
+ ### Input values filtering
151
244
 
152
- You can change this behaviour by setting: `Rubanok.ignore_empty_values = false`.
245
+ For complex input types, such as arrays, it might be useful to _prepare_ the value before passing to a transforming block or prevent the activation altogether.
246
+
247
+ We provide a `filter_with:` option for the `.map` method, which could be used as follows:
248
+
249
+ ```ruby
250
+ class PostsProcessor < Rubanok::Processor
251
+ # We can pass a Proc
252
+ map :ids, filter_with: ->(vals) { vals.reject(&:blank?).presence } do |ids:|
253
+ raw.where(id: ids)
254
+ end
255
+
256
+ # or define a class method
257
+ def self.non_empty_array(val)
258
+ non_blank = val.reject(&:blank?)
259
+ return if non_blank.empty?
260
+
261
+ non_blank
262
+ end
263
+
264
+ # and pass its name as a filter_with value
265
+ map :ids, filter_with: :non_empty_array do |ids:|
266
+ raw.where(id: ids)
267
+ end
268
+ end
269
+
270
+ # Filtered values are used in rules
271
+ PostsProcessor.call(Post.all, {ids: ["1", ""]}) == Post.where(id: ["1"])
272
+
273
+ # When filter returns empty value, the rule is not applied
274
+ PostsProcessor.call(Post.all, {ids: [nil, ""]}) == Post.all
275
+ ```
153
276
 
154
277
  ### Testing
155
278
 
156
- One of the benefits of having all the modification logic in its own class is the ability to test it in isolation:
279
+ One of the benefits of having modification logic contained in its own class is the ability to test modifications in isolation:
157
280
 
158
281
  ```ruby
159
282
  # For example, with RSpec
160
- describe CourseSessionsPlane do
161
- let(:input ) { CourseSession.all }
283
+ RSpec.describe CourseSessionsProcessor do
284
+ let(:input) { CourseSession.all }
162
285
  let(:params) { {} }
163
286
 
164
287
  subject { described_class.call(input, params) }
@@ -174,19 +297,19 @@ end
174
297
  Now in your controller you only have to test that the specific _plane_ is applied:
175
298
 
176
299
  ```ruby
177
- describe CourseSessionController do
300
+ RSpec.describe CourseSessionController do
178
301
  subject { get :index }
179
302
 
180
303
  specify do
181
- expect { subject }.to have_planished(CourseSession.all).
182
- with(CourseSessionsPlane)
304
+ expect { subject }.to have_rubanok_processed(CourseSession.all)
305
+ .with(CourseSessionsProcessor)
183
306
  end
184
307
  end
185
308
  ```
186
309
 
187
310
  **NOTE**: input matching only checks for the class equality.
188
311
 
189
- To use `have_planished` matcher you must add the following line to your `spec_helper.rb` / `rails_helper.rb` (it's added automatically if RSpec defined and `RAILS_ENV`/`RACK_ENV` is equal to `"test"`):
312
+ To use `have_rubanok_processed` matcher you must add the following line to your `spec_helper.rb` / `rails_helper.rb` (it's added automatically if RSpec defined and `RAILS_ENV`/`RACK_ENV` is equal to `"test"`):
190
313
 
191
314
  ```ruby
192
315
  require "rubanok/rspec"
@@ -194,10 +317,85 @@ require "rubanok/rspec"
194
317
 
195
318
  ### Rails vs. non-Rails
196
319
 
197
- Rubanok is a Rails-free library but has some useful Rails extensions, such as `planish` helper for controllers (included automatically into `ActionController::Base` and `ActionController::API`).
320
+ Rubanok does not require Rails, but it has some useful Rails extensions such as `rubanok_process` helper for controllers (included automatically into `ActionController::Base` and `ActionController::API`).
198
321
 
199
322
  If you use `ActionController::Metal` you must include the `Rubanok::Controller` module yourself.
200
323
 
324
+ ### Processor class inference in Rails controllers
325
+
326
+ By default, `rubanok_process` uses the following algorithm to define a processor class: `"#{controller_path.classify.pluralize}Processor".safe_constantize`.
327
+
328
+ You can change this by overriding the `#implicit_rubanok_class` method:
329
+
330
+ ```ruby
331
+ class ApplicationController < ActionController::Smth
332
+ # override the `implicit_rubanok_class` method
333
+ def implicit_rubanok_class
334
+ "#{controller_path.classify.pluralize}Scoper".safe_constantize
335
+ end
336
+ end
337
+ ```
338
+
339
+ Now you can use it like this:
340
+
341
+ ```ruby
342
+ class CourseSessionsController < ApplicationController
343
+ def index
344
+ @sessions = rubanok_process(CourseSession.all, params)
345
+ # which equals to
346
+ @sessions = CourseSessionsScoper.call(CourseSession.all, params.to_unsafe_h)
347
+ end
348
+ end
349
+ ```
350
+
351
+ **NOTE:** the `planish` method is still available and it uses `#{controller_path.classify.pluralize}Plane".safe_constantize` under the hood (via the `#implicit_plane_class` method).
352
+
353
+ ## Using with RBS/Steep
354
+
355
+ _Read ["Climbing Steep hills, or adopting Ruby 3 types with RBS"](https://evilmartians.com/chronicles/climbing-steep-hills-or-adopting-ruby-types) for the context._
356
+
357
+ Rubanok comes with Ruby type signatures (RBS).
358
+
359
+ To use them with Steep, add `library "rubanok"` to your Steepfile.
360
+
361
+ Since Rubanok provides DSL with implicit context switching (via `instance_eval`), you need to provide type hints for the type checker to help it
362
+ figure out the current context. Here is an example:
363
+
364
+ ```ruby
365
+ class MyProcessor < Rubanok::Processor
366
+ map :q do |q:|
367
+ # @type self : Rubanok::Processor
368
+ raw
369
+ end
370
+
371
+ match :sort_by, :sort, activate_on: :sort_by do
372
+ # @type self : Rubanok::DSL::Matching::Rule
373
+ having "status", "asc" do
374
+ # @type self : Rubanok::Processor
375
+ raw
376
+ end
377
+
378
+ # @type self : Rubanok::DSL::Matching::Rule
379
+ default do |sort_by:, sort: "asc"|
380
+ # @type self : Rubanok::Processor
381
+ raw
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ Yeah, a lot of annotations ๐Ÿ˜ž Welcome to the type-safe world!
388
+
389
+ ## Questions & Answers
390
+
391
+ - **Where to put my processor/plane classes?**
392
+
393
+ I put mine under `app/planes` (as `<resources>_plane.rb`) in my Rails app.
394
+
395
+ - **I don't like the naming ("planes" โœˆ๏ธ?), can I still use the library?**
396
+
397
+ Good newsโ€”the default naming [has been changed](https://github.com/palkan/rubanok/pull/8). "Planes" are still available if you prefer them (just like me ๐Ÿ˜‰).
398
+
201
399
  ## Contributing
202
400
 
203
401
  Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/rubanok.