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