transproc 1.0.3 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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