transproc 0.2.0 → 0.2.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.
@@ -1,5 +1,4 @@
1
1
  module Transproc
2
-
3
2
  # Conditional transformation functions
4
3
  #
5
4
  # @example
@@ -0,0 +1,16 @@
1
+ module Transproc
2
+ Error = Class.new(StandardError)
3
+ FunctionNotFoundError = Class.new(Error)
4
+ FunctionAlreadyRegisteredError = Class.new(Error)
5
+
6
+ class MalformedInputError < Error
7
+ def initialize(function, value, error)
8
+ @function = function
9
+ @value = value
10
+ @original_error = error
11
+ super("failed to call function #{function} on #{value}, #{error}")
12
+ end
13
+
14
+ attr_reader :function, :value, :original_error
15
+ end
16
+ end
@@ -1,3 +1,5 @@
1
+ require 'transproc/composite'
2
+
1
3
  module Transproc
2
4
  # Transformation proc wrapper allowing composition of multiple procs into
3
5
  # a data-transformation pipeline.
@@ -33,8 +35,10 @@ module Transproc
33
35
  # @alias []
34
36
  #
35
37
  # @api public
36
- def call(value)
37
- fn[value, *args]
38
+ def call(*value)
39
+ fn[*value, *args]
40
+ rescue => ex
41
+ raise MalformedInputError.new(@fn, value, ex)
38
42
  end
39
43
  alias_method :[], :call
40
44
 
@@ -48,7 +52,7 @@ module Transproc
48
52
  #
49
53
  # @api public
50
54
  def compose(other)
51
- Composite.new(self, right: other)
55
+ Composite.new(self, other)
52
56
  end
53
57
  alias_method :+, :compose
54
58
  alias_method :>>, :compose
@@ -59,54 +63,8 @@ module Transproc
59
63
  #
60
64
  # @api public
61
65
  def to_ast
62
- identifier = Proc === fn ? fn : fn.name
66
+ identifier = fn.is_a?(::Proc) ? fn : fn.name
63
67
  [identifier, args]
64
68
  end
65
-
66
- # Composition of two functions
67
- #
68
- # @api private
69
- class Composite < Function
70
- alias_method :left, :fn
71
-
72
- # @return [Proc]
73
- #
74
- # @api private
75
- attr_reader :right
76
-
77
- # @api private
78
- def initialize(fn, options = {})
79
- super
80
- @right = options.fetch(:right)
81
- end
82
-
83
- # Call right side with the result from the left side
84
- #
85
- # @param [Object] value The input value
86
- #
87
- # @return [Object]
88
- #
89
- # @api public
90
- def call(value)
91
- right[left[value]]
92
- end
93
- alias_method :[], :call
94
-
95
- # @see Function#compose
96
- #
97
- # @api public
98
- def compose(other)
99
- Composite.new(self, right: other)
100
- end
101
- alias_method :+, :compose
102
- alias_method :>>, :compose
103
-
104
- # @see Function#to_ast
105
- #
106
- # @api public
107
- def to_ast
108
- left.to_ast << right.to_ast
109
- end
110
- end
111
69
  end
112
70
  end
@@ -145,10 +145,60 @@ module Transproc
145
145
  hash
146
146
  end
147
147
 
148
+ # Same as `:reject_keys` but mutates the hash
149
+ #
150
+ # @see HashTransformations.reject_keys
151
+ #
152
+ # @api public
153
+ def reject_keys!(hash, keys)
154
+ hash.reject { |k,_| keys.include?(k) }
155
+ end
156
+
157
+ # Rejects specified keys from a hash
158
+ #
159
+ # @example
160
+ # Transproc(:reject_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
161
+ # # => {:email => "jane@doe.org"}
162
+ #
163
+ # @param [Hash] hash The input hash
164
+ # @param [Array] keys The keys to be rejected
165
+ #
166
+ # @return [Hash]
167
+ #
168
+ # @api public
169
+ def reject_keys(hash, keys)
170
+ reject_keys!(Hash[hash], keys)
171
+ end
172
+
173
+ # Same as `:accept_keys` but mutates the hash
174
+ #
175
+ # @see HashTransformations.reject_keys
176
+ #
177
+ # @api public
178
+ def accept_keys!(hash, keys)
179
+ reject_keys!(hash, hash.keys - keys)
180
+ end
181
+
182
+ # Accepts specified keys from a hash
183
+ #
184
+ # @example
185
+ # Transproc(:accept_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
186
+ # # => {:email => "jane@doe.org"}
187
+ #
188
+ # @param [Hash] hash The input hash
189
+ # @param [Array] keys The keys to be accepted
190
+ #
191
+ # @return [Hash]
192
+ #
193
+ # @api public
194
+ def accept_keys(hash, keys)
195
+ accept_keys!(Hash[hash], keys)
196
+ end
197
+
148
198
  # Map a key in a hash with the provided transformation function
149
199
  #
150
200
  # @example
151
- # Transproc(:map_value, -> s { s.upcase })['name' => 'jane']
201
+ # Transproc(:map_value, 'name', -> s { s.upcase })['name' => 'jane']
152
202
  # # => {"name" => "JANE"}
