rubanok 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -0
- data/.travis.yml +2 -2
- data/CHANGELOG.md +32 -0
- data/README.md +78 -36
- data/gemfiles/rails42.gemfile +1 -2
- data/lib/rubanok.rb +7 -4
- data/lib/rubanok/dsl/mapping.rb +11 -5
- data/lib/rubanok/dsl/matching.rb +31 -12
- data/lib/rubanok/{plane.rb → processor.rb} +33 -10
- data/lib/rubanok/rails/controller.rb +57 -14
- data/lib/rubanok/rspec.rb +25 -11
- data/lib/rubanok/version.rb +1 -1
- data/rubanok.gemspec +9 -1
- metadata +12 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 657a457fbefb6bf0f596ec5d0933dce51cbf74ca2a6cfed72f9c439d02e3d727
|
4
|
+
data.tar.gz: a820673bee22722ad9291332e2cbcef9aa0088a07c0077cc24d99777eca73f3b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: af7d742f04ba7399d40e92e66a5e26dda0dfa487254f347ee450020fe308e9f1aab906a69c1ba04468825619f3e49acb3ecca61ce22615ce1aab07c150dbe4ee
|
7
|
+
data.tar.gz: 75fb744a42d0248cc6b4b83da6758fca149fc1212fa4fc434a02ece81ce4bd046653192de7209ef0660f4c3302fb24301b07c418c82f1b6e6d0457000e820428
|
data/.rubocop.yml
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,37 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.2.0 (2019-08-23)
|
6
|
+
|
7
|
+
- Add `Process.project` and `rubanok_scope` methods to get the Hash of recognized params. ([@palkan][])
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
class PostsProcessor < Rubanok::Processor
|
11
|
+
map :q { ... }
|
12
|
+
match :page, :per_page, activate_on: :page { ... }
|
13
|
+
end
|
14
|
+
|
15
|
+
PostsProcessor.project(q: "search_me", filter: "smth", page: 2)
|
16
|
+
# => { q: "search_me", page: 2 }
|
17
|
+
|
18
|
+
class PostsController < ApplicationController
|
19
|
+
def index
|
20
|
+
@filter_params = rubanok_scope
|
21
|
+
# or
|
22
|
+
@filter_params = rubanok_scope params.require(:filter), with: PostsProcessor
|
23
|
+
# ...
|
24
|
+
end
|
25
|
+
end
|
26
|
+
```
|
27
|
+
|
28
|
+
- Improve naming by using "processor" instead of "plane". ([@palkan][])
|
29
|
+
|
30
|
+
See [the discussion](https://github.com/palkan/rubanok/issues/3).
|
31
|
+
|
32
|
+
**NOTE**: Older API is still available without deprecation.
|
33
|
+
|
34
|
+
- Add `fail_when_no_matches` parameter to `match` method. ([@Earendil95][])
|
35
|
+
|
5
36
|
## 0.1.3 (2019-03-05)
|
6
37
|
|
7
38
|
- Fix using `activate_always: true` with `default` matching clause. ([@palkan][])
|
@@ -19,3 +50,4 @@ Initial implementation.
|
|
19
50
|
Proposal added.
|
20
51
|
|
21
52
|
[@palkan]: https://github.com/palkan
|
53
|
+
[@Earendil95]: https://github.com/Earendil95
|
data/README.md
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
|
5
5
|
Rubanok provides a DSL to build parameters-based data transformers.
|
6
6
|
|
7
|
-
📖 Read the introduction post: ["Carve your controllers like Papa Carlo"](https://
|
7
|
+
📖 Read the introduction post: ["Carve your controllers like Papa Carlo"](https://evilmartians.com/chronicles/rubanok-carve-your-rails-controllers-like-papa-carlo)
|
8
8
|
|
9
9
|
The typical usage is to describe all the possible collection manipulation for REST `index` action, e.g. filtering, sorting, searching, pagination, etc..
|
10
10
|
|
@@ -28,13 +28,13 @@ You have:
|
|
28
28
|
```ruby
|
29
29
|
class CourseSessionController < ApplicationController
|
30
30
|
def index
|
31
|
-
@sessions =
|
31
|
+
@sessions = rubanok_process(
|
32
32
|
# pass input
|
33
33
|
CourseSession.all,
|
34
34
|
# pass params
|
35
35
|
params,
|
36
|
-
# provide a
|
37
|
-
with:
|
36
|
+
# provide a processor to use
|
37
|
+
with: CourseSessionsProcessor
|
38
38
|
)
|
39
39
|
end
|
40
40
|
end
|
@@ -42,16 +42,16 @@ end
|
|
42
42
|
|
43
43
|
Or we can try to infer all the configuration for you:
|
44
44
|
|
45
|
-
|
46
45
|
```ruby
|
47
46
|
class CourseSessionController < ApplicationController
|
48
47
|
def index
|
49
|
-
@sessions =
|
48
|
+
@sessions = rubanok_process(CourseSession.all)
|
50
49
|
end
|
51
50
|
end
|
52
51
|
```
|
53
52
|
|
54
53
|
Requirements:
|
54
|
+
|
55
55
|
- Ruby ~> 2.5
|
56
56
|
- Rails >= 4.2 (only for using with Rails)
|
57
57
|
|
@@ -70,12 +70,12 @@ And run `bundle install`.
|
|
70
70
|
|
71
71
|
## Usage
|
72
72
|
|
73
|
-
The core concept of this library is a _plane_
|
73
|
+
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.
|
74
74
|
|
75
75
|
From the example above:
|
76
76
|
|
77
77
|
```ruby
|
78
|
-
class
|
78
|
+
class CourseSessionsProcessor < Rubanok::Processor
|
79
79
|
# You can map keys
|
80
80
|
map :q do |q:|
|
81
81
|
# `raw` is an accessor for input data
|
@@ -84,7 +84,7 @@ class CourseSessionsPlane < Rubanok::Plane
|
|
84
84
|
end
|
85
85
|
|
86
86
|
# The following code
|
87
|
-
|
87
|
+
CourseSessionsProcessor.call(CourseSession.all, q: "xyz")
|
88
88
|
|
89
89
|
# is equal to
|
90
90
|
CourseSession.all.search("xyz")
|
@@ -93,7 +93,7 @@ CourseSession.all.search("xyz")
|
|
93
93
|
You can map multiple keys at once:
|
94
94
|
|
95
95
|
```ruby
|
96
|
-
class
|
96
|
+
class CourseSessionsProcessor < Rubanok::Processor
|
97
97
|
DEFAULT_PAGE_SIZE = 25
|
98
98
|
|
99
99
|
map :page, :per_page do |page:, per_page: DEFAULT_PAGE_SIZE|
|
@@ -105,7 +105,7 @@ end
|
|
105
105
|
There is also `match` method to handle values:
|
106
106
|
|
107
107
|
```ruby
|
108
|
-
class
|
108
|
+
class CourseSessionsProcessor < Rubanok::Processor
|
109
109
|
SORT_ORDERS = %w(asc desc).freeze
|
110
110
|
SORTABLE_FIELDS = %w(id name created_at).freeze
|
111
111
|
|
@@ -132,11 +132,54 @@ class CourseSessionsPlane < Rubanok::Plane
|
|
132
132
|
raw.order(sort_by => sort)
|
133
133
|
end
|
134
134
|
end
|
135
|
+
|
136
|
+
# strict matching; if Processor will not match parameter, it will raise Rubanok::UnexpectedInputError
|
137
|
+
# You can handle it in controller, for example, with sending 422 Unprocessable Entity to client
|
138
|
+
match :filter, fail_when_no_matches: true do
|
139
|
+
having "active" do
|
140
|
+
raw.active
|
141
|
+
end
|
142
|
+
|
143
|
+
having "finished" do
|
144
|
+
raw.finished
|
145
|
+
end
|
146
|
+
end
|
135
147
|
end
|
136
148
|
```
|
137
149
|
|
150
|
+
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`.
|
151
|
+
If in example above you will call `CourseSessionsProcessor.call(CourseSession, filter: 'acitve')`, you will get `Rubanok::UnexpectedInputError: Unexpected input: {:filter=>'acitve'}`.
|
152
|
+
|
138
153
|
**NOTE:** Rubanok only matches exact values; more complex matching could be added in the future.
|
139
154
|
|
155
|
+
### Getting the matching params
|
156
|
+
|
157
|
+
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).
|
158
|
+
|
159
|
+
In Rails, you can use the `#rubanok_scope` method for that:
|
160
|
+
|
161
|
+
```ruby
|
162
|
+
class CourseSessionController < ApplicationController
|
163
|
+
def index
|
164
|
+
@sessions = rubanok_process(CourseSession.all)
|
165
|
+
# Returns the Hash of params recognized by the CourseSessionProcessor.
|
166
|
+
# For example:
|
167
|
+
#
|
168
|
+
# params == {q: "search", role_id: 2, date: "2019-08-22"}
|
169
|
+
# @session_filter == {q: "search", role_id: 2}
|
170
|
+
@sessions_filter = rubanok_scope(
|
171
|
+
params.permit(:q, :role_id),
|
172
|
+
with: CourseSessionProcessor
|
173
|
+
)
|
174
|
+
|
175
|
+
# You can omit all the arguments
|
176
|
+
@sessions_filter = rubanok_scope #=> equals to rubanok_scope(params, with: implicit_rubanok_class)
|
177
|
+
end
|
178
|
+
end
|
179
|
+
```
|
180
|
+
|
181
|
+
You can also accesss `rubanok_scope` in views (it's a helper method).
|
182
|
+
|
140
183
|
### Rule activation
|
141
184
|
|
142
185
|
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.
|
@@ -165,7 +208,7 @@ One of the benefits of having modification logic contained in its own class is t
|
|
165
208
|
|
166
209
|
```ruby
|
167
210
|
# For example, with RSpec
|
168
|
-
describe
|
211
|
+
RSpec.describe CourseSessionsProcessor do
|
169
212
|
let(:input ) { CourseSession.all }
|
170
213
|
let(:params) { {} }
|
171
214
|
|
@@ -182,19 +225,19 @@ end
|
|
182
225
|
Now in your controller you only have to test that the specific _plane_ is applied:
|
183
226
|
|
184
227
|
```ruby
|
185
|
-
describe CourseSessionController do
|
228
|
+
RSpec.describe CourseSessionController do
|
186
229
|
subject { get :index }
|
187
230
|
|
188
231
|
specify do
|
189
|
-
expect { subject }.to
|
190
|
-
with(
|
232
|
+
expect { subject }.to have_rubanok_processed(CourseSession.all).
|
233
|
+
with(CourseSessionsProcessor)
|
191
234
|
end
|
192
235
|
end
|
193
236
|
```
|
194
237
|
|
195
238
|
**NOTE**: input matching only checks for the class equality.
|
196
239
|
|
197
|
-
To use `
|
240
|
+
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"`):
|
198
241
|
|
199
242
|
```ruby
|
200
243
|
require "rubanok/rspec"
|
@@ -202,33 +245,20 @@ require "rubanok/rspec"
|
|
202
245
|
|
203
246
|
### Rails vs. non-Rails
|
204
247
|
|
205
|
-
Rubanok does not require Rails, but it has some useful Rails extensions such as `
|
248
|
+
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`).
|
206
249
|
|
207
250
|
If you use `ActionController::Metal` you must include the `Rubanok::Controller` module yourself.
|
208
251
|
|
209
|
-
|
210
|
-
|
211
|
-
- **🧐"Planish"? Is there a word?**
|
212
|
-
|
213
|
-
Yes, [it is](https://en.wiktionary.org/wiki/planish).
|
214
|
-
|
215
|
-
- **Where to put my _plane_ classes?**
|
252
|
+
### Processor class inference in Rails controllers
|
216
253
|
|
217
|
-
|
218
|
-
|
219
|
-
- **I don't like the naming ("planes" ✈️?), can I still use the library?**
|
254
|
+
By default, `rubanok_process` uses the following algorithm to define a processor class: `"#{controller_path.classify.pluralize}Processor".safe_constantize`.
|
220
255
|
|
221
|
-
|
222
|
-
|
223
|
-
Secondly, you can easily avoid it by adding a few lines to your `ApplicationController`:
|
256
|
+
You can change this by overriding the `#implicit_rubanok_class` method:
|
224
257
|
|
225
258
|
```ruby
|
226
259
|
class ApplicationController < ActionController::Smth
|
227
|
-
#
|
228
|
-
|
229
|
-
|
230
|
-
# override the `implicit_plane_class` method
|
231
|
-
def implicit_plane_class
|
260
|
+
# override the `implicit_rubanok_class` method
|
261
|
+
def implicit_rubanok_class
|
232
262
|
"#{controller_path.classify.pluralize}Scoper".safe_constantize
|
233
263
|
end
|
234
264
|
end
|
@@ -239,13 +269,25 @@ Now you can use it like this:
|
|
239
269
|
```ruby
|
240
270
|
class CourseSessionsController < ApplicationController
|
241
271
|
def index
|
242
|
-
@sessions =
|
272
|
+
@sessions = rubanok_process(CourseSession.all, params)
|
243
273
|
# which equals to
|
244
274
|
@sessions = CourseSessionsScoper.call(CourseSession.all, params.to_unsafe_h)
|
245
275
|
end
|
246
276
|
end
|
247
277
|
```
|
248
278
|
|
279
|
+
**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).
|
280
|
+
|
281
|
+
## Questions & Answers
|
282
|
+
|
283
|
+
- **Where to put my processor/plane classes?**
|
284
|
+
|
285
|
+
I put mine under `app/planes` (as `<resources>_plane.rb`) in my Rails app.
|
286
|
+
|
287
|
+
- **I don't like the naming ("planes" ✈️?), can I still use the library?**
|
288
|
+
|
289
|
+
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 😉).
|
290
|
+
|
249
291
|
## Contributing
|
250
292
|
|
251
293
|
Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/rubanok.
|
data/gemfiles/rails42.gemfile
CHANGED
data/lib/rubanok.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "rubanok/version"
|
4
|
-
require "rubanok/
|
4
|
+
require "rubanok/processor"
|
5
5
|
|
6
6
|
require "rubanok/railtie" if defined?(Rails)
|
7
7
|
|
@@ -13,15 +13,15 @@ end
|
|
13
13
|
#
|
14
14
|
# Example:
|
15
15
|
#
|
16
|
-
# class
|
16
|
+
# class CourseSessionProcessor < Rubanok::Processor
|
17
17
|
# map :q do |q:|
|
18
|
-
#
|
18
|
+
# raw.searh(q)
|
19
19
|
# end
|
20
20
|
# end
|
21
21
|
#
|
22
22
|
# class CourseSessionController < ApplicationController
|
23
23
|
# def index
|
24
|
-
# @sessions =
|
24
|
+
# @sessions = rubanok_process(CourseSession.all)
|
25
25
|
# end
|
26
26
|
# end
|
27
27
|
module Rubanok
|
@@ -30,7 +30,10 @@ module Rubanok
|
|
30
30
|
# When the value is empty and ignored the corresponding matcher/mapper
|
31
31
|
# is not activated (true by default)
|
32
32
|
attr_accessor :ignore_empty_values
|
33
|
+
# Define wheter to fail when `match` rule cannot find matching value
|
34
|
+
attr_accessor :fail_when_no_matches
|
33
35
|
end
|
34
36
|
|
35
37
|
self.ignore_empty_values = true
|
38
|
+
self.fail_when_no_matches = false
|
36
39
|
end
|
data/lib/rubanok/dsl/mapping.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
module Rubanok
|
4
4
|
module DSL
|
5
|
-
# Adds `.map` method to
|
5
|
+
# Adds `.map` method to Processor to define key-matching rules:
|
6
6
|
#
|
7
7
|
# map :q do |q:|
|
8
8
|
# # this rule is activated iff "q" (or :q) param is present
|
@@ -20,12 +20,18 @@ module Rubanok
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
module ClassMethods
|
24
|
+
def map(*fields, **options, &block)
|
25
|
+
rule = Rule.new(fields, options)
|
25
26
|
|
26
|
-
|
27
|
+
define_method(rule.to_method_name, &block)
|
27
28
|
|
28
|
-
|
29
|
+
add_rule rule
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.included(base)
|
34
|
+
base.extend ClassMethods
|
29
35
|
end
|
30
36
|
end
|
31
37
|
end
|
data/lib/rubanok/dsl/matching.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Rubanok
|
4
|
+
class UnexpectedInputError < StandardError; end
|
5
|
+
|
4
6
|
module DSL
|
5
|
-
# Adds `.match` method to
|
7
|
+
# Adds `.match` method to Processor class to define key-value-matching rules:
|
6
8
|
#
|
7
9
|
# match :sort, :sort_by do |sort:, sort_by:|
|
8
10
|
# # this rule is activated iff both "sort" and "sort_by" params are present
|
@@ -64,23 +66,40 @@ module Rubanok
|
|
64
66
|
end
|
65
67
|
end
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
+
module ClassMethods
|
70
|
+
def match(*fields, **options, &block)
|
71
|
+
rule = Rule.new(fields, options.slice(:activate_on, :activate_always))
|
69
72
|
|
70
|
-
|
73
|
+
rule.instance_eval(&block)
|
71
74
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
+
define_method(rule.to_method_name) do |params = {}|
|
76
|
+
clause = rule.matching_clause(params)
|
77
|
+
next default_match_handler(rule, params, options[:fail_when_no_matches]) unless clause
|
75
78
|
|
76
|
-
|
77
|
-
|
79
|
+
apply_rule! clause.to_method_name, clause.project(params)
|
80
|
+
end
|
81
|
+
|
82
|
+
rule.clauses.each do |clause|
|
83
|
+
define_method(clause.to_method_name, &clause.block)
|
84
|
+
end
|
78
85
|
|
79
|
-
|
80
|
-
define_method(clause.to_method_name, &clause.block)
|
86
|
+
add_rule rule
|
81
87
|
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.included(base)
|
91
|
+
base.extend ClassMethods
|
92
|
+
end
|
93
|
+
|
94
|
+
def default_match_handler(rule, params, fail_when_no_matches)
|
95
|
+
fail_when_no_matches = Rubanok.fail_when_no_matches if fail_when_no_matches.nil?
|
96
|
+
return raw unless fail_when_no_matches
|
82
97
|
|
83
|
-
|
98
|
+
raise ::Rubanok::UnexpectedInputError, <<~MSG
|
99
|
+
Unexpected input: #{params.slice(*rule.fields)}.
|
100
|
+
Available values are:
|
101
|
+
#{rule.clauses.map(&:values).join("\n ")}
|
102
|
+
MSG
|
84
103
|
end
|
85
104
|
end
|
86
105
|
end
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "set"
|
4
|
+
|
3
5
|
require "rubanok/rule"
|
4
6
|
|
5
7
|
require "rubanok/dsl/mapping"
|
@@ -9,36 +11,37 @@ require "rubanok/ext/symbolize_keys"
|
|
9
11
|
using Rubanok::SymbolizeKeys
|
10
12
|
|
11
13
|
module Rubanok
|
12
|
-
# Base class for
|
14
|
+
# Base class for processors (_planes_)
|
13
15
|
#
|
14
16
|
# Define transformation rules via `map` and `match` methods
|
15
|
-
# and apply them by calling the
|
17
|
+
# and apply them by calling the processor:
|
16
18
|
#
|
17
|
-
# class
|
19
|
+
# class MyTransformer < Rubanok::Processor
|
18
20
|
# map :type do
|
19
21
|
# raw.where(type: type)
|
20
22
|
# end
|
21
23
|
# end
|
22
24
|
#
|
23
|
-
#
|
25
|
+
# MyTransformer.call(MyModel.all, {type: "public"})
|
24
26
|
#
|
25
27
|
# NOTE: the second argument (`params`) MUST be a Hash. Keys could be either Symbols
|
26
28
|
# or Strings (we automatically transform strings to symbols while matching rules).
|
27
29
|
#
|
28
30
|
# All transformation methods are called within the context of the instance of
|
29
|
-
# a
|
31
|
+
# a processor class.
|
30
32
|
#
|
31
33
|
# You can access the input data via `raw` method.
|
32
|
-
class
|
33
|
-
|
34
|
-
|
35
|
-
include DSL::Matching
|
34
|
+
class Processor
|
35
|
+
include DSL::Matching
|
36
|
+
include DSL::Mapping
|
36
37
|
|
38
|
+
class << self
|
37
39
|
def call(input, params)
|
38
40
|
new(input).call(params)
|
39
41
|
end
|
40
42
|
|
41
43
|
def add_rule(rule)
|
44
|
+
fields_set.merge rule.fields
|
42
45
|
rules << rule
|
43
46
|
end
|
44
47
|
|
@@ -46,12 +49,30 @@ module Rubanok
|
|
46
49
|
return @rules if instance_variable_defined?(:@rules)
|
47
50
|
|
48
51
|
@rules =
|
49
|
-
if superclass <=
|
52
|
+
if superclass <= Processor
|
50
53
|
superclass.rules.dup
|
51
54
|
else
|
52
55
|
[]
|
53
56
|
end
|
54
57
|
end
|
58
|
+
|
59
|
+
def fields_set
|
60
|
+
return @fields_set if instance_variable_defined?(:@fields_set)
|
61
|
+
|
62
|
+
@fields_set =
|
63
|
+
if superclass <= Processor
|
64
|
+
superclass.fields_set.dup
|
65
|
+
else
|
66
|
+
Set.new
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Generates a `params` projection including only the keys used
|
71
|
+
# by the rules
|
72
|
+
def project(params)
|
73
|
+
params = params.symbolize_keys
|
74
|
+
params.slice(*fields_set)
|
75
|
+
end
|
55
76
|
end
|
56
77
|
|
57
78
|
def initialize(input)
|
@@ -89,4 +110,6 @@ module Rubanok
|
|
89
110
|
self.class.rules
|
90
111
|
end
|
91
112
|
end
|
113
|
+
|
114
|
+
Plane = Processor
|
92
115
|
end
|
@@ -4,33 +4,76 @@ require "active_support/concern"
|
|
4
4
|
|
5
5
|
module Rubanok
|
6
6
|
# Controller concern.
|
7
|
-
# Adds `
|
7
|
+
# Adds `rubanok_process` method.
|
8
8
|
module Controller
|
9
9
|
extend ActiveSupport::Concern
|
10
10
|
|
11
|
-
|
12
|
-
|
11
|
+
included do
|
12
|
+
helper_method :rubanok_scope
|
13
|
+
helper_method :planish_scope
|
14
|
+
end
|
15
|
+
|
16
|
+
# This method process passed data (e.g. ActiveRecord relation) using
|
17
|
+
# the corresponding Processor class.
|
18
|
+
#
|
19
|
+
# Processor is inferred from controller name, e.g.
|
20
|
+
# "PostsController -> PostProcessor".
|
21
|
+
#
|
22
|
+
# You can specify the Processor class explicitly via `with` option.
|
23
|
+
#
|
24
|
+
# By default, `params` object is passed as parameters, but you
|
25
|
+
# can specify the params via `params` option.
|
26
|
+
def rubanok_process(data, params = nil, with: nil)
|
27
|
+
with_inferred_rubanok_params(with, params) do |rubanok_class, rubanok_params|
|
28
|
+
rubanok_class.call(data, rubanok_params)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# This method filters the passed params and returns the Hash (!)
|
33
|
+
# of the params recongnized by the processor.
|
13
34
|
#
|
14
|
-
#
|
15
|
-
# "PostsController ->
|
35
|
+
# Processor is inferred from controller name, e.g.
|
36
|
+
# "PostsController -> PostProcessor".
|
16
37
|
#
|
17
|
-
# You can specify the
|
38
|
+
# You can specify the Processor class explicitly via `with` option.
|
18
39
|
#
|
19
|
-
# By default, `params` object is passed as
|
40
|
+
# By default, `params` object is passed as parameters, but you
|
20
41
|
# can specify the params via `params` option.
|
21
|
-
def
|
42
|
+
def rubanok_scope(params = nil, with: nil)
|
43
|
+
with_inferred_rubanok_params(with, params) do |rubanok_class, rubanok_params|
|
44
|
+
rubanok_class.project(rubanok_params)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Tries to infer the rubanok processor class from controller path
|
49
|
+
def implicit_rubanok_class
|
50
|
+
"#{controller_path.classify.pluralize}Processor".safe_constantize
|
51
|
+
end
|
52
|
+
|
53
|
+
def with_inferred_rubanok_params(with, params)
|
54
|
+
with ||= implicit_rubanok_class
|
55
|
+
|
22
56
|
if with.nil?
|
23
|
-
raise ArgumentError, "Failed to find a
|
24
|
-
"Please, specify the
|
57
|
+
raise ArgumentError, "Failed to find a processor class for #{self.class.name}. " \
|
58
|
+
"Please, specify the processor class explicitly via `with` option"
|
25
59
|
end
|
26
60
|
|
27
|
-
|
61
|
+
params ||= self.params
|
62
|
+
|
63
|
+
params = params.to_unsafe_h if params.is_a?(ActionController::Parameters)
|
64
|
+
|
65
|
+
yield with, params
|
66
|
+
end
|
67
|
+
|
68
|
+
# Backward compatibility
|
69
|
+
def planish(*args, with: implicit_plane_class)
|
70
|
+
rubanok_process(*args, with: with)
|
71
|
+
end
|
28
72
|
|
29
|
-
|
30
|
-
|
73
|
+
def planish_scope(*args, with: implicit_plane_class)
|
74
|
+
rubanok_scope(*args, with: with)
|
31
75
|
end
|
32
76
|
|
33
|
-
# Tries to infer the plane class from controller path
|
34
77
|
def implicit_plane_class
|
35
78
|
"#{controller_path.classify.pluralize}Plane".safe_constantize
|
36
79
|
end
|
data/lib/rubanok/rspec.rb
CHANGED
@@ -3,20 +3,26 @@
|
|
3
3
|
require "rspec/mocks"
|
4
4
|
|
5
5
|
module Rubanok
|
6
|
-
class
|
6
|
+
class HaveProcessed < RSpec::Matchers::BuiltIn::BaseMatcher
|
7
7
|
include RSpec::Mocks::ExampleMethods
|
8
8
|
|
9
|
-
attr_reader :data_class, :
|
9
|
+
attr_reader :data_class, :processor, :matcher
|
10
10
|
|
11
11
|
def initialize(data_class = nil)
|
12
12
|
if data_class
|
13
13
|
@data_class = data_class.is_a?(Module) ? data_class : data_class.class
|
14
14
|
end
|
15
15
|
@matcher = have_received(:call)
|
16
|
+
@name = "have_rubanok_processed"
|
16
17
|
end
|
17
18
|
|
18
|
-
def
|
19
|
-
@
|
19
|
+
def as(alias_name)
|
20
|
+
@name = alias_name
|
21
|
+
self
|
22
|
+
end
|
23
|
+
|
24
|
+
def with(processor)
|
25
|
+
@processor = processor
|
20
26
|
self
|
21
27
|
end
|
22
28
|
|
@@ -25,32 +31,40 @@ module Rubanok
|
|
25
31
|
end
|
26
32
|
|
27
33
|
def matches?(proc)
|
28
|
-
raise ArgumentError, "
|
34
|
+
raise ArgumentError, "#{name} only supports block expectations" unless Proc === proc
|
29
35
|
|
30
|
-
raise ArgumentError, "
|
36
|
+
raise ArgumentError, "Processor class is required. Please, specify it using `.with` modifier" if processor.nil?
|
31
37
|
|
32
|
-
allow(
|
38
|
+
allow(processor).to receive(:call).and_call_original
|
33
39
|
proc.call
|
34
40
|
|
35
41
|
matcher.with(an_instance_of(data_class), anything) if data_class
|
36
42
|
|
37
|
-
matcher.matches?(
|
43
|
+
matcher.matches?(processor)
|
38
44
|
end
|
39
45
|
|
40
46
|
def failure_message
|
41
|
-
"expected to use #{
|
47
|
+
"expected to use #{processor.name}#{data_class ? " for #{data_class.name}" : ""}, but didn't"
|
42
48
|
end
|
43
49
|
|
44
50
|
def failure_message_when_negated
|
45
|
-
"expected not to use #{
|
51
|
+
"expected not to use #{processor.name}#{data_class ? " for #{data_class.name} " : ""}, but have used"
|
46
52
|
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
attr_reader :name
|
47
57
|
end
|
48
58
|
end
|
49
59
|
|
50
60
|
RSpec.configure do |config|
|
51
61
|
config.include(Module.new do
|
62
|
+
def have_rubanok_processed(*args)
|
63
|
+
Rubanok::HaveProcessed.new(*args)
|
64
|
+
end
|
65
|
+
|
52
66
|
def have_planished(*args)
|
53
|
-
Rubanok::
|
67
|
+
Rubanok::HaveProcessed.new(*args).as("have_planished")
|
54
68
|
end
|
55
69
|
end)
|
56
70
|
end
|
data/lib/rubanok/version.rb
CHANGED
data/rubanok.gemspec
CHANGED
@@ -22,6 +22,14 @@ Gem::Specification.new do |spec|
|
|
22
22
|
`git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
23
23
|
end
|
24
24
|
|
25
|
+
spec.metadata = {
|
26
|
+
"bug_tracker_uri" => "http://github.com/palkan/rubanok/issues",
|
27
|
+
"changelog_uri" => "https://github.com/palkan/rubanok/blob/master/CHANGELOG.md",
|
28
|
+
"documentation_uri" => "http://github.com/palkan/rubanok",
|
29
|
+
"homepage_uri" => "http://github.com/palkan/rubanok",
|
30
|
+
"source_code_uri" => "http://github.com/palkan/rubanok"
|
31
|
+
}
|
32
|
+
|
25
33
|
spec.require_paths = ["lib"]
|
26
34
|
|
27
35
|
spec.add_development_dependency "actionpack", ">= 4.2"
|
@@ -31,5 +39,5 @@ Gem::Specification.new do |spec|
|
|
31
39
|
spec.add_development_dependency "rspec", "~> 3.0"
|
32
40
|
spec.add_development_dependency "rspec-rails"
|
33
41
|
spec.add_development_dependency "rubocop-rspec"
|
34
|
-
spec.add_development_dependency "standard", "~> 0.0.
|
42
|
+
spec.add_development_dependency "standard", "~> 0.0.39"
|
35
43
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubanok
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2019-
|
11
|
+
date: 2019-08-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -114,14 +114,14 @@ dependencies:
|
|
114
114
|
requirements:
|
115
115
|
- - "~>"
|
116
116
|
- !ruby/object:Gem::Version
|
117
|
-
version: 0.0.
|
117
|
+
version: 0.0.39
|
118
118
|
type: :development
|
119
119
|
prerelease: false
|
120
120
|
version_requirements: !ruby/object:Gem::Requirement
|
121
121
|
requirements:
|
122
122
|
- - "~>"
|
123
123
|
- !ruby/object:Gem::Version
|
124
|
-
version: 0.0.
|
124
|
+
version: 0.0.39
|
125
125
|
description: Parameters-based transformation DSL
|
126
126
|
email:
|
127
127
|
- dementiev.vm@gmail.com
|
@@ -149,7 +149,7 @@ files:
|
|
149
149
|
- lib/rubanok/dsl/mapping.rb
|
150
150
|
- lib/rubanok/dsl/matching.rb
|
151
151
|
- lib/rubanok/ext/symbolize_keys.rb
|
152
|
-
- lib/rubanok/
|
152
|
+
- lib/rubanok/processor.rb
|
153
153
|
- lib/rubanok/rails/controller.rb
|
154
154
|
- lib/rubanok/railtie.rb
|
155
155
|
- lib/rubanok/rspec.rb
|
@@ -159,7 +159,12 @@ files:
|
|
159
159
|
homepage: https://github.com/palkan/rubanok
|
160
160
|
licenses:
|
161
161
|
- MIT
|
162
|
-
metadata:
|
162
|
+
metadata:
|
163
|
+
bug_tracker_uri: http://github.com/palkan/rubanok/issues
|
164
|
+
changelog_uri: https://github.com/palkan/rubanok/blob/master/CHANGELOG.md
|
165
|
+
documentation_uri: http://github.com/palkan/rubanok
|
166
|
+
homepage_uri: http://github.com/palkan/rubanok
|
167
|
+
source_code_uri: http://github.com/palkan/rubanok
|
163
168
|
post_install_message:
|
164
169
|
rdoc_options: []
|
165
170
|
require_paths:
|
@@ -175,7 +180,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
175
180
|
- !ruby/object:Gem::Version
|
176
181
|
version: '0'
|
177
182
|
requirements: []
|
178
|
-
rubygems_version: 3.0.
|
183
|
+
rubygems_version: 3.0.4
|
179
184
|
signing_key:
|
180
185
|
specification_version: 4
|
181
186
|
summary: Parameters-based transformation DSL
|