transproc 1.0.3 → 1.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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +3 -0
  4. data/.travis.yml +10 -10
  5. data/CHANGELOG.md +14 -2
  6. data/Gemfile +5 -17
  7. data/lib/transproc.rb +3 -4
  8. data/lib/transproc/all.rb +2 -0
  9. data/lib/transproc/array.rb +2 -0
  10. data/lib/transproc/array/combine.rb +2 -0
  11. data/lib/transproc/class.rb +2 -0
  12. data/lib/transproc/coercions.rb +2 -0
  13. data/lib/transproc/compiler.rb +45 -0
  14. data/lib/transproc/composer.rb +2 -0
  15. data/lib/transproc/composite.rb +2 -0
  16. data/lib/transproc/conditional.rb +2 -0
  17. data/lib/transproc/constants.rb +5 -0
  18. data/lib/transproc/error.rb +2 -0
  19. data/lib/transproc/function.rb +2 -0
  20. data/lib/transproc/functions.rb +2 -0
  21. data/lib/transproc/hash.rb +31 -0
  22. data/lib/transproc/proc.rb +2 -0
  23. data/lib/transproc/recursion.rb +2 -0
  24. data/lib/transproc/registry.rb +1 -0
  25. data/lib/transproc/store.rb +1 -0
  26. data/lib/transproc/support/deprecations.rb +2 -0
  27. data/lib/transproc/transformer.rb +7 -1
  28. data/lib/transproc/transformer/class_interface.rb +31 -65
  29. data/lib/transproc/transformer/deprecated/class_interface.rb +80 -0
  30. data/lib/transproc/transformer/dsl.rb +51 -0
  31. data/lib/transproc/version.rb +3 -1
  32. data/spec/spec_helper.rb +5 -7
  33. data/spec/unit/array/combine_spec.rb +3 -1
  34. data/spec/unit/array_transformations_spec.rb +1 -1
  35. data/spec/unit/class_transformations_spec.rb +9 -6
  36. data/spec/unit/coercions_spec.rb +1 -1
  37. data/spec/unit/composer_spec.rb +1 -1
  38. data/spec/unit/conditional_spec.rb +1 -1
  39. data/spec/unit/function_not_found_error_spec.rb +1 -1
  40. data/spec/unit/function_spec.rb +1 -1
  41. data/spec/unit/hash_transformations_spec.rb +12 -1
  42. data/spec/unit/proc_transformations_spec.rb +3 -1
  43. data/spec/unit/recursion_spec.rb +1 -1
  44. data/spec/unit/registry_spec.rb +1 -1
  45. data/spec/unit/store_spec.rb +2 -1
  46. data/spec/unit/transformer/class_interface_spec.rb +364 -0
  47. data/spec/unit/transformer/dsl_spec.rb +15 -0
  48. data/spec/unit/transformer/instance_methods_spec.rb +25 -0
  49. data/spec/unit/transformer_spec.rb +128 -40
  50. data/spec/unit/transproc_spec.rb +1 -1
  51. data/transproc.gemspec +0 -4
  52. metadata +15 -53
  53. data/.rubocop.yml +0 -66
  54. data/.rubocop_todo.yml +0 -11
  55. data/rakelib/mutant.rake +0 -16
  56. data/rakelib/rubocop.rake +0 -18
  57. data/spec/support/mutant.rb +0 -10
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transproc
4
+ class Transformer
5
+ module Deprecated
6
+ # @api public
7
+ module ClassInterface
8
+ # @api public
9
+ def new(*args)
10
+ super(*args).tap do |transformer|
11
+ transformer.instance_variable_set('@transproc', transproc) if transformations.any?
12
+ end
13
+ end
14
+
15
+ # @api private
16
+ def inherited(subclass)
17
+ super
18
+
19
+ if transformations.any?
20
+ subclass.instance_variable_set('@transformations', transformations.dup)
21
+ end
22
+ end
23
+
24
+ # Define an anonymous transproc derived from given Transformer
25
+ # Evaluates block with transformations and returns initialized transproc.
26
+ # Does not mutate original Transformer
27
+ #
28
+ # @example
29
+ #
30
+ # class MyTransformer < Transproc::Transformer[MyContainer]
31
+ # end
32
+ #
33
+ # transproc = MyTransformer.define do
34
+ # map_values t(:to_string)
35
+ # end
36
+ # transproc.call(a: 1, b: 2)
37
+ # # => {a: '1', b: '2'}
38
+ #
39
+ # @yield Block allowing to define transformations. The same as class level DSL
40
+ #
41
+ # @return [Function] Composed transproc
42
+ #
43
+ # @api public
44
+ def define(&block)
45
+ return transproc unless block_given?
46
+
47
+ Class.new(Transformer[container]).tap { |klass| klass.instance_eval(&block) }.transproc
48
+ end
49
+ alias build define
50
+
51
+ # @api private
52
+ def method_missing(method, *args, &block)
53
+ super unless container.contain?(method)
54
+ func = block ? t(method, *args, define(&block)) : t(method, *args)
55
+ transformations << func
56
+ func
57
+ end
58
+
59
+ # @api private
60
+ def respond_to_missing?(method, _include_private = false)
61
+ super || container.contain?(method)
62
+ end
63
+
64
+ # @api private
65
+ def transproc
66
+ transformations.reduce(:>>)
67
+ end
68
+
69
+ private
70
+
71
+ # An array containing the transformation pipeline
72
+ #
73
+ # @api private
74
+ def transformations
75
+ @transformations ||= []
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'transproc/compiler'
4
+
5
+ module Transproc
6
+ class Transformer
7
+ # @api public
8
+ class DSL
9
+ # @api private
10
+ attr_reader :container
11
+
12
+ # @api private
13
+ attr_reader :ast
14
+
15
+ # @api private
16
+ def initialize(container, ast: [], &block)
17
+ @container = container
18
+ @ast = ast
19
+ instance_eval(&block) if block
20
+ end
21
+
22
+ # @api private
23
+ def dup
24
+ self.class.new(container, ast: ast.dup)
25
+ end
26
+
27
+ # @api private
28
+ def call(transformer)
29
+ Compiler.new(container, transformer).(ast)
30
+ end
31
+
32
+ private
33
+
34
+ # @api private
35
+ def node(&block)
36
+ [:t, self.class.new(container, &block).ast]
37
+ end
38
+
39
+ # @api private
40
+ def respond_to_missing?(method, _include_private = false)
41
+ super || container.contain?(method)
42
+ end
43
+
44
+ # @api private
45
+ def method_missing(meth, *args, &block)
46
+ arg_nodes = *args.map { |a| [:arg, a] }
47
+ ast << [:fn, (block ? [meth, [*arg_nodes, node(&block)]] : [meth, arg_nodes])]
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
- VERSION = '1.0.3'.freeze
4
+ VERSION = '1.1.0'.freeze
3
5
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  if RUBY_ENGINE == 'ruby' && ENV['COVERAGE'] == 'true'
2
4
  require 'yaml'
