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.
- checksums.yaml +4 -4
- data/.rubocop.yml +66 -0
- data/.rubocop_todo.yml +11 -0
- data/.travis.yml +2 -1
- data/CHANGELOG.md +31 -2
- data/Gemfile +5 -0
- data/Guardfile +2 -2
- data/README.md +5 -4
- data/Rakefile +1 -1
- data/lib/transproc.rb +13 -2
- data/lib/transproc/all.rb +2 -0
- data/lib/transproc/array.rb +78 -1
- data/lib/transproc/class.rb +52 -0
- data/lib/transproc/composite.rb +50 -0
- data/lib/transproc/conditional.rb +0 -1
- data/lib/transproc/error.rb +16 -0
- data/lib/transproc/function.rb +8 -50
- data/lib/transproc/hash.rb +52 -2
- data/lib/transproc/object.rb +19 -0
- data/lib/transproc/version.rb +1 -1
- data/rakelib/mutant.rake +16 -0
- data/rakelib/rubocop.rake +18 -0
- data/spec/spec_helper.rb +4 -2
- data/spec/unit/array_transformations_spec.rb +166 -0
- data/spec/unit/class_transformations_spec.rb +47 -0
- data/spec/{integration → unit}/coercions_spec.rb +10 -10
- data/spec/{integration → unit}/composer_spec.rb +0 -0
- data/spec/{integration → unit}/conditional_spec.rb +2 -2
- data/spec/{integration → unit}/function_spec.rb +30 -7
- data/spec/{integration/hash_spec.rb → unit/hash_transformations_spec.rb} +71 -23
- data/spec/{integration → unit}/recursion_spec.rb +6 -9
- data/spec/unit/transproc_spec.rb +64 -0
- data/transproc.gemspec +10 -10
- metadata +28 -19
- data/spec/integration/array_spec.rb +0 -98
- data/spec/integration/transproc_spec.rb +0 -37
@@ -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
|
data/lib/transproc/function.rb
CHANGED
@@ -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,
|
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
|
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
|
data/lib/transproc/hash.rb
CHANGED
@@ -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
|
data/lib/transproc/version.rb
CHANGED
data/rakelib/mutant.rake
ADDED
@@ -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
@@ -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
|