rubanok 0.1.1 → 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 +59 -0
- data/LICENSE.txt +1 -1
- data/README.md +231 -33
- data/lib/rubanok.rb +8 -4
- data/lib/rubanok/dsl/mapping.rb +22 -5
- data/lib/rubanok/dsl/matching.rb +40 -16
- data/lib/rubanok/processor.rb +144 -0
- data/lib/rubanok/rails/controller.rb +59 -14
- data/lib/rubanok/railtie.rb +6 -0
- data/lib/rubanok/rspec.rb +25 -11
- data/lib/rubanok/rule.rb +32 -24
- 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 +27 -58
- data/.gitignore +0 -12
- data/.rspec +0 -3
- data/.rubocop.yml +0 -63
- data/.travis.yml +0 -24
- data/Gemfile +0 -13
- data/Gemfile.local +0 -2
- data/Rakefile +0 -10
- data/bin/console +0 -8
- data/bin/setup +0 -8
- data/gemfiles/rails42.gemfile +0 -6
- data/gemfiles/rails52.gemfile +0 -6
- data/gemfiles/railsmaster.gemfile +0 -5
- data/lib/rubanok/ext/symbolize_keys.rb +0 -13
- data/lib/rubanok/plane.rb +0 -92
- data/rubanok.gemspec +0 -35
data/lib/rubanok.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
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
|
|
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
|
@@ -13,15 +14,15 @@ end
|
|
13
14
|
#
|
14
15
|
# Example:
|
15
16
|
#
|
16
|
-
# class
|
17
|
+
# class CourseSessionProcessor < Rubanok::Processor
|
17
18
|
# map :q do |q:|
|
18
|
-
#
|
19
|
+
# raw.searh(q)
|
19
20
|
# end
|
20
21
|
# end
|
21
22
|
#
|
22
23
|
# class CourseSessionController < ApplicationController
|
23
24
|
# def index
|
24
|
-
# @sessions =
|
25
|
+
# @sessions = rubanok_process(CourseSession.all)
|
25
26
|
# end
|
26
27
|
# end
|
27
28
|
module Rubanok
|
@@ -30,7 +31,10 @@ module Rubanok
|
|
30
31
|
# When the value is empty and ignored the corresponding matcher/mapper
|
31
32
|
# is not activated (true by default)
|
32
33
|
attr_accessor :ignore_empty_values
|
34
|
+
# Define wheter to fail when `match` rule cannot find matching value
|
35
|
+
attr_accessor :fail_when_no_matches
|
33
36
|
end
|
34
37
|
|
35
38
|
self.ignore_empty_values = true
|
39
|
+
self.fail_when_no_matches = false
|
36
40
|
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,29 @@ module Rubanok
|
|
20
20
|
end
|
21
21
|
end
|
22
22
|
|
23
|
-
|
24
|
-
|
23
|
+
module ClassMethods
|
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
|
25
26
|
|
26
|
-
|
27
|
+
if filter.is_a?(Symbol)
|
28
|
+
respond_to?(filter) || raise(
|
29
|
+
ArgumentError,
|
30
|
+
"Unknown class method #{filter} for #{self}. " \
|
31
|
+
"Make sure that a filter method is defined before the call to .map."
|
32
|
+
)
|
33
|
+
filter = method(filter)
|
34
|
+
end
|
27
35
|
|
28
|
-
|
36
|
+
rule = Rule.new(fields, activate_on: activate_on, activate_always: activate_always, ignore_empty_values: ignore_empty_values, filter_with: filter)
|
37
|
+
|
38
|
+
define_method(rule.to_method_name, &block)
|
39
|
+
|
40
|
+
add_rule rule
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.included(base)
|
45
|
+
base.extend ClassMethods
|
29
46
|
end
|
30
47
|
end
|
31
48
|
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
|
@@ -20,8 +22,8 @@ module Rubanok
|
|
20
22
|
class Clause < Rubanok::Rule
|
21
23
|
attr_reader :values, :id, :block
|
22
24
|
|
23
|
-
def initialize(id, fields, values
|
24
|
-
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)
|
25
27
|
@id = id
|
26
28
|
@block = block
|
27
29
|
@values = Hash[fields.take(values.size).zip(values)].freeze
|
@@ -37,7 +39,7 @@ module Rubanok
|
|
37
39
|
|
38
40
|
attr_reader :clauses
|
39
41
|
|
40
|
-
def initialize(
|
42
|
+
def initialize(fields, activate_on: fields, activate_always: false, ignore_empty_values: Rubanok.ignore_empty_values, filter_with: nil)
|
41
43
|
super
|
42
44
|
@clauses = []
|
43
45
|
end
|
@@ -53,7 +55,7 @@ module Rubanok
|
|
53
55
|
end
|
54
56
|
|
55
57
|
def default(&block)
|
56
|
-
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)
|
57
59
|
end
|
58
60
|
|
59
61
|
private
|
@@ -64,23 +66,45 @@ module Rubanok
|
|
64
66
|
end
|
65
67
|
end
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
+
module ClassMethods
|
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)
|
69
72
|
|
70
|
-
|
73
|
+
rule.instance_eval(&block)
|
71
74
|
|
72
|
-
|
73
|
-
|
74
|
-
next raw unless clause
|
75
|
+
define_method(rule.to_method_name) do |params = {}|
|
76
|
+
params ||= {} if params.nil?
|
75
77
|
|
76
|
-
|
77
|
-
|
78
|
+
clause = rule.matching_clause(params)
|
79
|
+
|
80
|
+
if clause
|
81
|
+
apply_rule! clause, params
|
82
|
+
else
|
83
|
+
default_match_handler(rule, params, fail_when_no_matches)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
rule.clauses.each do |clause|
|
88
|
+
define_method(clause.to_method_name, &clause.block)
|
89
|
+
end
|
78
90
|
|
79
|
-
|
80
|
-
define_method(clause.to_method_name, &clause.block)
|
91
|
+
add_rule rule
|
81
92
|
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.included(base)
|
96
|
+
base.extend ClassMethods
|
97
|
+
end
|
98
|
+
|
99
|
+
def default_match_handler(rule, params, fail_when_no_matches)
|
100
|
+
fail_when_no_matches = Rubanok.fail_when_no_matches if fail_when_no_matches.nil?
|
101
|
+
return raw unless fail_when_no_matches
|
82
102
|
|
83
|
-
|
103
|
+
raise ::Rubanok::UnexpectedInputError, <<~MSG
|
104
|
+
Unexpected input: #{params.slice(*rule.fields)}.
|
105
|
+
Available values are:
|
106
|
+
#{rule.clauses.map(&:values).join("\n ")}
|
107
|
+
MSG
|
84
108
|
end
|
85
109
|
end
|
86
110
|
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "set"
|
4
|
+
|
5
|
+
require "rubanok/rule"
|
6
|
+
|
7
|
+
require "rubanok/dsl/mapping"
|
8
|
+
require "rubanok/dsl/matching"
|
9
|
+
|
10
|
+
module Rubanok
|
11
|
+
# Base class for processors (_planes_)
|
12
|
+
#
|
13
|
+
# Define transformation rules via `map` and `match` methods
|
14
|
+
# and apply them by calling the processor:
|
15
|
+
#
|
16
|
+
# class MyTransformer < Rubanok::Processor
|
17
|
+
# map :type do
|
18
|
+
# raw.where(type: type)
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# MyTransformer.call(MyModel.all, {type: "public"})
|
23
|
+
#
|
24
|
+
# NOTE: the second argument (`params`) MUST be a Hash. Keys could be either Symbols
|
25
|
+
# or Strings (we automatically transform strings to symbols while matching rules).
|
26
|
+
#
|
27
|
+
# All transformation methods are called within the context of the instance of
|
28
|
+
# a processor class.
|
29
|
+
#
|
30
|
+
# You can access the input data via `raw` method.
|
31
|
+
class Processor
|
32
|
+
include DSL::Matching
|
33
|
+
include DSL::Mapping
|
34
|
+
|
35
|
+
UNDEFINED = Object.new
|
36
|
+
|
37
|
+
class << self
|
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?
|
42
|
+
|
43
|
+
# @type var params: untyped
|
44
|
+
new(input).call(params)
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_rule(rule)
|
48
|
+
fields_set.merge rule.fields
|
49
|
+
rules << rule
|
50
|
+
end
|
51
|
+
|
52
|
+
def rules
|
53
|
+
return @rules if instance_variable_defined?(:@rules)
|
54
|
+
|
55
|
+
@rules =
|
56
|
+
if superclass <= Processor
|
57
|
+
superclass.rules.dup
|
58
|
+
else
|
59
|
+
[]
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def fields_set
|
64
|
+
return @fields_set if instance_variable_defined?(:@fields_set)
|
65
|
+
|
66
|
+
@fields_set =
|
67
|
+
if superclass <= Processor
|
68
|
+
superclass.fields_set.dup
|
69
|
+
else
|
70
|
+
Set.new
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
# Generates a `params` projection including only the keys used
|
75
|
+
# by the rules
|
76
|
+
def project(params)
|
77
|
+
params = params.transform_keys(&:to_sym)
|
78
|
+
params.slice(*fields_set.to_a)
|
79
|
+
end
|
80
|
+
|
81
|
+
# DSL to define the #prepare method
|
82
|
+
def prepare(&block)
|
83
|
+
define_method(:prepare, &block)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def initialize(input)
|
88
|
+
@input = input
|
89
|
+
@prepared = false
|
90
|
+
end
|
91
|
+
|
92
|
+
def call(params)
|
93
|
+
params = params.transform_keys(&:to_sym)
|
94
|
+
|
95
|
+
rules.each do |rule|
|
96
|
+
next unless rule.applicable?(params)
|
97
|
+
|
98
|
+
prepare! unless prepared?
|
99
|
+
apply_rule! rule, params
|
100
|
+
end
|
101
|
+
|
102
|
+
input
|
103
|
+
end
|
104
|
+
|
105
|
+
private
|
106
|
+
|
107
|
+
attr_accessor :input, :prepared
|
108
|
+
|
109
|
+
alias raw input
|
110
|
+
alias prepared? prepared
|
111
|
+
|
112
|
+
def apply_rule!(rule, params)
|
113
|
+
method_name, data = rule.to_method_name, rule.project(params)
|
114
|
+
|
115
|
+
return unless data
|
116
|
+
|
117
|
+
self.input =
|
118
|
+
if data.empty?
|
119
|
+
send(method_name)
|
120
|
+
else
|
121
|
+
send(method_name, **data)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def prepare
|
126
|
+
# no-op
|
127
|
+
end
|
128
|
+
|
129
|
+
def prepare!
|
130
|
+
@prepared = true
|
131
|
+
|
132
|
+
prepared_input = prepare
|
133
|
+
return unless prepared_input
|
134
|
+
|
135
|
+
self.input = prepared_input
|
136
|
+
end
|
137
|
+
|
138
|
+
def rules
|
139
|
+
self.class.rules
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
Plane = Processor
|
144
|
+
end
|
@@ -4,33 +4,78 @@ 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
|
+
if respond_to?(:helper_method)
|
13
|
+
helper_method :rubanok_scope
|
14
|
+
helper_method :planish_scope
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# This method process passed data (e.g. ActiveRecord relation) using
|
19
|
+
# the corresponding Processor class.
|
13
20
|
#
|
14
|
-
#
|
15
|
-
# "PostsController ->
|
21
|
+
# Processor is inferred from controller name, e.g.
|
22
|
+
# "PostsController -> PostProcessor".
|
16
23
|
#
|
17
|
-
# You can specify the
|
24
|
+
# You can specify the Processor class explicitly via `with` option.
|
18
25
|
#
|
19
|
-
# By default, `params` object is passed as
|
26
|
+
# By default, `params` object is passed as parameters, but you
|
20
27
|
# can specify the params via `params` option.
|
21
|
-
def
|
28
|
+
def rubanok_process(data, params = nil, with: nil)
|
29
|
+
with_inferred_rubanok_params(with, params) do |rubanok_class, rubanok_params|
|
30
|
+
rubanok_class.call(data, rubanok_params)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# This method filters the passed params and returns the Hash (!)
|
35
|
+
# of the params recongnized by the processor.
|
36
|
+
#
|
37
|
+
# Processor is inferred from controller name, e.g.
|
38
|
+
# "PostsController -> PostProcessor".
|
39
|
+
#
|
40
|
+
# You can specify the Processor class explicitly via `with` option.
|
41
|
+
#
|
42
|
+
# By default, `params` object is passed as parameters, but you
|
43
|
+
# can specify the params via `params` option.
|
44
|
+
def rubanok_scope(params = nil, with: nil)
|
45
|
+
with_inferred_rubanok_params(with, params) do |rubanok_class, rubanok_params|
|
46
|
+
rubanok_class.project(rubanok_params)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Tries to infer the rubanok processor class from controller path
|
51
|
+
def implicit_rubanok_class
|
52
|
+
"#{controller_path.classify.pluralize}Processor".safe_constantize
|
53
|
+
end
|
54
|
+
|
55
|
+
def with_inferred_rubanok_params(with, params)
|
56
|
+
with ||= implicit_rubanok_class
|
57
|
+
|
22
58
|
if with.nil?
|
23
|
-
raise ArgumentError, "Failed to find a
|
24
|
-
"Please, specify the
|
59
|
+
raise ArgumentError, "Failed to find a processor class for #{self.class.name}. " \
|
60
|
+
"Please, specify the processor class explicitly via `with` option"
|
25
61
|
end
|
26
62
|
|
27
|
-
|
63
|
+
params ||= self.params
|
64
|
+
|
65
|
+
params = params.to_unsafe_h if params.is_a?(ActionController::Parameters)
|
66
|
+
|
67
|
+
yield with, params
|
68
|
+
end
|
69
|
+
|
70
|
+
# Backward compatibility
|
71
|
+
def planish(*args, with: implicit_plane_class)
|
72
|
+
rubanok_process(*args, with: with)
|
73
|
+
end
|
28
74
|
|
29
|
-
|
30
|
-
|
75
|
+
def planish_scope(*args, with: implicit_plane_class)
|
76
|
+
rubanok_scope(*args, with: with)
|
31
77
|
end
|
32
78
|
|
33
|
-
# Tries to infer the plane class from controller path
|
34
79
|
def implicit_plane_class
|
35
80
|
"#{controller_path.classify.pluralize}Plane".safe_constantize
|
36
81
|
end
|
data/lib/rubanok/railtie.rb
CHANGED
@@ -8,6 +8,12 @@ module Rubanok # :nodoc:
|
|
8
8
|
|
9
9
|
ActionController::Base.include Rubanok::Controller
|
10
10
|
end
|
11
|
+
|
12
|
+
ActiveSupport.on_load(:action_controller_api) do
|
13
|
+
require "rubanok/rails/controller"
|
14
|
+
|
15
|
+
ActionController::API.include Rubanok::Controller
|
16
|
+
end
|
11
17
|
end
|
12
18
|
end
|
13
19
|
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
|