3
5
  rubies = YAML.load(File.read(File.join(__dir__, '..', '.travis.yml')))['rvm']
@@ -11,15 +13,11 @@ if RUBY_ENGINE == 'ruby' && ENV['COVERAGE'] == 'true'
11
13
  end
12
14
  end
13
15
 
14
- require 'equalizer'
15
- require 'anima'
16
- require 'ostruct'
17
- require 'transproc/all'
18
-
19
16
  begin
20
17
  require 'byebug'
21
- rescue LoadError
22
- end
18
+ rescue LoadError;end
19
+
20
+ require 'transproc/all'
23
21
 
24
22
  root = Pathname(__FILE__).dirname
25
23
  Dir[root.join('support/*.rb').to_s].each { |f| require f }
@@ -1,4 +1,6 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
2
4
 
3
5
  describe Transproc::ArrayTransformations do
4
6
  describe '.combine' do
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::ArrayTransformations do
4
4
  let(:hashes) { Transproc::HashTransformations }
@@ -1,9 +1,11 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/equalizer'
2
4
 
3
5
  describe Transproc::ClassTransformations do
4
6
  describe '.constructor_inject' do
5
7
  let(:klass) do
6
- Struct.new(:name, :age) { include Equalizer.new(:name, :age) }
8
+ Struct.new(:name, :age) { include Dry::Equalizer.new(:name, :age) }
7
9
  end
