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.
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