rubanok 0.3.0 → 0.4.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/CHANGELOG.md +6 -0
- data/README.md +36 -0
- data/lib/rubanok.rb +1 -0
- data/lib/rubanok/dsl/mapping.rb +4 -4
- data/lib/rubanok/dsl/matching.rb +13 -8
- data/lib/rubanok/processor.rb +9 -8
- data/lib/rubanok/rule.rb +20 -21
- data/lib/rubanok/version.rb +1 -1
- data/sig/rubanok.rbs +6 -0
- data/sig/rubanok/dsl/mapping.rbs +18 -0
- data/sig/rubanok/dsl/matching.rbs +44 -0
- data/sig/rubanok/processor.rbs +53 -0
- data/sig/rubanok/rule.rbs +29 -0
- data/sig/rubanok/version.rbs +3 -0
- data/sig/typeprof.rb +25 -0
- metadata +9 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 90f6ca9dfd61a6f143eff1f3ab3c2d7e41502c992879af785dcf414af15249fd
|
4
|
+
data.tar.gz: 8807cbb2a680e9fbc739a9f0214fb4753e24ee55e69da72d9b45c7f121900dd9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2e015d7c3c517e42e5fe42037035db950cc87f9bd0099b38580c03ad156c36d0133681bd659451e8bdb58f15254799ce8d7b3158a1dc998210e3a425867a521d
|
7
|
+
data.tar.gz: 6c22e9a876e86f144726e8dbbe79b42f9d3052feb6f9c31688f7259872a5eed1e5ac66b031b890e0d1b4eceba7d3903331a073f2e13321b9f3d45c9fe53229d2
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -350,6 +350,42 @@ end
|
|
350
350
|
|
351
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
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
|
+
|
353
389
|
## Questions & Answers
|
354
390
|
|
355
391
|
- **Where to put my processor/plane classes?**
|
data/lib/rubanok.rb
CHANGED
data/lib/rubanok/dsl/mapping.rb
CHANGED
@@ -21,8 +21,8 @@ module Rubanok
|
|
21
21
|
end
|
22
22
|
|
23
23
|
module ClassMethods
|
24
|
-
def map(*fields,
|
25
|
-
filter =
|
24
|
+
def map(*fields, activate_on: fields, activate_always: false, ignore_empty_values: Rubanok.ignore_empty_values, filter_with: nil, &block)
|
25
|
+
filter = filter_with
|
26
26
|
|
27
27
|
if filter.is_a?(Symbol)
|
28
28
|
respond_to?(filter) || raise(
|
@@ -30,10 +30,10 @@ module Rubanok
|
|
30
30
|
"Unknown class method #{filter} for #{self}. " \
|
31
31
|
"Make sure that a filter method is defined before the call to .map."
|
32
32
|
)
|
33
|
-
|
33
|
+
filter = method(filter)
|
34
34
|
end
|
35
35
|
|
36
|
-
rule = Rule.new(fields,
|
36
|
+
rule = Rule.new(fields, activate_on: activate_on, activate_always: activate_always, ignore_empty_values: ignore_empty_values, filter_with: filter)
|
37
37
|
|
38
38
|
define_method(rule.to_method_name, &block)
|
39
39
|
|
data/lib/rubanok/dsl/matching.rb
CHANGED
@@ -22,8 +22,8 @@ module Rubanok
|
|
22
22
|
class Clause < Rubanok::Rule
|
23
23
|
attr_reader :values, :id, :block
|
24
24
|
|
25
|
-
def initialize(id, fields, values
|
26
|
-
super(fields,
|
25
|
+
def initialize(id, fields, values, activate_on: fields, activate_always: false, &block)
|
26
|
+
super(fields, activate_on: activate_on, activate_always: activate_always)
|
27
27
|
@id = id
|
28
28
|
@block = block
|
29
29
|
@values = Hash[fields.take(values.size).zip(values)].freeze
|
@@ -39,7 +39,7 @@ module Rubanok
|
|
39
39
|
|
40
40
|
attr_reader :clauses
|
41
41
|
|
42
|
-
def initialize(
|
42
|
+
def initialize(fields, activate_on: fields, activate_always: false, ignore_empty_values: Rubanok.ignore_empty_values, filter_with: nil)
|
43
43
|
super
|
44
44
|
@clauses = []
|
45
45
|
end
|
@@ -55,7 +55,7 @@ module Rubanok
|
|
55
55
|
end
|
56
56
|
|
57
57
|
def default(&block)
|
58
|
-
clauses << Clause.new("#{to_method_name}_default", fields, activate_always: true, &block)
|
58
|
+
clauses << Clause.new("#{to_method_name}_default", fields, [], activate_always: true, &block)
|
59
59
|
end
|
60
60
|
|
61
61
|
private
|
@@ -67,16 +67,21 @@ module Rubanok
|
|
67
67
|
end
|
68
68
|
|
69
69
|
module ClassMethods
|
70
|
-
def match(*fields,
|
71
|
-
rule = Rule.new(fields,
|
70
|
+
def match(*fields, activate_on: fields, activate_always: false, fail_when_no_matches: nil, &block)
|
71
|
+
rule = Rule.new(fields, activate_on: activate_on, activate_always: activate_always)
|
72
72
|
|
73
73
|
rule.instance_eval(&block)
|
74
74
|
|
75
75
|
define_method(rule.to_method_name) do |params = {}|
|
76
|
+
params ||= {} if params.nil?
|
77
|
+
|
76
78
|
clause = rule.matching_clause(params)
|
77
|
-
next default_match_handler(rule, params, options[:fail_when_no_matches]) unless clause
|
78
79
|
|
79
|
-
|
80
|
+
if clause
|
81
|
+
apply_rule! clause, params
|
82
|
+
else
|
83
|
+
default_match_handler(rule, params, fail_when_no_matches)
|
84
|
+
end
|
80
85
|
end
|
81
86
|
|
82
87
|
rule.clauses.each do |clause|
|
data/lib/rubanok/processor.rb
CHANGED
@@ -32,15 +32,15 @@ module Rubanok
|
|
32
32
|
include DSL::Matching
|
33
33
|
include DSL::Mapping
|
34
34
|
|
35
|
+
UNDEFINED = Object.new
|
36
|
+
|
35
37
|
class << self
|
36
|
-
def call(
|
37
|
-
input, params =
|
38
|
-
|
39
|
-
|
40
|
-
else
|
41
|
-
args
|
42
|
-
end
|
38
|
+
def call(input, params = UNDEFINED)
|
39
|
+
input, params = nil, input if params == UNDEFINED
|
40
|
+
|
41
|
+
raise ArgumentError, "Params could not be nil" if params.nil?
|
43
42
|
|
43
|
+
# @type var params: untyped
|
44
44
|
new(input).call(params)
|
45
45
|
end
|
46
46
|
|
@@ -75,7 +75,7 @@ module Rubanok
|
|
75
75
|
# by the rules
|
76
76
|
def project(params)
|
77
77
|
params = params.transform_keys(&:to_sym)
|
78
|
-
params.slice(*fields_set)
|
78
|
+
params.slice(*fields_set.to_a)
|
79
79
|
end
|
80
80
|
|
81
81
|
# DSL to define the #prepare method
|
@@ -86,6 +86,7 @@ module Rubanok
|
|
86
86
|
|
87
87
|
def initialize(input)
|
88
88
|
@input = input
|
89
|
+
@prepared = false
|
89
90
|
end
|
90
91
|
|
91
92
|
def call(params)
|
data/lib/rubanok/rule.rb
CHANGED
@@ -1,15 +1,30 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Rubanok
|
4
|
+
using(Module.new do
|
5
|
+
refine NilClass do
|
6
|
+
def empty?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
refine Object do
|
12
|
+
def empty?
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end)
|
17
|
+
|
4
18
|
class Rule # :nodoc:
|
5
19
|
UNDEFINED = Object.new
|
6
20
|
|
7
|
-
attr_reader :
|
21
|
+
attr_reader :fields, :activate_on, :activate_always, :ignore_empty_values, :filter_with
|
8
22
|
|
9
23
|
def initialize(fields, activate_on: fields, activate_always: false, ignore_empty_values: Rubanok.ignore_empty_values, filter_with: nil)
|
10
|
-
@owner = owner
|
11
24
|
@fields = fields.freeze
|
12
|
-
@activate_on
|
25
|
+
# @type var activate_on: Array[Symbol]
|
26
|
+
activate_on = Array(activate_on)
|
27
|
+
@activate_on = activate_on.freeze
|
13
28
|
@activate_always = activate_always
|
14
29
|
@ignore_empty_values = ignore_empty_values
|
15
30
|
@filter_with = filter_with
|
@@ -18,9 +33,7 @@ module Rubanok
|
|
18
33
|
def project(params)
|
19
34
|
fields.each_with_object({}) do |field, acc|
|
20
35
|
val = fetch_value params, field
|
21
|
-
|
22
|
-
|
23
|
-
acc[field] = val
|
36
|
+
acc[field] = val if val != UNDEFINED
|
24
37
|
acc
|
25
38
|
end
|
26
39
|
end
|
@@ -46,27 +59,13 @@ module Rubanok
|
|
46
59
|
|
47
60
|
val = params[field]
|
48
61
|
|
49
|
-
val = filter_with
|
62
|
+
val = filter_with&.call(val) if filter_with
|
50
63
|
|
51
64
|
return UNDEFINED if empty?(val)
|
52
65
|
|
53
66
|
val
|
54
67
|
end
|
55
68
|
|
56
|
-
using(Module.new do
|
57
|
-
refine NilClass do
|
58
|
-
def empty?
|
59
|
-
true
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
refine Object do
|
64
|
-
def empty?
|
65
|
-
false
|
66
|
-
end
|
67
|
-
end
|
68
|
-
end)
|
69
|
-
|
70
69
|
def empty?(val)
|
71
70
|
return false unless ignore_empty_values
|
72
71
|
|
data/lib/rubanok/version.rb
CHANGED
data/sig/rubanok.rbs
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Rubanok
|
2
|
+
module DSL
|
3
|
+
module Mapping : Processor
|
4
|
+
class Rule < Rubanok::Rule
|
5
|
+
METHOD_PREFIX: String
|
6
|
+
|
7
|
+
private
|
8
|
+
def build_method_name: () -> String
|
9
|
+
end
|
10
|
+
|
11
|
+
module ClassMethods : Module, _RulesAdding
|
12
|
+
def map: (*field fields, ?activate_on: (field | Array[field]), ?activate_always: bool, ?ignore_empty_values: bool, ?filter_with: Symbol) { () -> input } -> void
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.included: (singleton(Processor) base) -> void
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Rubanok
|
2
|
+
class UnexpectedInputError < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
module DSL
|
6
|
+
module Matching : Processor
|
7
|
+
class Rule < Rubanok::Rule
|
8
|
+
METHOD_PREFIX: String
|
9
|
+
@method_name: String
|
10
|
+
@fields: Array[field]
|
11
|
+
|
12
|
+
class Clause < Rubanok::Rule
|
13
|
+
@fields: Array[field]
|
14
|
+
|
15
|
+
attr_reader values: params
|
16
|
+
attr_reader id: String
|
17
|
+
attr_reader block: ^() -> input
|
18
|
+
|
19
|
+
def initialize: (String id, Array[field] fields, untyped values, ?activate_on: field | Array[field], ?activate_always: bool) { () -> input } -> void
|
20
|
+
def applicable?: (params) -> bool
|
21
|
+
|
22
|
+
alias to_method_name id
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader clauses: Array[Clause]
|
26
|
+
|
27
|
+
def matching_clause: (hash params) -> Clause?
|
28
|
+
def having: (*untyped values) { () -> input } -> void
|
29
|
+
def default: () { () -> input } -> void
|
30
|
+
|
31
|
+
private
|
32
|
+
def build_method_name: () -> String
|
33
|
+
end
|
34
|
+
|
35
|
+
module ClassMethods : Module, _RulesAdding, Matching
|
36
|
+
def match: (*field fields, ?activate_on: field | Array[field], ?activate_always: bool, ?fail_when_no_matches: bool?) { (Rule) -> void } -> void
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.included: (singleton(Processor) base) -> void
|
40
|
+
|
41
|
+
def default_match_handler: (Rule rule, hash params, bool? fail_when_no_matches) -> void
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module Rubanok
|
2
|
+
# Transformation parameters
|
3
|
+
type params = Hash[Symbol | String, untyped]
|
4
|
+
# Untyped Hash
|
5
|
+
type hash = Hash[untyped, untyped]
|
6
|
+
type field = Symbol
|
7
|
+
# Transformation target (we assume that input and output types are the same)
|
8
|
+
type input = Object?
|
9
|
+
|
10
|
+
interface _RulesAdding
|
11
|
+
def add_rule: (Rule rule) -> void
|
12
|
+
end
|
13
|
+
|
14
|
+
class Processor
|
15
|
+
extend DSL::Matching::ClassMethods
|
16
|
+
|
17
|
+
extend DSL::Mapping::ClassMethods
|
18
|
+
|
19
|
+
extend _RulesAdding
|
20
|
+
|
21
|
+
UNDEFINED: Object
|
22
|
+
|
23
|
+
self.@rules: Array[Rule]
|
24
|
+
self.@fields_set: Set[field]
|
25
|
+
|
26
|
+
def self.superclass: () -> singleton(Processor)
|
27
|
+
|
28
|
+
def self.call: (input, params) -> input
|
29
|
+
| (params) -> input
|
30
|
+
def self.rules: () -> Array[Rule]
|
31
|
+
def self.fields_set: () -> Set[field]
|
32
|
+
def self.project: (params) -> params
|
33
|
+
def self.prepare: () { () -> input } -> void
|
34
|
+
|
35
|
+
def initialize: (input) -> void
|
36
|
+
def call: (params) -> input
|
37
|
+
|
38
|
+
attr_accessor input: input
|
39
|
+
|
40
|
+
private
|
41
|
+
attr_accessor prepared: bool
|
42
|
+
|
43
|
+
alias raw input
|
44
|
+
alias prepared? prepared
|
45
|
+
|
46
|
+
def apply_rule!: (Rule rule, params) -> void
|
47
|
+
def prepare: () -> input
|
48
|
+
def prepare!: () -> void
|
49
|
+
def rules: () -> Array[Rule]
|
50
|
+
end
|
51
|
+
|
52
|
+
Plane: singleton(Processor)
|
53
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Rubanok
|
2
|
+
class Rule
|
3
|
+
UNDEFINED: Object
|
4
|
+
|
5
|
+
@method_name: String
|
6
|
+
|
7
|
+
attr_reader fields: Array[field]
|
8
|
+
attr_reader activate_on: Array[field]
|
9
|
+
attr_reader activate_always: bool
|
10
|
+
attr_reader ignore_empty_values: bool
|
11
|
+
attr_reader filter_with: Method?
|
12
|
+
|
13
|
+
%a{rbs:test:skip} def initialize: (
|
14
|
+
Array[field] fields,
|
15
|
+
?activate_on: field | Array[field],
|
16
|
+
?activate_always: bool,
|
17
|
+
?ignore_empty_values: bool,
|
18
|
+
?filter_with: Method?
|
19
|
+
) -> void
|
20
|
+
def project: (params) -> params
|
21
|
+
def applicable?: (params) -> bool
|
22
|
+
def to_method_name: () -> String
|
23
|
+
|
24
|
+
private
|
25
|
+
def build_method_name: () -> String
|
26
|
+
def fetch_value: (params, field) -> Object
|
27
|
+
def empty?: (untyped) -> bool
|
28
|
+
end
|
29
|
+
end
|
data/sig/typeprof.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Run typeprofiler:
|
4
|
+
#
|
5
|
+
# typeprof -Ilib sig/typeprof.rb
|
6
|
+
require "rubanok"
|
7
|
+
|
8
|
+
processor = Class.new(Rubanok::Processor) do
|
9
|
+
map :q do |q:|
|
10
|
+
raw
|
11
|
+
end
|
12
|
+
|
13
|
+
match :sort_by, :sort, activate_on: :sort_by do
|
14
|
+
having "status", "asc" do
|
15
|
+
raw
|
16
|
+
end
|
17
|
+
|
18
|
+
default do |sort_by:, sort: "asc"|
|
19
|
+
raw
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
processor.project({q: "search", sort_by: "name"})
|
25
|
+
processor.call([], {q: "search", sort_by: "name"})
|
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.4.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:
|
11
|
+
date: 2021-03-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: actionpack
|
@@ -113,6 +113,13 @@ files:
|
|
113
113
|
- lib/rubanok/rspec.rb
|
114
114
|
- lib/rubanok/rule.rb
|
115
115
|
- lib/rubanok/version.rb
|
116
|
+
- sig/rubanok.rbs
|
117
|
+
- sig/rubanok/dsl/mapping.rbs
|
118
|
+
- sig/rubanok/dsl/matching.rbs
|
119
|
+
- sig/rubanok/processor.rbs
|
120
|
+
- sig/rubanok/rule.rbs
|
121
|
+
- sig/rubanok/version.rbs
|
122
|
+
- sig/typeprof.rb
|
116
123
|
homepage: https://github.com/palkan/rubanok
|
117
124
|
licenses:
|
118
125
|
- MIT
|