transproc 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +3 -0
  4. data/.travis.yml +14 -10
  5. data/CHANGELOG.md +61 -17
  6. data/Gemfile +9 -18
  7. data/README.md +17 -19
  8. data/Rakefile +1 -0
  9. data/lib/transproc.rb +3 -4
  10. data/lib/transproc/all.rb +2 -0
  11. data/lib/transproc/array.rb +9 -51
  12. data/lib/transproc/array/combine.rb +61 -0
  13. data/lib/transproc/class.rb +2 -0
  14. data/lib/transproc/coercions.rb +2 -0
  15. data/lib/transproc/compiler.rb +45 -0
  16. data/lib/transproc/composer.rb +2 -0
  17. data/lib/transproc/composite.rb +2 -0
  18. data/lib/transproc/conditional.rb +2 -0
  19. data/lib/transproc/constants.rb +5 -0
  20. data/lib/transproc/error.rb +2 -0
  21. data/lib/transproc/function.rb +4 -1
  22. data/lib/transproc/functions.rb +2 -0
  23. data/lib/transproc/hash.rb +105 -45
  24. data/lib/transproc/proc.rb +2 -0
  25. data/lib/transproc/recursion.rb +2 -0
  26. data/lib/transproc/registry.rb +4 -2
  27. data/lib/transproc/store.rb +1 -0
  28. data/lib/transproc/support/deprecations.rb +2 -0
  29. data/lib/transproc/transformer.rb +7 -1
  30. data/lib/transproc/transformer/class_interface.rb +32 -65
  31. data/lib/transproc/transformer/deprecated/class_interface.rb +81 -0
  32. data/lib/transproc/transformer/dsl.rb +51 -0
  33. data/lib/transproc/version.rb +3 -1
  34. data/spec/spec_helper.rb +21 -10
  35. data/spec/unit/array/combine_spec.rb +224 -0
  36. data/spec/unit/array_transformations_spec.rb +1 -52
  37. data/spec/unit/class_transformations_spec.rb +10 -7
  38. data/spec/unit/coercions_spec.rb +1 -1
  39. data/spec/unit/composer_spec.rb +1 -1
  40. data/spec/unit/conditional_spec.rb +1 -1
  41. data/spec/unit/function_not_found_error_spec.rb +1 -1
  42. data/spec/unit/function_spec.rb +16 -1
  43. data/spec/unit/hash_transformations_spec.rb +12 -1
  44. data/spec/unit/proc_transformations_spec.rb +3 -1
  45. data/spec/unit/recursion_spec.rb +1 -1
  46. data/spec/unit/registry_spec.rb +9 -1
  47. data/spec/unit/store_spec.rb +2 -1
  48. data/spec/unit/transformer/class_interface_spec.rb +364 -0
  49. data/spec/unit/transformer/dsl_spec.rb +15 -0
  50. data/spec/unit/transformer/instance_methods_spec.rb +25 -0
  51. data/spec/unit/transformer_spec.rb +128 -40
  52. data/spec/unit/transproc_spec.rb +1 -1
  53. data/transproc.gemspec +1 -5
  54. metadata +19 -54
  55. data/.rubocop.yml +0 -66
  56. data/.rubocop_todo.yml +0 -11
  57. data/rakelib/mutant.rake +0 -16
  58. data/rakelib/rubocop.rake +0 -18
  59. data/spec/support/mutant.rb +0 -10
@@ -1,7 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'transproc/transformer/dsl'
4
+
1
5
  module Transproc
2
6
  class Transformer
3
- # @api private
7
+ # @api public
4
8
  module ClassInterface
9
+ # @api private
10
+ attr_reader :dsl
11
+
5
12
  # Return a base Transproc::Transformer class with the
6
13
  # container configured to the passed argument.
7
14
  #
@@ -17,14 +24,18 @@ module Transproc
17
24
  #
18
25
  # @api public
19
26
  def [](container)
20
- klass = Class.new(Transformer)
27
+ klass = Class.new(self)
21
28
  klass.container(container)
22
29
  klass
23
30
  end
24
31
 
25
32
  # @api private
26
33
  def inherited(subclass)
34
+ super
35
+
27
36
  subclass.container(@container) if defined?(@container)
37
+
38
+ subclass.instance_variable_set('@dsl', dsl.dup) if dsl
28
39
  end
29
40
 
30
41
  # Get or set the container to resolve transprocs from.
@@ -45,41 +56,33 @@ module Transproc
45
56
  # @return [Transproc::Registry]
46
57
  #
47
58
  # @api private
48
- def container(container = ::Transproc::Undefined)
49
- if container == ::Transproc::Undefined
50
- ensure_container_presence!
51
- @container
59
+ def container(container = Undefined)
60
+ if container.equal?(Undefined)
61
+ @container ||= Module.new.extend(Transproc::Registry)
52
62
  else
