dry-transformer 0.1.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 +7 -0
- data/.codeclimate.yml +12 -0
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/custom_ci.yml +66 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +16 -0
- data/.rspec +4 -0
- data/.rubocop.yml +95 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +19 -0
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/docsite/source/built-in-transformations.html.md +47 -0
- data/docsite/source/index.html.md +15 -0
- data/docsite/source/transformation-objects.html.md +32 -0
- data/docsite/source/using-standalone-functions.html.md +82 -0
- data/dry-transformer.gemspec +22 -0
- data/lib/dry-transformer.rb +3 -0
- data/lib/dry/transformer.rb +23 -0
- data/lib/dry/transformer/all.rb +11 -0
- data/lib/dry/transformer/array.rb +183 -0
- data/lib/dry/transformer/array/combine.rb +65 -0
- data/lib/dry/transformer/class.rb +56 -0
- data/lib/dry/transformer/coercions.rb +196 -0
- data/lib/dry/transformer/compiler.rb +47 -0
- data/lib/dry/transformer/composite.rb +54 -0
- data/lib/dry/transformer/conditional.rb +76 -0
- data/lib/dry/transformer/constants.rb +7 -0
- data/lib/dry/transformer/error.rb +16 -0
- data/lib/dry/transformer/function.rb +109 -0
- data/lib/dry/transformer/hash.rb +453 -0
- data/lib/dry/transformer/pipe.rb +75 -0
- data/lib/dry/transformer/pipe/class_interface.rb +115 -0
- data/lib/dry/transformer/pipe/dsl.rb +58 -0
- data/lib/dry/transformer/proc.rb +46 -0
- data/lib/dry/transformer/recursion.rb +121 -0
- data/lib/dry/transformer/registry.rb +150 -0
- data/lib/dry/transformer/store.rb +128 -0
- data/lib/dry/transformer/version.rb +7 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/array/combine_spec.rb +224 -0
- data/spec/unit/array_transformations_spec.rb +233 -0
- data/spec/unit/class_transformations_spec.rb +50 -0
- data/spec/unit/coercions_spec.rb +132 -0
- data/spec/unit/conditional_spec.rb +48 -0
- data/spec/unit/function_not_found_error_spec.rb +12 -0
- data/spec/unit/function_spec.rb +193 -0
- data/spec/unit/hash_transformations_spec.rb +490 -0
- data/spec/unit/proc_transformations_spec.rb +20 -0
- data/spec/unit/recursion_spec.rb +145 -0
- data/spec/unit/registry_spec.rb +202 -0
- data/spec/unit/store_spec.rb +198 -0
- data/spec/unit/transformer/class_interface_spec.rb +350 -0
- data/spec/unit/transformer/dsl_spec.rb +15 -0
- data/spec/unit/transformer/instance_methods_spec.rb +25 -0
- metadata +119 -0
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/equalizer'
|
4
|
+
|
5
|
+
RSpec.describe Dry::Transformer::ClassTransformations do
|
6
|
+
describe '.constructor_inject' do
|
7
|
+
let(:klass) do
|
8
|
+
Struct.new(:name, :age) { include Dry::Equalizer.new(:name, :age) }
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns a new object initialized with the given arguments' do
|
12
|
+
constructor_inject = described_class.t(:constructor_inject, klass)
|
13
|
+
|
14
|
+
input = ['Jane', 25]
|
15
|
+
output = klass.new(*input)
|
16
|
+
result = constructor_inject[*input]
|
17
|
+
|
18
|
+
expect(result).to eql(output)
|
19
|
+
expect(result).to be_instance_of(klass)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe '.set_ivars' do
|
24
|
+
let(:klass) do
|
25
|
+
Class.new do
|
26
|
+
include Dry::Equalizer.new(:name, :age)
|
27
|
+
|
28
|
+
attr_reader :name, :age, :test
|
29
|
+
|
30
|
+
def initialize(name:, age:)
|
31
|
+
@name = name
|
32
|
+
@age = age
|
33
|
+
@test = true
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'allocates a new object and sets instance variables from hash key/value pairs' do
|
39
|
+
set_ivars = described_class.t(:set_ivars, klass)
|
40
|
+
|
41
|
+
input = { name: 'Jane', age: 25 }
|
42
|
+
output = klass.new(input)
|
43
|
+
result = set_ivars[input]
|
44
|
+
|
45
|
+
expect(result).to eql(output)
|
46
|
+
expect(result.test).to be(nil)
|
47
|
+
expect(result).to be_instance_of(klass)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Dry::Transformer::Coercions do
|
4
|
+
describe '.identity' do
|
5
|
+
let(:fn) { described_class.t(:identity) }
|
6
|
+
|
7
|
+
it 'returns the original value' do
|
8
|
+
expect(fn[:foo]).to eql :foo
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'returns nil by default' do
|
12
|
+
expect(fn[]).to eql nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.to_string' do
|
17
|
+
it 'turns integer into a string' do
|
18
|
+
expect(described_class.t(:to_string)[1]).to eql('1')
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe '.to_symbol' do
|
23
|
+
it 'turns string into a symbol' do
|
24
|
+
expect(described_class.t(:to_symbol)['test']).to eql(:test)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'turns non-string into a symbol' do
|
28
|
+
expect(described_class.t(:to_symbol)[1]).to eql(:'1')
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '.to_integer' do
|
33
|
+
it 'turns string into an integer' do
|
34
|
+
expect(described_class.t(:to_integer)['1']).to eql(1)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '.to_float' do
|
39
|
+
it 'turns string into a float' do
|
40
|
+
expect(described_class.t(:to_float)['1']).to eql(1.0)
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'turns integer into a float' do
|
44
|
+
expect(described_class.t(:to_float)[1]).to eql(1.0)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe '.to_decimal' do
|
49
|
+
it 'turns string into a decimal' do
|
50
|
+
expect(described_class.t(:to_decimal)['1.251']).to eql(BigDecimal('1.251'))
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'turns float into a decimal' do
|
54
|
+
expect(described_class.t(:to_decimal)[1.251]).to eql(BigDecimal('1.251'))
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'turns integer into a decimal' do
|
58
|
+
expect(described_class.t(:to_decimal)[1]).to eql(BigDecimal('1.0'))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '.to_date' do
|
63
|
+
it 'turns string into a date' do
|
64
|
+
date = Date.new(1983, 11, 18)
|
65
|
+
expect(described_class.t(:to_date)['18th, November 1983']).to eql(date)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '.to_time' do
|
70
|
+
it 'turns string into a time object' do
|
71
|
+
time = Time.new(2012, 1, 23, 11, 7, 7)
|
72
|
+
expect(described_class.t(:to_time)['2012-01-23 11:07:07']).to eql(time)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '.to_datetime' do
|
77
|
+
it 'turns string into a date' do
|
78
|
+
datetime = DateTime.new(2012, 1, 23, 11, 7, 7)
|
79
|
+
expect(described_class.t(:to_datetime)['2012-01-23 11:07:07']).to eql(datetime)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '.to_boolean' do
|
84
|
+
subject(:coercer) { described_class.t(:to_boolean) }
|
85
|
+
|
86
|
+
Dry::Transformer::Coercions::TRUE_VALUES.each do |value|
|
87
|
+
it "turns #{value.inspect} to true" do
|
88
|
+
expect(coercer[value]).to be(true)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
Dry::Transformer::Coercions::FALSE_VALUES.each do |value|
|
93
|
+
it "turns #{value.inspect} to false" do
|
94
|
+
expect(coercer[value]).to be(false)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '.to_tuples' do
|
100
|
+
subject(:to_tuples) { described_class.t(:to_tuples) }
|
101
|
+
|
102
|
+
context 'non-array' do
|
103
|
+
let(:input) { :foo }
|
104
|
+
|
105
|
+
it 'returns an array with one blank tuple' do
|
106
|
+
output = [{}]
|
107
|
+
|
108
|
+
expect(to_tuples[input]).to eql(output)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
context 'empty array' do
|
113
|
+
let(:input) { [] }
|
114
|
+
|
115
|
+
it 'returns an array with one blank tuple' do
|
116
|
+
output = [{}]
|
117
|
+
|
118
|
+
expect(to_tuples[input]).to eql(output)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
context 'array of tuples' do
|
123
|
+
let(:input) { [:foo, { bar: :BAZ }, :qux] }
|
124
|
+
|
125
|
+
it 'returns an array with tuples only' do
|
126
|
+
output = [{ bar: :BAZ }]
|
127
|
+
|
128
|
+
expect(to_tuples[input]).to eql(output)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Dry::Transformer::Conditional do
|
4
|
+
describe '.not' do
|
5
|
+
let(:fn) { described_class.t(:not, -> value { value.is_a? String }) }
|
6
|
+
subject { fn[input] }
|
7
|
+
|
8
|
+
context 'when predicate returns truthy value' do
|
9
|
+
let(:input) { 'foo' }
|
10
|
+
let(:output) { false }
|
11
|
+
|
12
|
+
it 'applies the first transformation' do
|
13
|
+
expect(subject).to eql output
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
context 'when predicate returns falsey value' do
|
18
|
+
let(:input) { :foo }
|
19
|
+
let(:output) { true }
|
20
|
+
|
21
|
+
it 'applies the first transformation' do
|
22
|
+
expect(subject).to eql output
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
describe '.guard' do
|
28
|
+
let(:fn) { described_class.t(:guard, condition, operation) }
|
29
|
+
let(:condition) { ->(value) { value.is_a?(::String) } }
|
30
|
+
let(:operation) { Dry::Transformer::Coercions.t(:to_integer) }
|
31
|
+
|
32
|
+
context 'when predicate returns truthy value' do
|
33
|
+
it 'applies the transformation and returns the result' do
|
34
|
+
input = '2'
|
35
|
+
|
36
|
+
expect(fn[input]).to eql(2)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when predicate returns falsey value' do
|
41
|
+
it 'returns the original value' do
|
42
|
+
input = { 'foo' => 'bar' }
|
43
|
+
|
44
|
+
expect(fn[input]).to eql('foo' => 'bar')
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Dry::Transformer::FunctionNotFoundError do
|
4
|
+
it 'complains that the function not registered' do
|
5
|
+
Foo = Module.new { extend Dry::Transformer::Registry }
|
6
|
+
|
7
|
+
expect { Foo[:foo] }.to raise_error do |error|
|
8
|
+
expect(error).to be_kind_of described_class
|
9
|
+
expect(error.message['function Foo[:foo]']).not_to be_nil
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,193 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Dry::Transformer::Function do
|
4
|
+
let(:container) do
|
5
|
+
Module.new do
|
6
|
+
extend Dry::Transformer::Registry
|
7
|
+
|
8
|
+
import Dry::Transformer::HashTransformations
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#name' do
|
13
|
+
let(:block) { proc { |v| v } }
|
14
|
+
|
15
|
+
it 'returns the name of the module function' do
|
16
|
+
expect(container[:symbolize_keys].name).to eql :symbolize_keys
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns the explicitly assigned name' do
|
20
|
+
expect(described_class.new(block, name: :identity).name).to eql :identity
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'returns the unnamed closure' do
|
24
|
+
expect(described_class.new(block).name).to eql block
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#>>' do
|
29
|
+
it 'composes named functions' do
|
30
|
+
f1 = container[:symbolize_keys]
|
31
|
+
f2 = container[:rename_keys, user_name: :name]
|
32
|
+
|
33
|
+
f3 = f1 >> f2
|
34
|
+
|
35
|
+
expect(f3.to_ast).to eql(
|
36
|
+
[
|
37
|
+
:symbolize_keys, [],
|
38
|
+
[
|
39
|
+
:rename_keys, [user_name: :name]
|
40
|
+
]
|
41
|
+
]
|
42
|
+
)
|
43
|
+
|
44
|
+
expect(f3['user_name' => 'Jane']).to eql(name: 'Jane')
|
45
|
+
|
46
|
+
f4 = f3 >> container[:nest, :details, [:name]]
|
47
|
+
|
48
|
+
expect(f4.to_ast).to eql(
|
49
|
+
[
|
50
|
+
:symbolize_keys, [],
|
51
|
+
[
|
52
|
+
:rename_keys, [user_name: :name]
|
53
|
+
],
|
54
|
+
[
|
55
|
+
:nest, [:details, [:name]]
|
56
|
+
]
|
57
|
+
]
|
58
|
+
)
|
59
|
+
|
60
|
+
expect(f4['user_name' => 'Jane']).to eql(details: { name: 'Jane' })
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'composes anonymous functions' do
|
64
|
+
f1 = container[->(v, m) { v * m }, 2]
|
65
|
+
f2 = container[:to_s.to_proc]
|
66
|
+
|
67
|
+
f3 = f1 >> f2
|
68
|
+
|
69
|
+
expect(f3.to_ast).to eql(
|
70
|
+
[
|
71
|
+
f1.fn, [2],
|
72
|
+
[
|
73
|
+
f2.fn, []
|
74
|
+
]
|
75
|
+
]
|
76
|
+
)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'plays well with registered compositions' do
|
80
|
+
container.register(:user_names, container[:symbolize_keys] + container[:rename_keys, user_name: :name])
|
81
|
+
f = container[:user_names]
|
82
|
+
|
83
|
+
expect(f['user_name' => 'Jane']).to eql(name: 'Jane')
|
84
|
+
expect(f.to_ast).to eql(
|
85
|
+
[
|
86
|
+
:symbolize_keys, [],
|
87
|
+
[
|
88
|
+
:rename_keys, [user_name: :name]
|
89
|
+
]
|
90
|
+
]
|
91
|
+
)
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'plays well with registered functions' do
|
95
|
+
container.register(:to_string, Dry::Transformer::Coercions.t(:to_string))
|
96
|
+
fn = container.t(:to_string)
|
97
|
+
|
98
|
+
expect(fn[:ok]).to eql('ok')
|
99
|
+
expect(fn.to_ast).to eql([:to_string, []])
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'plays well with functions as arguments' do
|
103
|
+
container.register(:map_array, Dry::Transformer::ArrayTransformations.t(:map_array))
|
104
|
+
container.register(:to_symbol, Dry::Transformer::Coercions.t(:to_symbol))
|
105
|
+
fn = container.t(:map_array, container.t(:to_symbol))
|
106
|
+
|
107
|
+
expect(fn.call(%w(a b c))).to eql([:a, :b, :c])
|
108
|
+
expect(fn.to_ast).to eql(
|
109
|
+
[
|
110
|
+
:map_array, [
|
111
|
+
[:to_symbol, []]
|
112
|
+
]
|
113
|
+
]
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
describe '#==' do
|
119
|
+
let(:fns) do
|
120
|
+
Module.new do
|
121
|
+
extend Dry::Transformer::Registry
|
122
|
+
import :wrap, from: Dry::Transformer::ArrayTransformations
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'returns true when the other is equal' do
|
127
|
+
left = fns[:wrap, :user, [:name, :email]]
|
128
|
+
right = fns[:wrap, :user, [:name, :email]]
|
129
|
+
|
130
|
+
expect(left == right).to be(true)
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'returns false when the other is not a fn' do
|
134
|
+
left = fns[:wrap, :user, [:name, :email]]
|
135
|
+
right = 'boo!'
|
136
|
+
|
137
|
+
expect(left == right).to be(false)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
describe '#to_proc' do
|
142
|
+
shared_examples :providing_a_proc do
|
143
|
+
let(:fn) { described_class.new(source) }
|
144
|
+
subject { fn.to_proc }
|
145
|
+
|
146
|
+
it 'returns a proc' do
|
147
|
+
expect(subject).to be_instance_of Proc
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'works fine' do
|
151
|
+
expect(subject.call :foo).to eql('foo')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
context 'from a method' do
|
156
|
+
let(:source) do
|
157
|
+
mod = Module.new do
|
158
|
+
def self.get(x)
|
159
|
+
x.to_s
|
160
|
+
end
|
161
|
+
end
|
162
|
+
mod.method(:get)
|
163
|
+
end
|
164
|
+
it_behaves_like :providing_a_proc
|
165
|
+
end
|
166
|
+
|
167
|
+
context 'from a proc' do
|
168
|
+
let(:source) { -> value { value.to_s } }
|
169
|
+
it_behaves_like :providing_a_proc
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'from a transproc' do
|
173
|
+
let(:source) { Dry::Transformer::Function.new -> value { value.to_s } }
|
174
|
+
it_behaves_like :providing_a_proc
|
175
|
+
|
176
|
+
it 'can be applied to collection' do
|
177
|
+
expect([:foo, :bar].map(&source)).to eql(%w(foo bar))
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
context 'with curried args' do
|
182
|
+
let(:source) { -> i, j { [i, j].join(' ') } }
|
183
|
+
|
184
|
+
it 'works fine' do
|
185
|
+
fn = described_class.new(source, args: ['world'])
|
186
|
+
|
187
|
+
result = fn.to_proc.call('hello')
|
188
|
+
|
189
|
+
expect(result).to eql('hello world')
|
190
|
+
end
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
@@ -0,0 +1,490 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe Dry::Transformer::HashTransformations do
|
4
|
+
describe '.map_keys' do
|
5
|
+
it 'returns a new hash with given proc applied to keys' do
|
6
|
+
map_keys = described_class.t(:map_keys, ->(key) { key.strip })
|
7
|
+
|
8
|
+
input = { ' foo ' => 'bar' }.freeze
|
9
|
+
output = { 'foo' => 'bar' }
|
10
|
+
|
11
|
+
expect(map_keys[input]).to eql(output)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
it { expect(described_class).not_to be_contain(:map_keys!) }
|
16
|
+
|
17
|
+
describe '.symbolize_keys' do
|
18
|
+
it 'returns a new hash with symbolized keys' do
|
19
|
+
symbolize_keys = described_class.t(:symbolize_keys)
|
20
|
+
|
21
|
+
input = { 1 => 'bar' }.freeze
|
22
|
+
output = { '1'.to_sym => 'bar' }
|
23
|
+
|
24
|
+
expect(symbolize_keys[input]).to eql(output)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '.deep_symbolize_keys' do
|
29
|
+
it 'returns a new hash with symbolized keys' do
|
30
|
+
symbolize_keys = described_class.t(:deep_symbolize_keys)
|
31
|
+
|
32
|
+
input = { 'foo' => 'bar', 'baz' => [{ 'one' => 1 }, 'two'] }
|
33
|
+
output = { foo: 'bar', baz: [{ one: 1 }, 'two'] }
|
34
|
+
|
35
|
+
expect(symbolize_keys[input]).to eql(output)
|
36
|
+
expect(input).to eql('foo' => 'bar', 'baz' => [{ 'one' => 1 }, 'two'])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
it { expect(described_class).not_to be_contain(:symbolize_keys!) }
|
41
|
+
|
42
|
+
describe '.stringify_keys' do
|
43
|
+
it 'returns a new hash with stringified keys' do
|
44
|
+
stringify_keys = described_class.t(:stringify_keys)
|
45
|
+
|
46
|
+
input = { foo: 'bar' }.freeze
|
47
|
+
output = { 'foo' => 'bar' }
|
48
|
+
|
49
|
+
expect(stringify_keys[input]).to eql(output)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe '.deep_stringify_keys' do
|
54
|
+
it 'returns a new hash with symbolized keys' do
|
55
|
+
stringify_keys = described_class.t(:deep_stringify_keys)
|
56
|
+
|
57
|
+
input = { foo: 'bar', baz: [{ one: 1 }, 'two'] }
|
58
|
+
output = { 'foo' => 'bar', 'baz' => [{ 'one' => 1 }, 'two'] }
|
59
|
+
|
60
|
+
expect(stringify_keys[input]).to eql(output)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it { expect(described_class).not_to be_contain(:stringify_keys!) }
|
65
|
+
|
66
|
+
describe '.map_values' do
|
67
|
+
it 'returns a new hash with given proc applied to values' do
|
68
|
+
map_values = described_class.t(:map_values, ->(value) { value.strip })
|
69
|
+
|
70
|
+
input = { 'foo' => ' bar ' }.freeze
|
71
|
+
output = { 'foo' => 'bar' }
|
72
|
+
|
73
|
+
expect(map_values[input]).to eql(output)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
it { expect(described_class).not_to be_contain(:map_values!) }
|
78
|
+
|
79
|
+
describe '.rename_keys' do
|
80
|
+
it 'returns a new hash with applied functions' do
|
81
|
+
map = described_class.t(:rename_keys, 'foo' => :foo)
|
82
|
+
|
83
|
+
input = { 'foo' => 'bar', :bar => 'baz' }.freeze
|
84
|
+
output = { foo: 'bar', bar: 'baz' }
|
85
|
+
|
86
|
+
expect(map[input]).to eql(output)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'only renames keys and never creates new ones' do
|
90
|
+
map = described_class.t(:rename_keys, 'foo' => :foo, 'bar' => :bar)
|
91
|
+
|
92
|
+
input = { 'bar' => 'baz' }.freeze
|
93
|
+
output = { bar: 'baz' }
|
94
|
+
|
95
|
+
expect(map[input]).to eql(output)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
it { expect(described_class).not_to be_contain(:rename_keys!) }
|
100
|
+
|
101
|
+
describe '.copy_keys' do
|
102
|
+
context 'with single destination key' do
|
103
|
+
it 'returns a new hash with applied functions' do
|
104
|
+
map = described_class.t(:copy_keys, 'foo' => :foo)
|
105
|
+
|
106
|
+
input = { 'foo' => 'bar', :bar => 'baz' }.freeze
|
107
|
+
output = { 'foo' => 'bar', foo: 'bar', bar: 'baz' }
|
108
|
+
|
109
|
+
expect(map[input]).to eql(output)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
context 'with multiple destination keys' do
|
114
|
+
it 'returns a new hash with applied functions' do
|
115
|
+
map = described_class.t(:copy_keys, 'foo' => [:foo, :baz])
|
116
|
+
|
117
|
+
input = { 'foo' => 'bar', :bar => 'baz' }.freeze
|
118
|
+
output = { 'foo' => 'bar', foo: 'bar', baz: 'bar', bar: 'baz' }
|
119
|
+
|
120
|
+
expect(map[input]).to eql(output)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it { expect(described_class).not_to be_contain(:copy_keys!) }
|
126
|
+
|
127
|
+
describe '.map_value' do
|
128
|
+
it 'applies function to value under specified key' do
|
129
|
+
transformation =
|
130
|
+
described_class.t(:map_value, :user, described_class.t(:symbolize_keys))
|
131
|
+
|
132
|
+
input = { user: { 'name' => 'Jane' }.freeze }.freeze
|
133
|
+
output = { user: { name: 'Jane' } }
|
134
|
+
|
135
|
+
expect(transformation[input]).to eql(output)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it { expect(described_class).not_to be_contain(:map_value!) }
|
140
|
+
|
141
|
+
describe '.nest' do
|
142
|
+
it 'returns new hash with keys nested under a new key' do
|
143
|
+
nest = described_class.t(:nest, :baz, ['foo'])
|
144
|
+
|
145
|
+
input = { 'foo' => 'bar' }.freeze
|
146
|
+
output = { baz: { 'foo' => 'bar' } }
|
147
|
+
|
148
|
+
expect(nest[input]).to eql(output)
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'returns new hash with keys nested under the existing key' do
|
152
|
+
nest = described_class.t(:nest, :baz, ['two'])
|
153
|
+
|
154
|
+
input = {
|
155
|
+
'foo' => 'bar',
|
156
|
+
baz: { 'one' => nil }.freeze,
|
157
|
+
'two' => false
|
158
|
+
}.freeze
|
159
|
+
|
160
|
+
output = { 'foo' => 'bar', baz: { 'one' => nil, 'two' => false } }
|
161
|
+
|
162
|
+
expect(nest[input]).to eql(output)
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'rewrites the existing key if its value is not a hash' do
|
166
|
+
nest = described_class.t(:nest, :baz, ['two'])
|
167
|
+
|
168
|
+
input = { 'foo' => 'bar', baz: 'one', 'two' => false }.freeze
|
169
|
+
output = { 'foo' => 'bar', baz: { 'two' => false } }
|
170
|
+
|
171
|
+
expect(nest[input]).to eql(output)
|
172
|
+
end
|
173
|
+
|
174
|
+
it 'returns new hash with an empty hash under a new key when nest-keys are missing' do
|
175
|
+
nest = described_class.t(:nest, :baz, ['foo'])
|
176
|
+
|
177
|
+
input = { 'bar' => 'foo' }.freeze
|
178
|
+
output = { 'bar' => 'foo', baz: {} }
|
179
|
+
|
180
|
+
expect(nest[input]).to eql(output)
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
it { expect(described_class).not_to be_contain(:nest!) }
|
185
|
+
|
186
|
+
describe '.unwrap' do
|
187
|
+
it 'returns new hash with nested keys lifted to the root' do
|
188
|
+
unwrap = described_class.t(:unwrap, 'wrapped', %w(one))
|
189
|
+
|
190
|
+
input = {
|
191
|
+
'foo' => 'bar',
|
192
|
+
'wrapped' => { 'one' => nil, 'two' => false }.freeze
|
193
|
+
}.freeze
|
194
|
+
|
195
|
+
output = { 'foo' => 'bar', 'one' => nil, 'wrapped' => { 'two' => false } }
|
196
|
+
|
197
|
+
expect(unwrap[input]).to eql(output)
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'lifts all keys if none are passed' do
|
201
|
+
unwrap = described_class.t(:unwrap, 'wrapped')
|
202
|
+
|
203
|
+
input = { 'wrapped' => { 'one' => nil, 'two' => false }.freeze }.freeze
|
204
|
+
output = { 'one' => nil, 'two' => false }
|
205
|
+
|
206
|
+
expect(unwrap[input]).to eql(output)
|
207
|
+
end
|
208
|
+
|
209
|
+
it 'ignores unknown keys' do
|
210
|
+
unwrap = described_class.t(:unwrap, 'wrapped', %w(one two three))
|
211
|
+
|
212
|
+
input = { 'wrapped' => { 'one' => nil, 'two' => false }.freeze }.freeze
|
213
|
+
output = { 'one' => nil, 'two' => false }
|
214
|
+
|
215
|
+
expect(unwrap[input]).to eql(output)
|
216
|
+
end
|
217
|
+
|
218
|
+
it 'prefixes unwrapped keys and retains root string type if prefix option is truthy' do
|
219
|
+
unwrap = described_class.t(:unwrap, 'wrapped', prefix: true)
|
220
|
+
|
221
|
+
input = { 'wrapped' => { one: nil, two: false }.freeze }.freeze
|
222
|
+
output = { 'wrapped_one' => nil, 'wrapped_two' => false }
|
223
|
+
|
224
|
+
expect(unwrap[input]).to eql(output)
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'prefixes unwrapped keys and retains root type if prefix option is truthy' do
|
228
|
+
unwrap = described_class.t(:unwrap, :wrapped, prefix: true)
|
229
|
+
|
230
|
+
input = { wrapped: { 'one' => nil, 'two' => false }.freeze }.freeze
|
231
|
+
output = { wrapped_one: nil, wrapped_two: false }
|
232
|
+
|
233
|
+
expect(unwrap[input]).to eql(output)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
it { expect(described_class).not_to be_contain(:unwrap!) }
|
238
|
+
|
239
|
+
describe 'nested transform' do
|
240
|
+
it 'applies functions to nested hashes' do
|
241
|
+
symbolize_keys = described_class.t(:symbolize_keys)
|
242
|
+
map_user_key = described_class.t(:map_value, :user, symbolize_keys)
|
243
|
+
|
244
|
+
transformation = symbolize_keys >> map_user_key
|
245
|
+
|
246
|
+
input = { 'user' => { 'name' => 'Jane' } }
|
247
|
+
output = { user: { name: 'Jane' } }
|
248
|
+
|
249
|
+
expect(transformation[input]).to eql(output)
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
describe 'combining transformations' do
|
254
|
+
it 'applies functions to the hash' do
|
255
|
+
symbolize_keys = described_class.t(:symbolize_keys)
|
256
|
+
map = described_class.t :rename_keys, user_name: :name, user_email: :email
|
257
|
+
|
258
|
+
transformation = symbolize_keys >> map
|
259
|
+
|
260
|
+
input = { 'user_name' => 'Jade', 'user_email' => 'jade@doe.org' }
|
261
|
+
output = { name: 'Jade', email: 'jade@doe.org' }
|
262
|
+
|
263
|
+
result = transformation[input]
|
264
|
+
|
265
|
+
expect(result).to eql(output)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
describe '.reject_keys' do
|
270
|
+
it 'returns a new hash with rejected keys' do
|
271
|
+
reject_keys = described_class.t(:reject_keys, [:name, :age])
|
272
|
+
|
273
|
+
input = { name: 'Jane', email: 'jane@doe.org', age: 21 }.freeze
|
274
|
+
output = { email: 'jane@doe.org' }
|
275
|
+
|
276
|
+
expect(reject_keys[input]).to eql(output)
|
277
|
+
end
|
278
|
+
end
|
279
|
+
|
280
|
+
it { expect(described_class).not_to be_contain(:reject_keys!) }
|
281
|
+
|
282
|
+
describe '.accept_keys' do
|
283
|
+
it 'returns a new hash with rejected keys' do
|
284
|
+
accept_keys = described_class.t(:accept_keys, [:age])
|
285
|
+
|
286
|
+
input = { name: 'Jane', email: 'jane@doe.org', age: 21 }.freeze
|
287
|
+
output = { age: 21 }
|
288
|
+
|
289
|
+
expect(accept_keys[input]).to eql(output)
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
it { expect(described_class).not_to be_contain(:accept_keys!) }
|
294
|
+
|
295
|
+
describe '.fold' do
|
296
|
+
let(:input) do
|
297
|
+
{
|
298
|
+
name: 'Jane',
|
299
|
+
tasks: [
|
300
|
+
{ title: 'be nice', priority: 1 }.freeze,
|
301
|
+
{ title: 'sleep well' }.freeze
|
302
|
+
].freeze
|
303
|
+
}.freeze
|
304
|
+
end
|
305
|
+
|
306
|
+
it 'returns a new hash with folded values' do
|
307
|
+
fold = described_class.t(:fold, :tasks, :title)
|
308
|
+
|
309
|
+
output = { name: 'Jane', tasks: ['be nice', 'sleep well'] }
|
310
|
+
|
311
|
+
expect(fold[input]).to eql(output)
|
312
|
+
end
|
313
|
+
|
314
|
+
it 'uses nil if there was not such attribute' do
|
315
|
+
fold = described_class.t(:fold, :tasks, :priority)
|
316
|
+
|
317
|
+
output = { name: 'Jane', tasks: [1, nil] }
|
318
|
+
|
319
|
+
expect(fold[input]).to eql(output)
|
320
|
+
end
|
321
|
+
end
|
322
|
+
|
323
|
+
it { expect(described_class).not_to be_contain(:fold!) }
|
324
|
+
|
325
|
+
describe '.split' do
|
326
|
+
let(:input) do
|
327
|
+
{
|
328
|
+
name: 'Joe',
|
329
|
+
tasks: [
|
330
|
+
{ title: 'sleep well', priority: 1 },
|
331
|
+
{ title: 'be nice', priority: 2 },
|
332
|
+
{ priority: 2 },
|
333
|
+
{ title: 'be cool' },
|
334
|
+
{}
|
335
|
+
]
|
336
|
+
}
|
337
|
+
end
|
338
|
+
|
339
|
+
it 'splits a tuple into array partially by given keys' do
|
340
|
+
split = described_class.t(:split, :tasks, [:priority])
|
341
|
+
|
342
|
+
output = [
|
343
|
+
{
|
344
|
+
name: 'Joe', priority: 1,
|
345
|
+
tasks: [{ title: 'sleep well' }]
|
346
|
+
},
|
347
|
+
{
|
348
|
+
name: 'Joe', priority: 2,
|
349
|
+
tasks: [{ title: 'be nice' }, { title: nil }]
|
350
|
+
},
|
351
|
+
{
|
352
|
+
name: 'Joe', priority: nil,
|
353
|
+
tasks: [{ title: 'be cool' }, { title: nil }]
|
354
|
+
}
|
355
|
+
]
|
356
|
+
|
357
|
+
expect(split[input]).to eql output
|
358
|
+
end
|
359
|
+
|
360
|
+
it 'splits a tuple into array fully by all subkeys' do
|
361
|
+
split = described_class.t(:split, :tasks, [:priority, :title])
|
362
|
+
|
363
|
+
output = [
|
364
|
+
{ name: 'Joe', title: 'sleep well', priority: 1 },
|
365
|
+
{ name: 'Joe', title: 'be nice', priority: 2 },
|
366
|
+
{ name: 'Joe', title: nil, priority: 2 },
|
367
|
+
{ name: 'Joe', title: 'be cool', priority: nil },
|
368
|
+
{ name: 'Joe', title: nil, priority: nil }
|
369
|
+
]
|
370
|
+
|
371
|
+
expect(split[input]).to eql output
|
372
|
+
end
|
373
|
+
|
374
|
+
it 'returns an array of one tuple with updated keys when there is nothing to split by' do
|
375
|
+
output = [
|
376
|
+
{
|
377
|
+
name: 'Joe',
|
378
|
+
tasks: [
|
379
|
+
{ title: 'sleep well', priority: 1 },
|
380
|
+
{ title: 'be nice', priority: 2 },
|
381
|
+
{ title: nil, priority: 2 },
|
382
|
+
{ title: 'be cool', priority: nil },
|
383
|
+
{ title: nil, priority: nil }
|
384
|
+
]
|
385
|
+
}
|
386
|
+
]
|
387
|
+
|
388
|
+
split = described_class.t(:split, :tasks, [])
|
389
|
+
expect(split[input]).to eql output
|
390
|
+
|
391
|
+
split = described_class.t(:split, :tasks, [:absent])
|
392
|
+
expect(split[input]).to eql output
|
393
|
+
end
|
394
|
+
|
395
|
+
it 'returns an array of initial tuple when attribute is absent' do
|
396
|
+
split = described_class.t(:split, :absent, [:priority, :title])
|
397
|
+
expect(split[input]).to eql [input]
|
398
|
+
end
|
399
|
+
|
400
|
+
it 'ignores empty array' do
|
401
|
+
input = { name: 'Joe', tasks: [] }
|
402
|
+
|
403
|
+
split = described_class.t(:split, :tasks, [:title])
|
404
|
+
|
405
|
+
expect(split[input]).to eql [{ name: 'Joe' }]
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
describe ':eval_values' do
|
410
|
+
it 'recursively evaluates values' do
|
411
|
+
evaluate = described_class.t(:eval_values, 1)
|
412
|
+
|
413
|
+
input = {
|
414
|
+
one: 1, two: -> i { i + 1 },
|
415
|
+
three: -> i { i + 2 }, four: 4,
|
416
|
+
more: [{ one: -> i { i }, two: 2 }]
|
417
|
+
}
|
418
|
+
|
419
|
+
output = {
|
420
|
+
one: 1, two: 2,
|
421
|
+
three: 3, four: 4,
|
422
|
+
more: [{ one: 1, two: 2 }]
|
423
|
+
}
|
424
|
+
|
425
|
+
expect(evaluate[input]).to eql(output)
|
426
|
+
end
|
427
|
+
|
428
|
+
it 'recursively evaluates values matching key names' do
|
429
|
+
evaluate = described_class.t(:eval_values, 1, [:one, :two])
|
430
|
+
|
431
|
+
input = {
|
432
|
+
one: 1, two: -> i { i + 1 },
|
433
|
+
three: -> i { i + 2 }, four: 4,
|
434
|
+
array: [{ one: -> i { i }, two: 2 }],
|
435
|
+
hash: { one: -> i { i } }
|
436
|
+
}
|
437
|
+
|
438
|
+
result = evaluate[input]
|
439
|
+
|
440
|
+
expect(result[:three]).to be_a(Proc)
|
441
|
+
expect(result).to include(two: 2)
|
442
|
+
expect(result[:array]).to eql([{ one: 1, two: 2 }])
|
443
|
+
expect(result[:hash]).to eql(one: 1)
|
444
|
+
end
|
445
|
+
end
|
446
|
+
|
447
|
+
describe '.deep_merge' do
|
448
|
+
let(:hash) {
|
449
|
+
{
|
450
|
+
name: 'Jane',
|
451
|
+
email: 'jane@doe.org',
|
452
|
+
favorites:
|
453
|
+
{
|
454
|
+
food: 'stroopwafel'
|
455
|
+
}
|
456
|
+
}
|
457
|
+
}
|
458
|
+
|
459
|
+
let(:update) {
|
460
|
+
{
|
461
|
+
email: 'jane@example.org',
|
462
|
+
favorites:
|
463
|
+
{
|
464
|
+
color: 'orange'
|
465
|
+
}
|
466
|
+
}
|
467
|
+
}
|
468
|
+
|
469
|
+
it 'recursively merges hash values' do
|
470
|
+
deep_merge = described_class.t(:deep_merge)
|
471
|
+
output = {
|
472
|
+
name: 'Jane',
|
473
|
+
email: 'jane@example.org',
|
474
|
+
favorites: { food: 'stroopwafel', color: 'orange' }
|
475
|
+
}
|
476
|
+
|
477
|
+
expect(deep_merge[hash, update]).to eql(output)
|
478
|
+
end
|
479
|
+
|
480
|
+
it 'does not alter the provided arguments' do
|
481
|
+
original_hash = hash.dup
|
482
|
+
original_update = update.dup
|
483
|
+
|
484
|
+
described_class.t(:deep_merge)[hash, update]
|
485
|
+
|
486
|
+
expect(hash).to eql(original_hash)
|
487
|
+
expect(update).to eql(original_update)
|
488
|
+
end
|
489
|
+
end
|
490
|
+
end
|