rubanok 0.3.0 → 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: 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