53
63
  @container = container
54
64
  end
55
65
  end
56
66
 
57
- # Define an anonymous transproc derived from given Transformer
58
- # Evaluates block with transformations and returns initialized transproc.
59
- # Does not mutate original Transformer
60
- #
61
- # @example
62
- #
63
- # class MyTransformer < Transproc::Transformer[MyContainer]
64
- # end
65
- #
66
- # transproc = MyTransformer.define do
67
- # map_values t(:to_string)
68
- # end
69
- # transproc.call(a: 1, b: 2)
70
- # # => {a: '1', b: '2'}
71
- #
72
- # @yield Block allowing to define transformations. The same as class level DSL
73
- #
74
- # @return [Function] Composed transproc
75
- #
76
67
  # @api public
77
- def define(&block)
78
- return transproc unless block_given?
68
+ def import(*args)
69
+ container.import(*args)
70
+ end
71
+
72
+ # @api public
73
+ def define!(&block)
74
+ @dsl ||= DSL.new(container)
75
+ @dsl.instance_eval(&block)
76
+ self
77
+ end
79
78
 
80
- Class.new(self).tap { |klass| klass.instance_eval(&block) }.transproc
79
+ # @api public
80
+ def new(*)
81
+ super.tap do |transformer|
82
+ transformer.instance_variable_set('@transproc', dsl.(transformer)) if dsl
83
+ end
81
84
  end
82
- alias build define
85
+ ruby2_keywords(:new) if respond_to?(:ruby2_keywords, true)
83
86
 
84
87
  # Get a transformation from the container,
85
88
  # without adding it to the transformation pipeline
@@ -105,42 +108,6 @@ module Transproc
105
108
  def t(fn, *args)
106
109
  container[fn, *args]
107
110
  end
108
-
109
- # @api private
110
- def method_missing(method, *args, &block)
111
- if container.contain?(method)
112
- args.push(define(&block)) if block_given?
113
- transformations << t(method, *args)
114
- else
115
- super
116
- end
117
- end
118
-
119
- # @api private
120
- def respond_to_missing?(method, _include_private = false)
121
- container.contain?(method) || super
122
- end
123
-
124
- # @api private
125
- def transproc
126
- transformations.reduce(:>>)
127
- end
128
-
129
- private
130
-
131
- # An array containing the transformation pipeline
132
- #
133
- # @api private
134
- def transformations
135
- @transformations ||= []
136
- end
137
-
138
- # @api private
139
- def ensure_container_presence!
140
- return if defined?(@container)
141
- raise ArgumentError, 'Transformer function registry is empty. '\
142
- 'Provide your registry via Transproc::Transformer[YourRegistry]'
143
- end
144
111
  end
145
112
  end
146
113
  end
@@ -0,0 +1,81 @@
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(*)
10
+ super.tap do |transformer|
11
+ transformer.instance_variable_set('@transproc', transproc) if transformations.any?
12
+ end
13
+ end
14
+ ruby2_keywords(:new) if respond_to?(:ruby2_keywords, true)
15
+
16
+ # @api private
17
+ def inherited(subclass)
18
+ super
19
+
20
+ if transformations.any?
21
+ subclass.instance_variable_set('@transformations', transformations.dup)
22
+ end
23
+ end
24
+
25
+ # Define an anonymous transproc derived from given Transformer
26
+ # Evaluates block with transformations and returns initialized transproc.
27
+ # Does not mutate original Transformer
28
+ #
29
+ # @example
30
+ #
31
+ # class MyTransformer < Transproc::Transformer[MyContainer]
32
+ # end
33
+ #
34
+ # transproc = MyTransformer.define do
35
+ # map_values t(:to_string)
36
+ # end
37
+ # transproc.call(a: 1, b: 2)
38
+ # # => {a: '1', b: '2'}
39
+ #
40
+ # @yield Block allowing to define transformations. The same as class level DSL
41
+ #
42
+ # @return [Function] Composed transproc
43
+ #
44
+ # @api public
45
+ def define(&block)
46
+ return transproc unless block_given?
47
+
48
+ Class.new(Transformer[container]).tap { |klass| klass.instance_eval(&block) }.transproc
49
+ end
50
+ alias build define
51
+
52
+ # @api private
53
+ def method_missing(method, *args, &block)
54
+ super unless container.contain?(method)
55
+ func = block ? t(method, *args, define(&block)) : t(method, *args)
56
+ transformations << func
57
+ func
58
+ end
59
+
60
+ # @api private
61
+ def respond_to_missing?(method, _include_private = false)
62
+ super || container.contain?(method)
63
+ end
64
+
65
+ # @api private
66
+ def transproc
67
+ transformations.reduce(:>>)
68
+ end
69
+
70
+ private
71
+
72
+ # An array containing the transformation pipeline
73
+ #
74
+ # @api private
75
+ def transformations
76
+ @transformations ||= []
77
+ end
78
+ end
79
+ end
80
+ end
81
+ 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.0'.freeze
4
+ VERSION = '1.1.1'.freeze
3
5
  end
