dry-transformer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +12 -0
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +66 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +16 -0
  10. data/.rspec +4 -0
  11. data/.rubocop.yml +95 -0
  12. data/CHANGELOG.md +3 -0
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +19 -0
  16. data/LICENSE +20 -0
  17. data/README.md +29 -0
  18. data/Rakefile +6 -0
  19. data/docsite/source/built-in-transformations.html.md +47 -0
  20. data/docsite/source/index.html.md +15 -0
  21. data/docsite/source/transformation-objects.html.md +32 -0
  22. data/docsite/source/using-standalone-functions.html.md +82 -0
  23. data/dry-transformer.gemspec +22 -0
  24. data/lib/dry-transformer.rb +3 -0
  25. data/lib/dry/transformer.rb +23 -0
  26. data/lib/dry/transformer/all.rb +11 -0
  27. data/lib/dry/transformer/array.rb +183 -0
  28. data/lib/dry/transformer/array/combine.rb +65 -0
  29. data/lib/dry/transformer/class.rb +56 -0
  30. data/lib/dry/transformer/coercions.rb +196 -0
  31. data/lib/dry/transformer/compiler.rb +47 -0
  32. data/lib/dry/transformer/composite.rb +54 -0
  33. data/lib/dry/transformer/conditional.rb +76 -0
  34. data/lib/dry/transformer/constants.rb +7 -0
  35. data/lib/dry/transformer/error.rb +16 -0
  36. data/lib/dry/transformer/function.rb +109 -0
  37. data/lib/dry/transformer/hash.rb +453 -0
  38. data/lib/dry/transformer/pipe.rb +75 -0
  39. data/lib/dry/transformer/pipe/class_interface.rb +115 -0
  40. data/lib/dry/transformer/pipe/dsl.rb +58 -0
  41. data/lib/dry/transformer/proc.rb +46 -0
  42. data/lib/dry/transformer/recursion.rb +121 -0
  43. data/lib/dry/transformer/registry.rb +150 -0
  44. data/lib/dry/transformer/store.rb +128 -0
  45. data/lib/dry/transformer/version.rb +7 -0
  46. data/spec/spec_helper.rb +31 -0
  47. data/spec/unit/array/combine_spec.rb +224 -0
  48. data/spec/unit/array_transformations_spec.rb +233 -0
  49. data/spec/unit/class_transformations_spec.rb +50 -0
  50. data/spec/unit/coercions_spec.rb +132 -0
  51. data/spec/unit/conditional_spec.rb +48 -0
  52. data/spec/unit/function_not_found_error_spec.rb +12 -0
  53. data/spec/unit/function_spec.rb +193 -0
  54. data/spec/unit/hash_transformations_spec.rb +490 -0
  55. data/spec/unit/proc_transformations_spec.rb +20 -0
  56. data/spec/unit/recursion_spec.rb +145 -0
  57. data/spec/unit/registry_spec.rb +202 -0
  58. data/spec/unit/store_spec.rb +198 -0
  59. data/spec/unit/transformer/class_interface_spec.rb +350 -0
  60. data/spec/unit/transformer/dsl_spec.rb +15 -0
  61. data/spec/unit/transformer/instance_methods_spec.rb +25 -0
  62. 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