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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b992103f2ed0900e3f7b9f7aa631ee6b78d7f564c439a978a71ee44411915278
4
- data.tar.gz: fcfe5bb16302cc7a45a75499e638fcde7288c269faf36f31c7abadb388cc29c1
3
+ metadata.gz: 90f6ca9dfd61a6f143eff1f3ab3c2d7e41502c992879af785dcf414af15249fd
4
+ data.tar.gz: 8807cbb2a680e9fbc739a9f0214fb4753e24ee55e69da72d9b45c7f121900dd9
5
5
  SHA512:
6
- metadata.gz: bbc24b6c4d71a01f92d458a72a994341972ae8fba49832756cacd994411d8dec6f6d390c7566d44c80d37ca52abcc15981bd34b14edd78d1941cd1fde6379be9
7
- data.tar.gz: f0c9ae2bf40cf2db770af48032e1b5665776ad87b3abb37fc111e93bd88091e919136351dadb5d45faf7b11ea7205cd10f3d92e4909a0538317e73bb3f1c3502
6
+ metadata.gz: 2e015d7c3c517e42e5fe42037035db950cc87f9bd0099b38580c03ad156c36d0133681bd659451e8bdb58f15254799ce8d7b3158a1dc998210e3a425867a521d
7
+ data.tar.gz: 6c22e9a876e86f144726e8dbbe79b42f9d3052feb6f9c31688f7259872a5eed1e5ac66b031b890e0d1b4eceba7d3903331a073f2e13321b9f3d45c9fe53229d2
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
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
+
5
11
  ## 0.3.0 (2020-10-21)
6
12
 
7
13
  - Add `filter_with: Symbol | Proc` option to `.map` to allowing filtering the input value. ([@palkan][])
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
@@ -5,6 +5,7 @@ require "rubanok/processor"
5
5
 
6
6
  require "rubanok/railtie" if defined?(Rails)
7
7
 
8
+ # @type const ENV: Hash[String, String]
8
9
  if defined?(RSpec) && (ENV["RACK_ENV"] == "test" || ENV["RAILS_ENV"] == "test")
9
10
  require "rubanok/rspec"
10
11
  end
@@ -21,8 +21,8 @@ module Rubanok
21
21
  end
22
22
 
23
23
  module ClassMethods
24
- def map(*fields, **options, &block)
25
- filter = options[:filter_with]
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
- options[:filter_with] = method(filter)
33
+ filter = method(filter)
34
34
  end
35
35
 
36
- rule = Rule.new(fields, **options)
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
 
@@ -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 = [], **options, &block)
26
- super(fields, options)
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, **options, &block)
71
- rule = Rule.new(fields, **options.slice(:activate_on, :activate_always))
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
- apply_rule! clause, params
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|
@@ -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(*args)
37
- input, params =
38
- if args.size == 1
39
- [nil, args.first]
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 :owner, :fields, :activate_on, :activate_always, :ignore_empty_values, :filter_with
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 = Array(activate_on).freeze
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
- next acc if val == UNDEFINED
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.call(val) if 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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rubanok
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
data/sig/rubanok.rbs ADDED
@@ -0,0 +1,6 @@
1
+ module Rubanok
2
+ def self.fail_when_no_matches: () -> bool
3
+ def self.fail_when_no_matches=: (bool) -> bool
4
+ def self.ignore_empty_values: () -> bool
5
+ def self.ignore_empty_values=: (bool) -> bool
6
+ end
@@ -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
@@ -0,0 +1,3 @@
1
+ module Rubanok
2
+ VERSION: String
3
+ 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.3.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: 2020-10-21 00:00:00.000000000 Z
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