153
203
  #
154
204
  # @param [Hash]
@@ -211,7 +261,7 @@ module Transproc
211
261
  # @return [Hash]
212
262
  #
213
263
  # @api public
214
- def unwrap(hash, root, keys)
264
+ def unwrap(hash, root, keys = nil)
215
265
  copy = Hash[hash].merge(root => Hash[hash[root]])
216
266
  unwrap!(copy, root, keys)
217
267
  end
@@ -0,0 +1,19 @@
1
+ module Transproc
2
+ # Transformation functions for Objects
3
+ #
4
+ # @example
5
+ # require 'transproc/object'
6
+ #
7
+ # include Transproc::Helper
8
+ #
9
+ # fn = t(:set_ivars, { name: 'Jane', age: 25 })
10
+ #
11
+ # fn[Object.new]
12
+ # # => #<Object:0x007f73afe7d6f8 @name="Jane", @age=25>
13
+ #
14
+ # @api public
15
+ module ObjectTransformations
16
+ extend Functions
17
+
18
+ end
19
+ end
@@ -1,3 +1,3 @@
1
1
  module Transproc
2
- VERSION = '0.2.0'.freeze
2
+ VERSION = '0.2.1'.freeze
3
3
  end
@@ -0,0 +1,16 @@
1
+ desc "Run mutant against a specific subject"
2
+ task :mutant do
3
+ subject = ARGV.last
4
+ if subject == 'mutant'
5
+ abort "usage: rake mutant SUBJECT\nexample: rake mutant Transproc::Recursion"
6
+ else
7
+ opts = {
8
+ 'include' => 'lib',
9
+ 'require' => 'transproc/all',
10
+ 'use' => 'rspec',
11
+ 'ignore-subject' => "#{subject}#respond_to_missing?"
12
+ }.to_a.map { |k, v| "--#{k} #{v}" }.join(' ')
13
+
14
+ exec("bundle exec mutant #{opts} #{subject}")
15
+ end
16
+ end
@@ -0,0 +1,18 @@
1
+ begin
2
+ require 'rubocop/rake_task'
3
+
4
+ Rake::Task[:default].enhance [:rubocop]
5
+
6
+ RuboCop::RakeTask.new do |task|
7
+ task.options << '--display-cop-names'
8
+ end
9
+
10
+ namespace :rubocop do
11
+ desc 'Generate a configuration file acting as a TODO list.'
12
+ task :auto_gen_config do
13
+ exec 'bundle exec rubocop --auto-gen-config'
14
+ end
15
+ end
16
+
17
+ rescue LoadError
18
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,8 +1,10 @@
1
- if RUBY_ENGINE == "rbx"
2
- require "codeclimate-test-reporter"
1
+ if RUBY_ENGINE == 'rbx'
2
+ require 'codeclimate-test-reporter'
3
3
  CodeClimate::TestReporter.start
4
4
  end
5
5
 
6
+ require 'equalizer'
7
+ require 'anima'
6
8
  require 'transproc/all'
7
9
 
8
10
  begin
