transproc 1.0.0 → 1.1.1

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