8
10
 
9
11
  it 'returns a new object initialized with the given arguments' do
@@ -21,12 +23,13 @@ describe Transproc::ClassTransformations do
21
23
  describe '.set_ivars' do
22
24
  let(:klass) do
23
25
  Class.new do
24
- include Anima.new(:name, :age)
26
+ include Dry::Equalizer.new(:name, :age)
25
27
 
26
- attr_reader :test
28
+ attr_reader :name, :age, :test
27
29
 
28
- def initialize(*args)
29
- super
30
+ def initialize(name:, age:)
31
+ @name = name
32
+ @age = age
30
33
  @test = true
31
34
  end
32
35
  end
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::Coercions do
4
4
  describe '.identity' do
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::Composer do
4
4
  before do
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::Conditional do
4
4
  describe '.not' do
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::FunctionNotFoundError do
4
4
  it 'complains that the function not registered' do
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::Function do
4
4
  let(:container) do
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::HashTransformations do
4
4
  describe '.map_keys' do
@@ -50,6 +50,17 @@ describe Transproc::HashTransformations do
50
50
  end
51
51
  end
52
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
+
53
64
  it { expect(described_class).not_to be_contain(:stringify_keys!) }
54
65
 
55
66
  describe '.map_values' do
@@ -1,4 +1,6 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
2
4
 
3
5
  describe Transproc::ProcTransformations do
4
6
  describe '.bind' do
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::Recursion do
4
4
  let(:hashes) { Transproc::HashTransformations }
@@ -1,4 +1,4 @@
1
- require 'spec_helper'
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::Registry do
4
4
  before { module Transproc::Test; end }
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  describe Transproc::Store do
4
4
  let(:store) { described_class.new methods }
@@ -186,6 +186,7 @@ describe Transproc::Store do
186
186
  end
187
187
 
188
188
  it 'skips Transproc::Registry singleton methods' do
189
+ pending "this fails for some reason" if RUBY_ENGINE == "jruby"
189
190
  expect(subject.methods.keys).to contain_exactly(:foo, :bar, :baz, :qux)
190
191
  end
191
192
  end