@@ -0,0 +1,166 @@
1
+ require 'spec_helper'
2
+
3
+ describe Transproc::ArrayTransformations do
4
+ describe '.extract_key' do
5
+ it 'extracts values by key from all hashes' do
6
+ extract_key = t(:extract_key, 'name')
7
+
8
+ original = [
9
+ { 'name' => 'Alice', 'role' => 'sender' },
10
+ { 'name' => 'Bob', 'role' => 'receiver' },
11
+ { 'role' => 'listener' }
12
+ ]
13
+
14
+ input = original
15
+
16
+ output = ['Alice', 'Bob', nil]
17
+
18
+ expect(extract_key[input]).to eql(output)
19
+ expect(input).to eql(original)
20
+ end
21
+ end
22
+
23
+ describe '.extract_key!' do
24
+ it 'extracts values by key from all hashes' do
25
+ extract_key = t(:extract_key!, 'name')
26
+
27
+ input = [
28
+ { 'name' => 'Alice', 'role' => 'sender' },
29
+ { 'name' => 'Bob', 'role' => 'receiver' },
30
+ { 'role' => 'listener' }
31
+ ]
32
+
33
+ output = ['Alice', 'Bob', nil]
34
+
35
+ extract_key[input]
36
+
37
+ expect(input).to eql(output)
38
+ end
39
+ end
40
+
41
+ describe '.map_array' do
42
+ it 'applies funtions to all values' do
43
+ map = t(:map_array, t(:symbolize_keys))
44
+
45
+ original = [
46
+ { 'name' => 'Jane', 'title' => 'One' },
47
+ { 'name' => 'Jane', 'title' => 'Two' }
48
+ ]
49
+
50
+ input = original
51
+
52
+ output = [
53
+ { name: 'Jane', title: 'One' },
54
+ { name: 'Jane', title: 'Two' }
55
+ ]
56
+
57
+ expect(map[input]).to eql(output)
58
+ expect(input).to eql(original)
59
+ end
60
+ end
61
+
62
+ describe '.map_array!' do
63
+ it 'updates array with the result of the function applied to each value' do
64
+ map = t(:map_array!, t(:symbolize_keys))
65
+
66
+ input = [
67
+ { 'name' => 'Jane', 'title' => 'One' },
68
+ { 'name' => 'Jane', 'title' => 'Two' }
69
+ ]
70
+
71
+ output = [
72
+ { name: 'Jane', title: 'One' },
73
+ { name: 'Jane', title: 'Two' }
74
+ ]
75
+
76
+ map[input]
77
+
78
+ expect(input).to eql(output)
79
+ end
80
+ end
81
+
82
+ describe '.wrap' do
83
+ it 'returns a new array with wrapped hashes' do
84
+ wrap = t(:wrap, :task, [:title])
85
+
86
+ input = [{ name: 'Jane', title: 'One' }]
87
+ output = [{ name: 'Jane', task: { title: 'One' } }]
88
+
89
+ expect(wrap[input]).to eql(output)
90
+ end
91
+
92
+ it 'returns a array new with deeply wrapped hashes' do
93
+ wrap =
94
+ t(
95
+ :map_array,
96
+ t(:nest, :user, [:name, :title]) +
97
+ t(:map_value, :user, t(:nest, :task, [:title]))
98
+ )
99
+
100
+ input = [{ name: 'Jane', title: 'One' }]
101
+ output = [{ user: { name: 'Jane', task: { title: 'One' } } }]
102
+
103
+ expect(wrap[input]).to eql(output)
104
+ end
105
+ end
106
+
107
+ describe '.group' do
108
+ it 'returns a new array with grouped hashes' do
109
+ group = t(:group, :tasks, [:title])
110
+
111
+ input = [{ name: 'Jane', title: 'One' }, { name: 'Jane', title: 'Two' }]
112
+ output = [{ name: 'Jane', tasks: [{ title: 'One' }, { title: 'Two' }] }]
113
+
114
+ expect(group[input]).to eql(output)
115
+ end
116
+ end
117
+
118
+ describe '.combine' do
119
+ let(:input) do
120
+ [
121
+ # parent users
122
+ [
123
+ { name: 'Jane', email: 'jane@doe.org' },
124
+ { name: 'Joe', email: 'joe@doe.org' }
125
+ ],
126
+ [
127
+ [
128
+ # user tasks
129
+ [
130
+ { user: 'Jane', title: 'One' },
131
+ { user: 'Jane', title: 'Two' },
132
+ { user: 'Joe', title: 'Three' }
133
+ ],
134
+ [
135
+ # task tags
136
+ [
137
+ { task: 'One', tag: 'red' },
138
+ { task: 'Three', tag: 'blue' }
139
+ ]
140
+ ]
141
+ ]
142
+ ]
143
+ ]
144
+ end
145
+
146
+ let(:output) do
147
+ [
148
+ { name: 'Jane', email: 'jane@doe.org', tasks: [
149
+ { user: 'Jane', title: 'One', tags: [{ task: 'One', tag: 'red' }] },
150
+ { user: 'Jane', title: 'Two', tags: [] } ]
151
+ },
152
+ { name: 'Joe', email: 'joe@doe.org', tasks: [
153
+ { user: 'Joe', title: 'Three', tags: [{ task: 'Three', tag: 'blue' }] } ]
154
+ }
155
+ ]
156
+ end
157
+
158
+ it 'merges hashes from arrays using provided join keys' do
159
+ combine = t(:combine, [
160
+ [:tasks, { name: :user }, [[:tags, title: :task]]]
161
+ ])
162
+
163
+ expect(combine[input]).to eql(output)
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+
3
+ describe Transproc::ClassTransformations do
4
+ describe '.constructor_inject' do
5
+ let(:klass) do
6
+ Struct.new(:name, :age) { include Equalizer.new(:name, :age) }
7
+ end
8
+
9
+ it 'returns a new object initialized with the given arguments' do
10
+ constructor_inject = t(:constructor_inject, klass)
11
+
12
+ input = ['Jane', 25]
13
+ output = klass.new(*input)
14
+ result = constructor_inject[*input]
15
+
16
+ expect(result).to eql(output)
17
+ expect(result).to be_instance_of(klass)
18
+ end
19
+ end
20
+
21
+ describe '.set_ivars' do
22
+ let(:klass) do
23
+ Class.new do
24
+ include Anima.new(:name, :age)
25
+
26
+ attr_reader :test
27
+
28
+ def initialize(*args)
29
+ super
30
+ @test = true
31
+ end
32
+ end
33
+ end
34
+
35
+ it 'allocates a new object and sets instance variables from hash key/value pairs' do
36
+ set_ivars = t(:set_ivars, klass)
37
+
38
+ input = { name: 'Jane', age: 25 }
39
+ output = klass.new(input)
40
+ result = set_ivars[input]
41
+
42
+ expect(result).to eql(output)
43
+ expect(result.test).to be(nil)
44
+ expect(result).to be_instance_of(klass)
45
+ end
46
+ end
47
+ end