data/spec/spec_helper.rb CHANGED
@@ -1,19 +1,30 @@
1
- if RUBY_ENGINE == 'ruby' && RUBY_VERSION == '2.3.1'
2
- require 'simplecov'
3
- SimpleCov.start do
4
- add_filter '/spec/'
1
+ # frozen_string_literal: true
2
+
3
+ if RUBY_ENGINE == 'ruby' && ENV['COVERAGE'] == 'true'
4
+ require 'yaml'
5
+ rubies = YAML.load(File.read(File.join(__dir__, '..', '.travis.yml')))['rvm']
6
+ latest_mri = rubies.select { |v| v =~ /\A\d+\.\d+.\d+\z/ }.max
7
+
8
+ if RUBY_VERSION == latest_mri
9
+ require 'simplecov'
10
+ SimpleCov.start do
11
+ add_filter '/spec/'
12
+ end
5
13
  end
6
14
  end
7
15
 
8
- require 'equalizer'
9
- require 'anima'
10
- require 'ostruct'
11
- require 'transproc/all'
16
+ if defined? Warning
17
+ require 'warning'
18
+
19
+ Warning.ignore(/rspec/)
20
+ Warning.process { |w| raise RuntimeError, w } unless ENV['NO_WARNING']
21
+ end
12
22
 
13
23
  begin
14
24
  require 'byebug'
15
- rescue LoadError
16
- end
25
+ rescue LoadError;end
26
+
27
+ require 'transproc/all'
17
28
 
18
29
  root = Pathname(__FILE__).dirname
19
30
  Dir[root.join('support/*.rb').to_s].each { |f| require f }
