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