rubanok 0.1.1 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|