@@ -0,0 +1,224 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ describe Transproc::ArrayTransformations do
6
+ describe '.combine' do
7
+ subject(:result) { described_class.t(:combine, mappings)[input] }
8
+
9
+ let(:input) { [[]] }
10
+ let(:mappings) { [] }
11
+
12
+ it { is_expected.to be_a(Array) }
13
+
14
+ it { is_expected.to eq([]) }
15
+
16
+ context 'without groups' do
17
+ let(:input) do
18
+ [
19
+ [
20
+ {name: 'Jane', email: 'jane@doe.org'}.freeze,
21
+ {name: 'Joe', email: 'joe@doe.org'}.freeze
22
+ ].freeze
23
+ ].freeze
24
+ end
25
+
26
+ it { is_expected.to eq input.first }
27
+ end
28
+
29
+ context 'with one group' do
30
+ let(:input) do
31
+ [
32
+ [
33
+ {name: 'Jane', email: 'jane@doe.org'}.freeze,
34
+ {name: 'Joe', email: 'joe@doe.org'}.freeze
35
+ ].freeze,
36
+ [
37
+ [
38
+ {user: 'Jane', title: 'One'}.freeze,
39
+ {user: 'Jane', title: 'Two'}.freeze,
40
+ {user: 'Joe', title: 'Three'}.freeze
41
+ ]
42
+ ]
43
+ ].freeze
44
+ end
45
+ let(:mappings) { [[:tasks, {name: :user}]] }
46
+
47
+ it 'merges hashes from arrays using provided join keys' do
48
+ output = [
49
+ {name: 'Jane', email: 'jane@doe.org', tasks: [
50
+ {user: 'Jane', title: 'One'},
51
+ {user: 'Jane', title: 'Two'}
52
+ ]},
53
+ {name: 'Joe', email: 'joe@doe.org', tasks: [
54
+ {user: 'Joe', title: 'Three'}
55
+ ]}
56
+ ]
57
+ is_expected.to eql(output)
58
+ end
59
+ end
60
+
61
+ context 'with empty nodes' do
62
+ let(:input) do
63
+ [
64
+ [{name: 'Jane', email: 'jane@doe.org'}.freeze].freeze,
65
+ [
66
+ []
67
+ ]
68
+ ].freeze
69
+ end
70
+
71
+ let(:mappings) { [[:tasks, {name: :user}]] }
72
+
73
+ it { is_expected.to eq([{name: 'Jane', email: 'jane@doe.org', tasks: []}]) }
74
+ end
75
+
76
+ context 'with double mapping' do
77
+ let(:input) do
78
+ [
79
+ [
80
+ {name: 'Jane', email: 'jane@doe.org'}.freeze
81
+ ].freeze,
82
+ [
83
+ [
84
+ {user: 'Jane', user_email: 'jane@doe.org', title: 'One'}.freeze,
85
+ {user: 'Jane', user_email: '', title: 'Two'}.freeze
86
+ ].freeze
87
+ ].freeze
88
+ ].freeze
89
+ end
90
+
91
+ let(:mappings) { [[:tasks, {name: :user, email: :user_email}]] }
92
+
93
+ it 'searches by two keys simultaneously' do
94
+ output = [
95
+ {name: 'Jane', email: 'jane@doe.org', tasks: [
96
+ {user: 'Jane', user_email: 'jane@doe.org', title: 'One'}
97
+ ]}
98
+ ]
99
+ is_expected.to eql(output)
100
+ end
101
+ end
102
+
103
+ context 'with non-array argument' do
104
+ let(:input) do
105
+ 123
106
+ end
107
+
108
+ let(:mappings) { [[:page, {page_id: :id}]] }
109
+
110
+ it { is_expected.to eq(123) }
111
+ end
112
+
113
+ context 'with empty nested array' do
114
+ let(:input) do
115
+ [
116
+ [],
117
+ [
118
+ []
119
+ ]
120
+ ]
121
+ end
122
+
123
+ let(:mappings) { [[:menu_items, {id: :menu_id}, [[:page, {page_id: :id}]]]] }
124
+
125
+ it 'does not crash' do
126
+ expect { result }.not_to raise_error
127
+ end
128
+ end
129
+
130
+ context 'with enumerable input' do
131
+ let(:my_enumerator) do
132
+ Class.new do
133
+ include Enumerable
134
+ extend Forwardable
135
+
136
+ def_delegator :@array, :each
137
+
138
+ def initialize(array)
139
+ @array = array
140
+ end
141
+ end
142
+ end
143
+
144
+ let(:input) do
145
+ [
146
+ my_enumerator.new([
147
+ {name: 'Jane', email: 'jane@doe.org'}.freeze,
148
+ {name: 'Joe', email: 'joe@doe.org'}.freeze
149
+ ].freeze),
150
+ my_enumerator.new([
151
+ my_enumerator.new([
152
+ {user: 'Jane', title: 'One'}.freeze,
153
+ {user: 'Jane', title: 'Two'}.freeze,
154
+ {user: 'Joe', title: 'Three'}.freeze
155
+ ].freeze)
156
+ ].freeze)
157
+ ].freeze
158
+ end
159
+ let(:mappings) { [[:tasks, {name: :user}]] }
160
+
161
+ it 'supports enumerables as well' do
162
+ output = [
163
+ {name: 'Jane', email: 'jane@doe.org', tasks: [
164
+ {user: 'Jane', title: 'One'},
165
+ {user: 'Jane', title: 'Two'}
166
+ ]},
167
+ {name: 'Joe', email: 'joe@doe.org', tasks: [
168
+ {user: 'Joe', title: 'Three'}
169
+ ]}
170
+ ]
171
+ is_expected.to eql(output)
172
+ end
173
+ end
174
+
175
+ describe 'integration test' do
176
+ let(:input) do
177
+ [
178
+ [
179
+ {name: 'Jane', email: 'jane@doe.org'},
180
+ {name: 'Joe', email: 'joe@doe.org'}
181
+ ],
182
+ [
183
+ [
184
+ # user tasks
185
+ [
186
+ {user: 'Jane', title: 'One'},
187
+ {user: 'Jane', title: 'Two'},
188
+ {user: 'Joe', title: 'Three'}
189
+ ],
190
+ [
191
+ # task tags
192
+ [
193
+ {task: 'One', tag: 'red'},
194
+ {task: 'Three', tag: 'blue'}
195
+ ]
196
+ ]
197
+ ]
198
+ ]
199
+ ]
200
+ end
201
+
202
+ let(:mappings) { [[:tasks, {name: :user}, [[:tags, title: :task]]]] }
203
+
204
+ it 'merges hashes from arrays using provided join keys' do
205
+ output = [
206
+ {name: 'Jane', email: 'jane@doe.org', tasks: [
207
+ {user: 'Jane', title: 'One', tags: [{task: 'One', tag: 'red'}]},
208
+ {user: 'Jane', title: 'Two', tags: []}
209
+ ]},
210
+ {
211
+ name: 'Joe', email: 'joe@doe.org', tasks: [
212
+ {
213
+ user: 'Joe', title: 'Three', tags: [
214
+ {task: 'Three', tag: 'blue'}
215
+ ]
216
+ }
217
+ ]
218
+ }
219
+ ]
220
+ is_expected.to eql(output)
221
+ end
222
+ end
223
+ end
224
+ end