transproc 1.0.0 → 1.1.1
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 +5 -5
- data/.codeclimate.yml +15 -0
- data/.gitignore +3 -0
- data/.travis.yml +14 -10
- data/CHANGELOG.md +61 -17
- data/Gemfile +9 -18
- data/README.md +17 -19
- data/Rakefile +1 -0
- data/lib/transproc.rb +3 -4
- data/lib/transproc/all.rb +2 -0
- data/lib/transproc/array.rb +9 -51
- data/lib/transproc/array/combine.rb +61 -0
- data/lib/transproc/class.rb +2 -0
- data/lib/transproc/coercions.rb +2 -0
- data/lib/transproc/compiler.rb +45 -0
- data/lib/transproc/composer.rb +2 -0
- data/lib/transproc/composite.rb +2 -0
- data/lib/transproc/conditional.rb +2 -0
- data/lib/transproc/constants.rb +5 -0
- data/lib/transproc/error.rb +2 -0
- data/lib/transproc/function.rb +4 -1
- data/lib/transproc/functions.rb +2 -0
- data/lib/transproc/hash.rb +105 -45
- data/lib/transproc/proc.rb +2 -0
- data/lib/transproc/recursion.rb +2 -0
- data/lib/transproc/registry.rb +4 -2
- data/lib/transproc/store.rb +1 -0
- data/lib/transproc/support/deprecations.rb +2 -0
- data/lib/transproc/transformer.rb +7 -1
- data/lib/transproc/transformer/class_interface.rb +32 -65
- data/lib/transproc/transformer/deprecated/class_interface.rb +81 -0
- data/lib/transproc/transformer/dsl.rb +51 -0
- data/lib/transproc/version.rb +3 -1
- data/spec/spec_helper.rb +21 -10
- data/spec/unit/array/combine_spec.rb +224 -0
- data/spec/unit/array_transformations_spec.rb +1 -52
- data/spec/unit/class_transformations_spec.rb +10 -7
- data/spec/unit/coercions_spec.rb +1 -1
- data/spec/unit/composer_spec.rb +1 -1
- data/spec/unit/conditional_spec.rb +1 -1
- data/spec/unit/function_not_found_error_spec.rb +1 -1
- data/spec/unit/function_spec.rb +16 -1
- data/spec/unit/hash_transformations_spec.rb +12 -1
- data/spec/unit/proc_transformations_spec.rb +3 -1
- data/spec/unit/recursion_spec.rb +1 -1
- data/spec/unit/registry_spec.rb +9 -1
- data/spec/unit/store_spec.rb +2 -1
- data/spec/unit/transformer/class_interface_spec.rb +364 -0
- data/spec/unit/transformer/dsl_spec.rb +15 -0
- data/spec/unit/transformer/instance_methods_spec.rb +25 -0
- data/spec/unit/transformer_spec.rb +128 -40
- data/spec/unit/transproc_spec.rb +1 -1
- data/transproc.gemspec +1 -5
- metadata +19 -54
- data/.rubocop.yml +0 -66
- data/.rubocop_todo.yml +0 -11
- data/rakelib/mutant.rake +0 -16
- data/rakelib/rubocop.rake +0 -18
- data/spec/support/mutant.rb +0 -10
@@ -1,7 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'transproc/transformer/dsl'
|
4
|
+
|
1
5
|
module Transproc
|
2
6
|
class Transformer
|
3
|
-
# @api
|
7
|
+
# @api public
|
4
8
|
module ClassInterface
|
9
|
+
# @api private
|
10
|
+
attr_reader :dsl
|
11
|
+
|
5
12
|
# Return a base Transproc::Transformer class with the
|
6
13
|
# container configured to the passed argument.
|
7
14
|
#
|
@@ -17,14 +24,18 @@ module Transproc
|
|
17
24
|
#
|
18
25
|
# @api public
|
19
26
|
def [](container)
|
20
|
-
klass = Class.new(
|
27
|
+
klass = Class.new(self)
|
21
28
|
klass.container(container)
|
22
29
|
klass
|
23
30
|
end
|
24
31
|
|
25
32
|
# @api private
|
26
33
|
def inherited(subclass)
|
34
|
+
super
|
35
|
+
|
27
36
|
subclass.container(@container) if defined?(@container)
|
37
|
+
|
38
|
+
subclass.instance_variable_set('@dsl', dsl.dup) if dsl
|
28
39
|
end
|
29
40
|
|
30
41
|
# Get or set the container to resolve transprocs from.
|
@@ -45,41 +56,33 @@ module Transproc
|
|
45
56
|
# @return [Transproc::Registry]
|
46
57
|
#
|
47
58
|
# @api private
|
48
|
-
def container(container =
|
49
|
-
if container
|
50
|
-
|
51
|
-
@container
|
59
|
+
def container(container = Undefined)
|
60
|
+
if container.equal?(Undefined)
|
61
|
+
@container ||= Module.new.extend(Transproc::Registry)
|
52
62
|
else
|
53
63
|
@container = container
|
54
64
|
end
|
55
65
|
end
|
56
66
|
|
57
|
-
# Define an anonymous transproc derived from given Transformer
|
58
|
-
# Evaluates block with transformations and returns initialized transproc.
|
59
|
-
# Does not mutate original Transformer
|
60
|
-
#
|
61
|
-
# @example
|
62
|
-
#
|
63
|
-
# class MyTransformer < Transproc::Transformer[MyContainer]
|
64
|
-
# end
|
65
|
-
#
|
66
|
-
# transproc = MyTransformer.define do
|
67
|
-
# map_values t(:to_string)
|
68
|
-
# end
|
69
|
-
# transproc.call(a: 1, b: 2)
|
70
|
-
# # => {a: '1', b: '2'}
|
71
|
-
#
|
72
|
-
# @yield Block allowing to define transformations. The same as class level DSL
|
73
|
-
#
|
74
|
-
# @return [Function] Composed transproc
|
75
|
-
#
|
76
67
|
# @api public
|
77
|
-
def
|
78
|
-
|
68
|
+
def import(*args)
|
69
|
+
container.import(*args)
|
70
|
+
end
|
71
|
+
|
72
|
+
# @api public
|
73
|
+
def define!(&block)
|
74
|
+
@dsl ||= DSL.new(container)
|
75
|
+
@dsl.instance_eval(&block)
|
76
|
+
self
|
77
|
+
end
|
79
78
|
|
80
|
-
|
79
|
+
# @api public
|
80
|
+
def new(*)
|
81
|
+
super.tap do |transformer|
|
82
|
+
transformer.instance_variable_set('@transproc', dsl.(transformer)) if dsl
|
83
|
+
end
|
81
84
|
end
|
82
|
-
|
85
|
+
ruby2_keywords(:new) if respond_to?(:ruby2_keywords, true)
|
83
86
|
|
84
87
|
# Get a transformation from the container,
|
85
88
|
# without adding it to the transformation pipeline
|
@@ -105,42 +108,6 @@ module Transproc
|
|
105
108
|
def t(fn, *args)
|
106
109
|
container[fn, *args]
|
107
110
|
end
|
108
|
-
|
109
|
-
# @api private
|
110
|
-
def method_missing(method, *args, &block)
|
111
|
-
if container.contain?(method)
|
112
|
-
args.push(define(&block)) if block_given?
|
113
|
-
transformations << t(method, *args)
|
114
|
-
else
|
115
|
-
super
|
116
|
-
end
|
117
|
-
end
|
118
|
-
|
119
|
-
# @api private
|
120
|
-
def respond_to_missing?(method, _include_private = false)
|
121
|
-
container.contain?(method) || super
|
122
|
-
end
|
123
|
-
|
124
|
-
# @api private
|
125
|
-
def transproc
|
126
|
-
transformations.reduce(:>>)
|
127
|
-
end
|
128
|
-
|
129
|
-
private
|
130
|
-
|
131
|
-
# An array containing the transformation pipeline
|
132
|
-
#
|
133
|
-
# @api private
|
134
|
-
def transformations
|
135
|
-
@transformations ||= []
|
136
|
-
end
|
137
|
-
|
138
|
-
# @api private
|
139
|
-
def ensure_container_presence!
|
140
|
-
return if defined?(@container)
|
141
|
-
raise ArgumentError, 'Transformer function registry is empty. '\
|
142
|
-
'Provide your registry via Transproc::Transformer[YourRegistry]'
|
143
|
-
end
|
144
111
|
end
|
145
112
|
end
|
146
113
|
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transproc
|
4
|
+
class Transformer
|
5
|
+
module Deprecated
|
6
|
+
# @api public
|
7
|
+
module ClassInterface
|
8
|
+
# @api public
|
9
|
+
def new(*)
|
10
|
+
super.tap do |transformer|
|
11
|
+
transformer.instance_variable_set('@transproc', transproc) if transformations.any?
|
12
|
+
end
|
13
|
+
end
|
14
|
+
ruby2_keywords(:new) if respond_to?(:ruby2_keywords, true)
|
15
|
+
|
16
|
+
# @api private
|
17
|
+
def inherited(subclass)
|
18
|
+
super
|
19
|
+
|
20
|
+
if transformations.any?
|
21
|
+
subclass.instance_variable_set('@transformations', transformations.dup)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Define an anonymous transproc derived from given Transformer
|
26
|
+
# Evaluates block with transformations and returns initialized transproc.
|
27
|
+
# Does not mutate original Transformer
|
28
|
+
#
|
29
|
+
# @example
|
30
|
+
#
|
31
|
+
# class MyTransformer < Transproc::Transformer[MyContainer]
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# transproc = MyTransformer.define do
|
35
|
+
# map_values t(:to_string)
|
36
|
+
# end
|
37
|
+
# transproc.call(a: 1, b: 2)
|
38
|
+
# # => {a: '1', b: '2'}
|
39
|
+
#
|
40
|
+
# @yield Block allowing to define transformations. The same as class level DSL
|
41
|
+
#
|
42
|
+
# @return [Function] Composed transproc
|
43
|
+
#
|
44
|
+
# @api public
|
45
|
+
def define(&block)
|
46
|
+
return transproc unless block_given?
|
47
|
+
|
48
|
+
Class.new(Transformer[container]).tap { |klass| klass.instance_eval(&block) }.transproc
|
49
|
+
end
|
50
|
+
alias build define
|
51
|
+
|
52
|
+
# @api private
|
53
|
+
def method_missing(method, *args, &block)
|
54
|
+
super unless container.contain?(method)
|
55
|
+
func = block ? t(method, *args, define(&block)) : t(method, *args)
|
56
|
+
transformations << func
|
57
|
+
func
|
58
|
+
end
|
59
|
+
|
60
|
+
# @api private
|
61
|
+
def respond_to_missing?(method, _include_private = false)
|
62
|
+
super || container.contain?(method)
|
63
|
+
end
|
64
|
+
|
65
|
+
# @api private
|
66
|
+
def transproc
|
67
|
+
transformations.reduce(:>>)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
# An array containing the transformation pipeline
|
73
|
+
#
|
74
|
+
# @api private
|
75
|
+
def transformations
|
76
|
+
@transformations ||= []
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'transproc/compiler'
|
4
|
+
|
5
|
+
module Transproc
|
6
|
+
class Transformer
|
7
|
+
# @api public
|
8
|
+
class DSL
|
9
|
+
# @api private
|
10
|
+
attr_reader :container
|
11
|
+
|
12
|
+
# @api private
|
13
|
+
attr_reader :ast
|
14
|
+
|
15
|
+
# @api private
|
16
|
+
def initialize(container, ast: [], &block)
|
17
|
+
@container = container
|
18
|
+
@ast = ast
|
19
|
+
instance_eval(&block) if block
|
20
|
+
end
|
21
|
+
|
22
|
+
# @api private
|
23
|
+
def dup
|
24
|
+
self.class.new(container, ast: ast.dup)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @api private
|
28
|
+
def call(transformer)
|
29
|
+
Compiler.new(container, transformer).(ast)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
# @api private
|
35
|
+
def node(&block)
|
36
|
+
[:t, self.class.new(container, &block).ast]
|
37
|
+
end
|
38
|
+
|
39
|
+
# @api private
|
40
|
+
def respond_to_missing?(method, _include_private = false)
|
41
|
+
super || container.contain?(method)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def method_missing(meth, *args, &block)
|
46
|
+
arg_nodes = *args.map { |a| [:arg, a] }
|
47
|
+
ast << [:fn, (block ? [meth, [*arg_nodes, node(&block)]] : [meth, arg_nodes])]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/transproc/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
@@ -1,19 +1,30 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
if RUBY_ENGINE == 'ruby' && ENV['COVERAGE'] == 'true'
|
4
|
+
require 'yaml'
|
5
|
+
rubies = YAML.load(File.read(File.join(__dir__, '..', '.travis.yml')))['rvm']
|
6
|
+
latest_mri = rubies.select { |v| v =~ /\A\d+\.\d+.\d+\z/ }.max
|
7
|
+
|
8
|
+
if RUBY_VERSION == latest_mri
|
9
|
+
require 'simplecov'
|
10
|
+
SimpleCov.start do
|
11
|
+
add_filter '/spec/'
|
12
|
+
end
|
5
13
|
end
|
6
14
|
end
|
7
15
|
|
8
|
-
|
9
|
-
require '
|
10
|
-
|
11
|
-
|
16
|
+
if defined? Warning
|
17
|
+
require 'warning'
|
18
|
+
|
19
|
+
Warning.ignore(/rspec/)
|
20
|
+
Warning.process { |w| raise RuntimeError, w } unless ENV['NO_WARNING']
|
21
|
+
end
|
12
22
|
|
13
23
|
begin
|
14
24
|
require 'byebug'
|
15
|
-
rescue LoadError
|
16
|
-
|
25
|
+
rescue LoadError;end
|
26
|
+
|
27
|
+
require 'transproc/all'
|
17
28
|
|
18
29
|
root = Pathname(__FILE__).dirname
|
19
30
|
Dir[root.join('support/*.rb').to_s].each { |f| require f }
|
@@ -0,0 +1,224 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
describe Transproc::ArrayTransformations do
|
6
|
+
describe '.combine' do
|
7
|
+
subject(:result) { described_class.t(:combine, mappings)[input] }
|
8
|
+
|
9
|
+
let(:input) { [[]] }
|
10
|
+
let(:mappings) { [] }
|
11
|
+
|
12
|
+
it { is_expected.to be_a(Array) }
|
13
|
+
|
14
|
+
it { is_expected.to eq([]) }
|
15
|
+
|
16
|
+
context 'without groups' do
|
17
|
+
let(:input) do
|
18
|
+
[
|
19
|
+
[
|
20
|
+
{name: 'Jane', email: 'jane@doe.org'}.freeze,
|
21
|
+
{name: 'Joe', email: 'joe@doe.org'}.freeze
|
22
|
+
].freeze
|
23
|
+
].freeze
|
24
|
+
end
|
25
|
+
|
26
|
+
it { is_expected.to eq input.first }
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'with one group' do
|
30
|
+
let(:input) do
|
31
|
+
[
|
32
|
+
[
|
33
|
+
{name: 'Jane', email: 'jane@doe.org'}.freeze,
|
34
|
+
{name: 'Joe', email: 'joe@doe.org'}.freeze
|
35
|
+
].freeze,
|
36
|
+
[
|
37
|
+
[
|
38
|
+
{user: 'Jane', title: 'One'}.freeze,
|
39
|
+
{user: 'Jane', title: 'Two'}.freeze,
|
40
|
+
{user: 'Joe', title: 'Three'}.freeze
|
41
|
+
]
|
42
|
+
]
|
43
|
+
].freeze
|
44
|
+
end
|
45
|
+
let(:mappings) { [[:tasks, {name: :user}]] }
|
46
|
+
|
47
|
+
it 'merges hashes from arrays using provided join keys' do
|
48
|
+
output = [
|
49
|
+
{name: 'Jane', email: 'jane@doe.org', tasks: [
|
50
|
+
{user: 'Jane', title: 'One'},
|
51
|
+
{user: 'Jane', title: 'Two'}
|
52
|
+
]},
|
53
|
+
{name: 'Joe', email: 'joe@doe.org', tasks: [
|
54
|
+
{user: 'Joe', title: 'Three'}
|
55
|
+
]}
|
56
|
+
]
|
57
|
+
is_expected.to eql(output)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'with empty nodes' do
|
62
|
+
let(:input) do
|
63
|
+
[
|
64
|
+
[{name: 'Jane', email: 'jane@doe.org'}.freeze].freeze,
|
65
|
+
[
|
66
|
+
[]
|
67
|
+
]
|
68
|
+
].freeze
|
69
|
+
end
|
70
|
+
|
71
|
+
let(:mappings) { [[:tasks, {name: :user}]] }
|
72
|
+
|
73
|
+
it { is_expected.to eq([{name: 'Jane', email: 'jane@doe.org', tasks: []}]) }
|
74
|
+
end
|
75
|
+
|
76
|
+
context 'with double mapping' do
|
77
|
+
let(:input) do
|
78
|
+
[
|
79
|
+
[
|
80
|
+
{name: 'Jane', email: 'jane@doe.org'}.freeze
|
81
|
+
].freeze,
|
82
|
+
[
|
83
|
+
[
|
84
|
+
{user: 'Jane', user_email: 'jane@doe.org', title: 'One'}.freeze,
|
85
|
+
{user: 'Jane', user_email: '', title: 'Two'}.freeze
|
86
|
+
].freeze
|
87
|
+
].freeze
|
88
|
+
].freeze
|
89
|
+
end
|
90
|
+
|
91
|
+
let(:mappings) { [[:tasks, {name: :user, email: :user_email}]] }
|
92
|
+
|
93
|
+
it 'searches by two keys simultaneously' do
|
94
|
+
output = [
|
95
|
+
{name: 'Jane', email: 'jane@doe.org', tasks: [
|
96
|
+
{user: 'Jane', user_email: 'jane@doe.org', title: 'One'}
|
97
|
+
]}
|
98
|
+
]
|
99
|
+
is_expected.to eql(output)
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
context 'with non-array argument' do
|
104
|
+
let(:input) do
|
105
|
+
123
|
106
|
+
end
|
107
|
+
|
108
|
+
let(:mappings) { [[:page, {page_id: :id}]] }
|
109
|
+
|
110
|
+
it { is_expected.to eq(123) }
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'with empty nested array' do
|
114
|
+
let(:input) do
|
115
|
+
[
|
116
|
+
[],
|
117
|
+
[
|
118
|
+
[]
|
119
|
+
]
|
120
|
+
]
|
121
|
+
end
|
122
|
+
|
123
|
+
let(:mappings) { [[:menu_items, {id: :menu_id}, [[:page, {page_id: :id}]]]] }
|
124
|
+
|
125
|
+
it 'does not crash' do
|
126
|
+
expect { result }.not_to raise_error
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'with enumerable input' do
|
131
|
+
let(:my_enumerator) do
|
132
|
+
Class.new do
|
133
|
+
include Enumerable
|
134
|
+
extend Forwardable
|
135
|
+
|
136
|
+
def_delegator :@array, :each
|
137
|
+
|
138
|
+
def initialize(array)
|
139
|
+
@array = array
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
let(:input) do
|
145
|
+
[
|
146
|
+
my_enumerator.new([
|
147
|
+
{name: 'Jane', email: 'jane@doe.org'}.freeze,
|
148
|
+
{name: 'Joe', email: 'joe@doe.org'}.freeze
|
149
|
+
].freeze),
|
150
|
+
my_enumerator.new([
|
151
|
+
my_enumerator.new([
|
152
|
+
{user: 'Jane', title: 'One'}.freeze,
|
153
|
+
{user: 'Jane', title: 'Two'}.freeze,
|
154
|
+
{user: 'Joe', title: 'Three'}.freeze
|
155
|
+
].freeze)
|
156
|
+
].freeze)
|
157
|
+
].freeze
|
158
|
+
end
|
159
|
+
let(:mappings) { [[:tasks, {name: :user}]] }
|
160
|
+
|
161
|
+
it 'supports enumerables as well' do
|
162
|
+
output = [
|
163
|
+
{name: 'Jane', email: 'jane@doe.org', tasks: [
|
164
|
+
{user: 'Jane', title: 'One'},
|
165
|
+
{user: 'Jane', title: 'Two'}
|
166
|
+
]},
|
167
|
+
{name: 'Joe', email: 'joe@doe.org', tasks: [
|
168
|
+
{user: 'Joe', title: 'Three'}
|
169
|
+
]}
|
170
|
+
]
|
171
|
+
is_expected.to eql(output)
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
175
|
+
describe 'integration test' do
|
176
|
+
let(:input) do
|
177
|
+
[
|
178
|
+
[
|
179
|
+
{name: 'Jane', email: 'jane@doe.org'},
|
180
|
+
{name: 'Joe', email: 'joe@doe.org'}
|
181
|
+
],
|
182
|
+
[
|
183
|
+
[
|
184
|
+
# user tasks
|
185
|
+
[
|
186
|
+
{user: 'Jane', title: 'One'},
|
187
|
+
{user: 'Jane', title: 'Two'},
|
188
|
+
{user: 'Joe', title: 'Three'}
|
189
|
+
],
|
190
|
+
[
|
191
|
+
# task tags
|
192
|
+
[
|
193
|
+
{task: 'One', tag: 'red'},
|
194
|
+
{task: 'Three', tag: 'blue'}
|
195
|
+
]
|
196
|
+
]
|
197
|
+
]
|
198
|
+
]
|
199
|
+
]
|
200
|
+
end
|
201
|
+
|
202
|
+
let(:mappings) { [[:tasks, {name: :user}, [[:tags, title: :task]]]] }
|
203
|
+
|
204
|
+
it 'merges hashes from arrays using provided join keys' do
|
205
|
+
output = [
|
206
|
+
{name: 'Jane', email: 'jane@doe.org', tasks: [
|
207
|
+
{user: 'Jane', title: 'One', tags: [{task: 'One', tag: 'red'}]},
|
208
|
+
{user: 'Jane', title: 'Two', tags: []}
|
209
|
+
]},
|
210
|
+
{
|
211
|
+
name: 'Joe', email: 'joe@doe.org', tasks: [
|
212
|
+
{
|
213
|
+
user: 'Joe', title: 'Three', tags: [
|
214
|
+
{task: 'Three', tag: 'blue'}
|
215
|
+
]
|
216
|
+
}
|
217
|
+
]
|
218
|
+
}
|
219
|
+
]
|
220
|
+
is_expected.to eql(output)
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|