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,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ RSpec.describe Dry::Transformer::ProcTransformations do
6
+ describe '.bind' do
7
+ let(:fn) { described_class.t(:bind, binding, proc) }
8
+ let(:binding) { OpenStruct.new(prefix: prefix) }
9
+ let(:proc) { -> v { [prefix, v].join('_') } }
10
+ let(:prefix) { 'foo' }
11
+ let(:input) { 'bar' }
12
+ let(:output) { 'foo_bar' }
13
+
14
+ subject { fn[input] }
15
+
16
+ it 'binds the given proc to the specified binding' do
17
+ is_expected.to eq(output)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Dry::Transformer::Recursion do
4
+ let(:hashes) { Dry::Transformer::HashTransformations }
5
+
6
+ describe '.recursion' do
7
+ let(:original) do
8
+ {
9
+ 'foo' => 'bar',
10
+ 'bar' => {
11
+ 'foo' => 'bar',
12
+ 'bar' => %w(foo bar baz),
13
+ 'baz' => 'foo'
14
+ },
15
+ 'baz' => 'bar'
16
+ }
17
+ end
18
+
19
+ let(:input) { original.dup }
20
+
21
+ let(:output) do
22
+ {
23
+ 'foo' => 'bar',
24
+ 'bar' => {
25
+ 'foo' => 'bar',
26
+ 'bar' => %w(foo bar)
27
+ }
28
+ }
29
+ end
30
+
31
+ context 'when function is non-destructive' do
32
+ let(:map) do
33
+ described_class.t(:recursion, -> enum {
34
+ enum.reject { |v| v == 'baz' }
35
+ })
36
+ end
37
+
38
+ it 'applies funtions to all items recursively' do
39
+ expect(map[input]).to eql(output)
40
+ expect(input).to eql(original)
41
+ end
42
+ end
43
+
44
+ context 'when function is destructive' do
45
+ let(:map) do
46
+ described_class.t(:recursion, -> enum {
47
+ enum.reject! { |v| v == 'baz' }
48
+ })
49
+ end
50
+
51
+ it 'applies funtions to all items recursively and destructively' do
52
+ expect(map[input]).to eql(output)
53
+ expect(input).to eql(output)
54
+ end
55
+ end
56
+ end
57
+
58
+ describe '.array_recursion' do
59
+ let(:original) do
60
+ [
61
+ 'foo',
62
+ 'bar',
63
+ nil,
64
+ [
65
+ 'foo',
66
+ 'bar',
67
+ nil,
68
+ [
69
+ 'foo',
70
+ 'bar',
71
+ nil
72
+ ]
73
+ ]
74
+ ]
75
+ end
76
+
77
+ let(:input) { original.dup }
78
+
79
+ let(:output) do
80
+ [
81
+ 'foo',
82
+ 'bar',
83
+ [
84
+ 'foo',
85
+ 'bar',
86
+ %w(foo bar)
87
+ ]
88
+ ]
89
+ end
90
+
91
+ context 'when function is non-destructive' do
92
+ let(:map) { described_class.t(:array_recursion, proc(&:compact)) }
93
+
94
+ it 'applies funtions to all items recursively' do
95
+ expect(map[input]).to eql(output)
96
+ expect(input).to eql(original)
97
+ end
98
+ end
99
+
100
+ context 'when function is destructive' do
101
+ let(:map) { described_class.t(:array_recursion, proc(&:compact!)) }
102
+
103
+ it 'applies funtions to all items recursively and destructively' do
104
+ expect(map[input]).to eql(output)
105
+ expect(input).to eql(output)
106
+ end
107
+ end
108
+ end
109
+
110
+ describe '.hash_recursion' do
111
+ let(:input) do
112
+ {
113
+ 'foo' => 'bar',
114
+ 'bar' => {
115
+ 'foo' => 'bar',
116
+ 'bar' => {
117
+ 'foo' => 'bar'
118
+ }.freeze
119
+ }.freeze,
120
+ 'baz' => 'bar'
121
+ }.freeze
122
+ end
123
+
124
+ let(:output) do
125
+ {
126
+ foo: 'bar',
127
+ bar: {
128
+ foo: 'bar',
129
+ bar: {
130
+ foo: 'bar'
131
+ }
132
+ },
133
+ baz: 'bar'
134
+ }
135
+ end
136
+
137
+ let(:map) do
138
+ described_class.t(:hash_recursion, hashes.t(:symbolize_keys))
139
+ end
140
+
141
+ it 'applies funtions to all values recursively' do
142
+ expect(map[input]).to eql(output)
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Dry::Transformer::Registry do
4
+ let(:foo) do
5
+ Test::Foo = Module.new do
6
+ extend Dry::Transformer::Registry
7
+
8
+ def self.prefix(value, prefix)
9
+ "#{prefix}_#{value}"
10
+ end
11
+ end
12
+ end
13
+
14
+ let(:bar) { Test::Bar = Module.new { extend Dry::Transformer::Registry } }
15
+ let(:baz) { Test::Baz = Module.new { extend Dry::Transformer::Registry } }
16
+
17
+ describe '.[]' do
18
+ subject(:transproc) { foo[fn, 'baz'] }
19
+
20
+ context 'from a method' do
21
+ let(:fn) { :prefix }
22
+
23
+ it 'builds a function from a method' do
24
+ expect(transproc['qux']).to eql 'baz_qux'
25
+ end
26
+ end
27
+
28
+ context 'from a closure' do
29
+ let(:fn) { -> value, prefix { [prefix, '_', value].join } }
30
+
31
+ it 'builds a function from a method' do
32
+ expect(transproc['qux']).to eql 'baz_qux'
33
+ end
34
+ end
35
+ end
36
+
37
+ describe '.t' do
38
+ subject(:transproc) { foo.t(:prefix, 'baz') }
39
+
40
+ it 'is an alias for .[]' do
41
+ expect(transproc['qux']).to eql 'baz_qux'
42
+ end
43
+ end
44
+
45
+ describe '.contain?' do
46
+ context 'with absent function' do
47
+ it { expect(foo.contain?(:something)).to be false }
48
+ end
49
+
50
+ context 'with class method' do
51
+ it { expect(foo.contain?(:prefix)).to be true }
52
+ end
53
+
54
+ context 'with imported methods' do
55
+ before { bar.import foo }
56
+
57
+ it { expect(bar.contain?(:prefix)).to be true }
58
+ end
59
+ end
60
+
61
+ describe '.register' do
62
+ it { expect(foo).not_to be_contain(:increment) }
63
+
64
+ it { expect(foo).to be_contain(:prefix) }
65
+
66
+ def do_register
67
+ foo.register(:increment, ->(v) { v + 1 })
68
+ end
69
+
70
+ it 'returns self' do
71
+ expect(do_register).to eq foo
72
+ end
73
+
74
+ it 'registers function' do
75
+ do_register
76
+ expect(foo).to be_contain(:increment)
77
+ end
78
+
79
+ it 'preserves previous functions' do
80
+ do_register
81
+ expect(foo).to be_contain(:prefix)
82
+ end
83
+
84
+ it 'makes function available' do
85
+ do_register
86
+ expect(foo[:increment][2]).to eq 3
87
+ end
88
+
89
+ it 'rejects to overwrite existing' do
90
+ expect { foo.register(:prefix) {} }
91
+ .to raise_error(Dry::Transformer::FunctionAlreadyRegisteredError)
92
+ end
93
+
94
+ it 'registers and fetches transproc function' do
95
+ function = foo[:prefix, '1']
96
+ foo.register(:prefix_one, function)
97
+
98
+ expect(foo[:prefix_one]).to eq function
99
+ end
100
+
101
+ it 'allows to overwrite function arguments' do
102
+ foo.register(:map_array, Dry::Transformer::ArrayTransformations.t(:map_array))
103
+
104
+ fn = foo[:map_array, ->(value) { value.to_sym }]
105
+
106
+ expect(fn.call(%w(a b c))).to eq([:a, :b, :c])
107
+ end
108
+
109
+ it 'registers and fetches composite' do
110
+ composite = foo[:prefix, '1'] + foo[:prefix, '2']
111
+ foo.register(:double_prefix, composite)
112
+
113
+ expect(foo[:double_prefix]).to eq composite
114
+ end
115
+
116
+ context 'with block argument' do
117
+ def do_register
118
+ foo.register(:increment) { |v| v + 1 }
119
+ end
120
+
121
+ it 'registers function' do
122
+ do_register
123
+ expect(foo).to be_contain(:increment)
124
+ end
125
+
126
+ it 'makes function available' do
127
+ do_register
128
+ expect(foo[:increment][2]).to eq 3
129
+ end
130
+ end
131
+ end
132
+
133
+ describe '.import' do
134
+ context 'a module' do
135
+ subject(:import) { bar.import foo }
136
+
137
+ it 'registers all its methods' do
138
+ import
139
+ expect(bar[:prefix, 'baz']['qux']).to eql 'baz_qux'
140
+ end
141
+
142
+ it 'returns itself' do
143
+ expect(import).to eq bar
144
+ end
145
+ end
146
+
147
+ context 'a method' do
148
+ before { bar.import :prefix, from: foo }
149
+
150
+ it 'registers a transproc' do
151
+ expect(bar[:prefix, 'bar']['baz']).to eql 'bar_baz'
152
+ end
153
+ end
154
+
155
+ context 'an imported method' do
156
+ before do
157
+ bar.import :prefix, from: foo, as: :affix
158
+ baz.import :affix, from: bar
159
+ end
160
+
161
+ it 'registers a transproc' do
162
+ expect(baz[:affix, 'bar']['baz']).to eql 'bar_baz'
163
+ end
164
+ end
165
+
166
+ context 'a list of methods' do
167
+ before { bar.import :prefix, from: foo }
168
+ before { bar.import :prefix, from: foo, as: :affix }
169
+ before { baz.import :prefix, :affix, from: bar }
170
+
171
+ it 'registers a transproc' do
172
+ expect(baz[:prefix, 'bar']['baz']).to eql 'bar_baz'
173
+ expect(baz[:affix, 'bar']['baz']).to eql 'bar_baz'
174
+ end
175
+ end
176
+
177
+ context 'a renamed method' do
178
+ before { bar.import :prefix, from: foo, as: :affix }
179
+
180
+ it 'registers a transproc under the new name' do
181
+ expect(bar[:affix, 'bar']['baz']).to eql 'bar_baz'
182
+ end
183
+ end
184
+
185
+ context 'an unknown method' do
186
+ it 'fails' do
187
+ expect { bar.import :suffix, from: foo }.to raise_error do |error|
188
+ expect(error).to be_kind_of Dry::Transformer::FunctionNotFoundError
189
+ expect(error.message).to include 'Foo[:suffix]'
190
+ end
191
+ end
192
+ end
193
+ end
194
+
195
+ describe '.uses' do
196
+ before { bar.uses foo }
197
+
198
+ it 'is an alias for .import' do
199
+ expect(bar[:prefix, 'baz']['qux']).to eql 'baz_qux'
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Dry::Transformer::Store do
4
+ let(:store) { described_class.new methods }
5
+ let(:methods) { { foo: instance_double(Proc) } }
6
+
7
+ describe '.new' do
8
+ it 'is immutable' do
9
+ expect(store).to be_frozen
10
+ end
11
+
12
+ it 'does not freeze the source hash' do
13
+ expect { store }.not_to change { methods.frozen? }
14
+ end
15
+ end # describe .new
16
+
17
+ describe '#methods' do
18
+ it 'returns the hash from the initializer' do
19
+ expect(store.methods).to eql methods
20
+ end
21
+
22
+ it 'returns empty hash by default' do
23
+ expect(described_class.new.methods).to eql({})
24
+ end
25
+
26
+ it 'is immutable' do
27
+ expect(store.methods).to be_frozen
28
+ end
29
+ end # describe #methods
30
+
31
+ describe '#fetch' do
32
+ it 'returns a registered proc' do
33
+ expect(store.fetch(:foo)).to eql methods[:foo]
34
+ end
35
+
36
+ it 'does not accepts anything but symbol as key' do
37
+ expect { store.fetch('foo') }.to raise_error KeyError
38
+ end
39
+
40
+ it 'raises KeyError if requested proc is unknown' do
41
+ expect { store.fetch(:bar) }.to raise_error KeyError
42
+ end
43
+ end # describe #fetch
44
+
45
+ describe '#contain?' do
46
+ it 'returns true for registered proc' do
47
+ expect(store.contain?(:foo)).to be true
48
+ end
49
+
50
+ it 'returns false if requested proc is unknown' do
51
+ expect(store.contain?(:bar)).to be false
52
+ end
53
+ end # describe #fetch
54
+
55
+ describe '#register' do
56
+ subject { new_store }
57
+ let(:new_store) { store.register(:increment, ->(v) { v + 1 }) }
58
+
59
+ it { is_expected.to be_contain(:increment) }
60
+
61
+ it { expect(new_store.fetch(:increment)[2]).to eq 3 }
62
+
63
+ it 'preserves existing methods' do
64
+ expect(new_store).to be_contain(:foo)
65
+ end
66
+
67
+ context 'with block argument' do
68
+ let(:new_store) { store.register(:increment) { |v| v + 1 } }
69
+
70
+ it 'works as well as with proc' do
71
+ expect(new_store.fetch(:increment)[2]).to eq 3
72
+ end
73
+ end
74
+ end
75
+
76
+ describe '#import', :focus do
77
+ before do
78
+ module Bar
79
+ def self.bar
80
+ :bar
81
+ end
82
+ end
83
+
84
+ module Baz
85
+ def self.baz
86
+ :baz
87
+ end
88
+ end
89
+
90
+ module Qux
91
+ extend Dry::Transformer::Registry
92
+
93
+ import Bar
94
+ import Baz
95
+
96
+ def self.baz
97
+ :qux_baz
98
+ end
99
+
100
+ def self.qux
101
+ :qux
102
+ end
103
+ end
104
+ end
105
+
106
+ shared_examples :importing_method do
107
+ let(:preserved) { subject.methods[:foo] }
108
+ let(:imported) { subject.methods[key] }
109
+
110
+ it '[preserves old methods]' do
111
+ expect(preserved).to eql(methods[:foo])
112
+ end
113
+
114
+ it '[registers a new method]' do
115
+ expect(imported).to be_kind_of Method
116
+ expect(imported.call).to eql(value)
117
+ end
118
+ end
119
+
120
+ context 'named method' do
121
+ subject { store.import 'qux', from: Qux }
122
+
123
+ it_behaves_like :importing_method do
124
+ let(:key) { :qux }
125
+ let(:value) { :qux }
126
+ end
127
+ end
128
+
129
+ context 'named methods' do
130
+ subject { store.import 'qux', 'bar', from: Qux }
131
+
132
+ it_behaves_like :importing_method do
133
+ let(:key) { :qux }
134
+ let(:value) { :qux }
135
+ end
136
+
137
+ it_behaves_like :importing_method do
138
+ let(:key) { :bar }
139
+ let(:value) { :bar }
140
+ end
141
+ end
142
+
143
+ context 'renamed method' do
144
+ subject { store.import 'qux', from: Qux, as: 'quxx' }
145
+
146
+ it_behaves_like :importing_method do
147
+ let(:key) { :quxx }
148
+ let(:value) { :qux }
149
+ end
150
+ end
151
+
152
+ context 'imported proc' do
153
+ subject { store.import 'bar', from: Qux, as: 'barr' }
154
+
155
+ it_behaves_like :importing_method do
156
+ let(:key) { :barr }
157
+ let(:value) { :bar }
158
+ end
159
+ end
160
+
161
+ context 'method that reloads imported proc' do
162
+ subject { store.import 'baz', from: Qux, as: 'bazz' }
163
+
164
+ it_behaves_like :importing_method do
165
+ let(:key) { :bazz }
166
+ let(:value) { :qux_baz }
167
+ end
168
+ end
169
+
170
+ context 'module' do
171
+ subject { store.import Qux }
172
+
173
+ it_behaves_like :importing_method do
174
+ let(:key) { :bar }
175
+ let(:value) { :bar }
176
+ end
177
+
178
+ it_behaves_like :importing_method do
179
+ let(:key) { :baz }
180
+ let(:value) { :qux_baz }
181
+ end
182
+
183
+ it_behaves_like :importing_method do
184
+ let(:key) { :qux }
185
+ let(:value) { :qux }
186
+ end
187
+
188
+ it 'skips Dry::Transformer::Registry singleton methods' do
189
+ pending "this fails for some reason" if RUBY_ENGINE == "jruby"
190
+ expect(subject.methods.keys).to contain_exactly(:foo, :bar, :baz, :qux)
191
+ end
192
+ end
193
+
194
+ after do
195
+ %w(Bar Baz Qux).each { |name| Object.send :remove_const, name }
196
+ end
197
+ end # describe #import
198
+ end # describe Dry::Transformer::Store