@@ -0,0 +1,364 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+ require 'dry/equalizer'
5
+
6
+ describe Transproc::Transformer do
7
+ let(:container) { Module.new { extend Transproc::Registry } }
8
+ let(:klass) { Transproc::Transformer[container] }
9
+ let(:transformer) { klass.new }
10
+
11
+ describe '.import' do
12
+ it 'allows importing functions into an auto-configured registry' do
13
+ klass = Class.new(Transproc::Transformer) do
14
+ import Transproc::ArrayTransformations
15
+ import Transproc::Coercions
16
+
17
+ define! do
18
+ map_array(&:to_symbol)
19
+ end
20
+ end
21
+
22
+ transformer = klass.new
23
+
24
+ expect(transformer.(['foo', 'bar'])).to eql([:foo, :bar])
25
+ end
26
+ end
27
+
28
+ describe '.new' do
29
+ it 'supports arguments' do
30
+ klass = Class.new(Transproc::Transformer) do
31
+ import Transproc::ArrayTransformations
32
+ import Transproc::Coercions
33
+
34
+ define! do
35
+ map_array(&:to_symbol)
36
+ end
37
+
38
+ def initialize(good)
39
+ @good = good
40
+ end
41
+
42
+ def good?
43
+ @good
44
+ end
45
+ end
46
+
47
+ transformer = klass.new(true)
48
+
49
+ expect(transformer).to be_good
50
+ end
51
+ end
52
+
53
+ describe '.container' do
54
+ it 'returns the configured container' do
55
+ expect(klass.container).to be(container)
56
+ end
57
+
58
+ context 'with setter argument' do
59
+ let(:container) { double(:custom_container) }
60
+
61
+ it 'sets and returns the container' do
62
+ klass.container(container)
63
+
64
+ expect(klass.container).to be(container)
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'inheritance' do
70
+ let(:container) do
71
+ Module.new do
72
+ extend Transproc::Registry
73
+
74
+ def self.arbitrary(value, fn)
75
+ fn[value]
76
+ end
77
+ end
78
+ end
79
+
80
+ let(:superclass) do
81
+ Class.new(Transproc::Transformer[container]) do
82
+ define! do
83
+ arbitrary ->(v) { v + 1 }
84
+ end
85
+ end
86
+ end
87
+
88
+ let(:subclass) do
89
+ Class.new(superclass) do
90
+ define! do
91
+ arbitrary ->(v) { v * 2 }
92
+ end
93
+ end
94
+ end
95
+
96
+ it 'inherits container from superclass' do
97
+ expect(subclass.container).to be(superclass.container)
98
+ end
99
+
100
+ it 'inherits transproc from superclass' do
101
+ expect(superclass.new.call(2)).to be(3)
102
+ expect(subclass.new.call(2)).to be(6)
103
+ end
104
+ end
105
+
106
+ describe '.[]' do
107
+ subject(:subclass) { klass[another_container] }
108
+
109
+ let(:another_container) { double('Transproc') }
110
+
111
+ it 'sets a container' do
112
+ expect(subclass.container).to be(another_container)
113
+ end
114
+
115
+ it 'returns a class' do
116
+ expect(subclass).to be_a(Class)
117
+ end
118
+
119
+ it 'creates a subclass of Transformer' do
120
+ expect(subclass).to be < Transproc::Transformer
121
+ end
122
+
123
+ it 'does not change super class' do
124
+ expect(klass.container).to be(container)
125
+ end
126
+
127
+ it 'does not inherit transproc' do
128
+ expect(klass[container].new.transproc).to be_nil
129
+ end
130
+
131
+ context 'with predefined transformer' do
132
+ let(:klass) do
133
+ Class.new(Transproc::Transformer[container]) do
134
+ container.import Transproc::Coercions
135
+ container.import Transproc::HashTransformations
136
+
137
+ map_value :attr, t(:to_symbol)
138
+ end
139
+ end
140
+
141
+ it "inherits parent's transproc" do
142
+ expect(klass[container].new.transproc).to eql(klass.new.transproc)
143
+ end
144
+ end
145
+ end
146
+
147
+ describe '.define' do
148
+ let(:container) do
149
+ Module.new do
150
+ extend Transproc::Registry
151
+
152
+ import Transproc::HashTransformations
153
+
154
+ def self.to_symbol(v)
155
+ v.to_sym
156
+ end
157
+ end
158
+ end
159
+
160
+ let(:klass) { Transproc::Transformer[container] }
161
+
162
+ it 'defines anonymous transproc' do
163
+ transproc = klass.define do
164
+ map_value(:attr, t(:to_symbol))
165
+ end
166
+
167
+ expect(transproc[attr: 'abc']).to eq(attr: :abc)
168
+ end
169
+
170
+ it 'has .build alias' do
171
+ transproc = klass.build do
172
+ map_value(:attr, t(:to_symbol))
173
+ end
174
+
175
+ expect(transproc[attr: 'abc']).to eq(attr: :abc)
176
+ end
177
+
178
+ it 'does not affect original transformer' do
179
+ klass.define do
180
+ map_value(:attr, :to_sym.to_proc)
181
+ end
182
+
183
+ expect(klass.new.transproc).to be_nil
184
+ end
185
+
186
+ context 'with custom container' do
187
+ let(:container) do
188
+ Module.new do
189
+ extend Transproc::Registry
190
+
191
+ def self.arbitrary(value, fn)
192
+ fn[value]
193
+ end
194
+ end
195
+ end
196
+
197
+ let(:klass) { described_class[container] }
198
+
199
+ it 'uses a container from the transformer' do
200
+ transproc = klass.define! do
201
+ arbitrary ->(v) { v + 1 }
202
+ end.new
203
+
204
+ expect(transproc.call(2)).to eq 3
205
+ end
206
+ end
207
+
208
+ context 'with predefined transformer' do
209
+ let(:klass) do
210
+ Class.new(described_class[container]) do
211
+ define! do
212
+ map_value :attr, ->(v) { v + 1 }
213
+ end
214
+ end
215
+ end
216
+
217
+ it 'builds transformation from the DSL definition' do
218
+ transproc = klass.new
219
+
220
+ expect(transproc.call(attr: 2)).to eql(attr: 3)
221
+ end
222
+ end
223
+ end
224
+
225
+ describe '.t' do
226
+ subject(:klass) { Transproc::Transformer[container] }
227
+
228
+ let(:container) do
229
+ Module.new do
230
+ extend Transproc::Registry
231
+
232
+ import Transproc::HashTransformations
233
+ import Transproc::Conditional
234
+
235
+ def self.custom(value, suffix)
236
+ value + suffix
237
+ end
238
+ end
239
+ end
240
+
241
+ it 'returns a registed function' do
242
+ expect(klass.t(:custom, '_bar')).to eql(container[:custom, '_bar'])
243
+ end
244
+
245
+ it 'is useful in DSL' do
246
+ transproc = Class.new(klass) do
247
+ map_value :a, t(:custom, '_bar')
248
+ end.new
249
+
250
+ expect(transproc.call(a: 'foo')).to eq(a: 'foo_bar')
251
+ end
252
+
253
+ it 'works in nested block' do
254
+ transproc = Class.new(klass) do
255
+ map_values do
256
+ is String, t(:custom, '_bar')
257
+ end
258
+ end.new
259
+
260
+ expect(transproc.call(a: 'foo', b: :symbol)).to eq(a: 'foo_bar', b: :symbol)
261
+ end
262
+ end
263
+
264
+ describe '.method_missing' do
265
+ it 'responds to missing when there is a corresponding function' do
266
+ container.import Transproc::HashTransformations
267
+
268
+ expect(klass.method(:map_values)).to be_a(Method)
269
+ end
270
+
271
+ it 'raises when there is no corresponding function or instance method' do
272
+ expect { klass.not_here }.to raise_error(NoMethodError, /not_here/)
273
+ end
274
+ end
275
+
276
+ describe '#call' do
277
+ let(:container) do
278
+ Module.new do
279
+ extend Transproc::Registry
280
+
281
+ import Transproc::HashTransformations
282
+ import Transproc::ArrayTransformations
283
+ import Transproc::ClassTransformations
284
+ end
285
+ end
286
+
287
+ let(:klass) do
288
+ Class.new(Transproc::Transformer[container]) do
289
+ map_array do
290
+ symbolize_keys
291
+ rename_keys user_name: :name
292
+ nest :address, [:city, :street, :zipcode]
293
+
294
+ map_value :address do
295
+ constructor_inject Test::Address
296
+ end
297
+
298
+ constructor_inject Test::User
299
+ end
300
+ end
301
+ end
302
+
303
+ let(:input) do
304
+ [
305
+ { 'user_name' => 'Jane',
306
+ 'city' => 'NYC',
307
+ 'street' => 'Street 1',
308
+ 'zipcode' => '123'
309
+ }
310
+ ]
311
+ end
312
+
313
+ let(:expected_output) do
314
+ [
315
+ Test::User.new(
316
+ name: 'Jane',
317
+ address: Test::Address.new(
318
+ city: 'NYC',
319
+ street: 'Street 1',
320
+ zipcode: '123'
321
+ )
322
+ )
323
+ ]
324
+ end
325
+
326
+ before do
327
+ module Test
328
+ class User < OpenStruct
329
+ include Dry::Equalizer(:name, :address)
330
+ end
331
+
332
+ class Address < OpenStruct
333
+ include Dry::Equalizer(:city, :street, :zipcode)
334
+ end
335
+ end
336
+ end
337
+
338
+ it "transforms input" do
339
+ expect(transformer.(input)).to eql(expected_output)
340
+ end
341
+
342
+ context 'with custom registry' do
343
+ let(:klass) do
344
+ Class.new(Transproc::Transformer[registry]) do
345
+ append ' is awesome'
346
+ end
347
+ end
348
+
349
+ let(:registry) do
350
+ Module.new do
351
+ extend Transproc::Registry
352
+
353
+ def self.append(value, suffix)
354
+ value + suffix
355
+ end
356
+ end
357
+ end
358
+
359
+ it 'uses custom functions' do
360
+ expect(transformer.('transproc')).to eql('transproc is awesome')
361
+ end
362
+ end
363
+ end
364
+ end