transproc 0.1.3 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +7 -0
- data/CHANGELOG.md +23 -1
- data/Gemfile +3 -0
- data/README.md +30 -12
- data/lib/transproc.rb +61 -6
- data/lib/transproc/all.rb +1 -0
- data/lib/transproc/array.rb +94 -19
- data/lib/transproc/coercions.rb +140 -28
- data/lib/transproc/composer.rb +43 -0
- data/lib/transproc/conditional.rb +56 -0
- data/lib/transproc/function.rb +95 -4
- data/lib/transproc/hash.rb +218 -41
- data/lib/transproc/recursion.rb +61 -20
- data/lib/transproc/version.rb +1 -1
- data/spec/integration/array_spec.rb +2 -2
- data/spec/integration/coercions_spec.rb +8 -2
- data/spec/integration/composer_spec.rb +1 -1
- data/spec/integration/conditional_spec.rb +23 -0
- data/spec/integration/function_spec.rb +56 -0
- data/spec/integration/hash_spec.rb +124 -52
- data/transproc.gemspec +2 -2
- metadata +10 -5
data/lib/transproc/composer.rb
CHANGED
@@ -1,31 +1,74 @@
|
|
1
1
|
module Transproc
|
2
|
+
# Transproc helper that adds `t` method as a shortcut for `Transproc` method
|
3
|
+
#
|
4
|
+
# @example
|
5
|
+
# include Transproc::Helper
|
6
|
+
#
|
7
|
+
# t(:to_string)
|
8
|
+
#
|
9
|
+
# @api public
|
2
10
|
module Helper
|
11
|
+
# @see Transproc
|
12
|
+
#
|
13
|
+
# @api public
|
3
14
|
def t(*args, &block)
|
4
15
|
Transproc(*args, &block)
|
5
16
|
end
|
6
17
|
end
|
7
18
|
|
19
|
+
# Helper extension handy for composing many functions in multiple steps
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# include Transproc::Composer
|
23
|
+
#
|
24
|
+
# fn = compose do |fns|
|
25
|
+
# fns << t(:map_array, t(:symbolize_keys))
|
26
|
+
# fns << t(:map_array, t(:nest, :address, [:city, :zipcode]))
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# fn.call [{ 'city' => 'NYC', 'zipcode' => '123' }]
|
30
|
+
# # => [{ address: { city: 'NYC', zipcode: '123' }]
|
31
|
+
#
|
32
|
+
# @api public
|
8
33
|
module Composer
|
9
34
|
include Helper
|
10
35
|
|
36
|
+
# @api private
|
11
37
|
class Factory
|
12
38
|
attr_reader :fns, :default
|
13
39
|
|
40
|
+
# @api private
|
14
41
|
def initialize(default = nil)
|
15
42
|
@fns = []
|
16
43
|
@default = default
|
17
44
|
end
|
18
45
|
|
46
|
+
# @api private
|
19
47
|
def <<(other)
|
20
48
|
fns.concat(Array(other).compact)
|
21
49
|
self
|
22
50
|
end
|
23
51
|
|
52
|
+
# @api private
|
24
53
|
def to_fn
|
25
54
|
fns.reduce(:+) || default
|
26
55
|
end
|
27
56
|
end
|
28
57
|
|
58
|
+
# Gather and compose functions and fall-back to a default one if provided
|
59
|
+
#
|
60
|
+
# @example
|
61
|
+
# include Transproc::Composer
|
62
|
+
#
|
63
|
+
# fn = compose(-> v { v }) do |fns|
|
64
|
+
# fns << t(:to_string) if something
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# fn[1] # => "1"
|
68
|
+
#
|
69
|
+
# @see Composer
|
70
|
+
#
|
71
|
+
# @api public
|
29
72
|
def compose(default = nil)
|
30
73
|
factory = Factory.new(default)
|
31
74
|
yield(factory)
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module Transproc
|
2
|
+
|
3
|
+
# Conditional transformation functions
|
4
|
+
#
|
5
|
+
# @example
|
6
|
+
# require 'transproc/conditional'
|
7
|
+
#
|
8
|
+
# include Transproc::Helper
|
9
|
+
#
|
10
|
+
# fn = t(:guard, -> s { s.is_a?(::String) }, -> s { s.to_sym })
|
11
|
+
#
|
12
|
+
# [fn[2], fn['Jane']]
|
13
|
+
# # => [2, :Jane]
|
14
|
+
#
|
15
|
+
# @api public
|
16
|
+
module Conditional
|
17
|
+
extend Functions
|
18
|
+
|
19
|
+
# Apply the transformation function to subject if the predicate returns true, or return un-modified
|
20
|
+
#
|
21
|
+
# @example
|
22
|
+
# [2, 'Jane'].map do |subject|
|
23
|
+
# Transproc(:guard, -> s { s.is_a?(::String) }, -> s { s.to_sym })[subject]
|
24
|
+
# end
|
25
|
+
# # => [2, :Jane]
|
26
|
+
#
|
27
|
+
# @param [Mixed]
|
28
|
+
#
|
29
|
+
# @return [Mixed]
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def guard(value, predicate, fn)
|
33
|
+
predicate[value] ? fn[value] : value
|
34
|
+
end
|
35
|
+
|
36
|
+
# Calls a function when type-check passes
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# fn = Transproc(:is, Array, -> arr { arr.map(&:upcase) })
|
40
|
+
# fn.call(['a', 'b', 'c']) # => ['A', 'B', 'C']
|
41
|
+
#
|
42
|
+
# fn = Transproc(:is, Array, -> arr { arr.map(&:upcase) })
|
43
|
+
# fn.call('foo') # => "foo"
|
44
|
+
#
|
45
|
+
# @param [Object]
|
46
|
+
# @param [Class]
|
47
|
+
# @param [Proc]
|
48
|
+
#
|
49
|
+
# @return [Object]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def is(value, type, fn)
|
53
|
+
guard(value, -> v { v.is_a?(type) }, fn)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
data/lib/transproc/function.rb
CHANGED
@@ -1,21 +1,112 @@
|
|
1
1
|
module Transproc
|
2
|
+
# Transformation proc wrapper allowing composition of multiple procs into
|
3
|
+
# a data-transformation pipeline.
|
4
|
+
#
|
5
|
+
# This is used by Transproc to wrap registered methods.
|
6
|
+
#
|
7
|
+
# @api private
|
2
8
|
class Function
|
3
|
-
|
9
|
+
# Wrapped proc or another composite function
|
10
|
+
#
|
11
|
+
# @return [Proc,Composed]
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
attr_reader :fn
|
4
15
|
|
5
|
-
|
16
|
+
# Additional arguments that will be passed to the wrapped proc
|
17
|
+
#
|
18
|
+
# @return [Array]
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
attr_reader :args
|
22
|
+
|
23
|
+
# @api private
|
24
|
+
def initialize(fn, options = {})
|
6
25
|
@fn = fn
|
7
|
-
@args = args
|
26
|
+
@args = options.fetch(:args) { [] }
|
8
27
|
end
|
9
28
|
|
29
|
+
# Call the wrapped proc
|
30
|
+
#
|
31
|
+
# @param [Object] value The input value
|
32
|
+
#
|
33
|
+
# @alias []
|
34
|
+
#
|
35
|
+
# @api public
|
10
36
|
def call(value)
|
11
37
|
fn[value, *args]
|
12
38
|
end
|
13
39
|
alias_method :[], :call
|
14
40
|
|
41
|
+
# Compose this function with another function or a proc
|
42
|
+
#
|
43
|
+
# @param [Proc,Function]
|
44
|
+
#
|
45
|
+
# @return [Composite]
|
46
|
+
#
|
47
|
+
# @alias :>>
|
48
|
+
#
|
49
|
+
# @api public
|
15
50
|
def compose(other)
|
16
|
-
|
51
|
+
Composite.new(self, right: other)
|
17
52
|
end
|
18
53
|
alias_method :+, :compose
|
19
54
|
alias_method :>>, :compose
|
55
|
+
|
56
|
+
# Return a simple AST representation of this function
|
57
|
+
#
|
58
|
+
# @return [Array]
|
59
|
+
#
|
60
|
+
# @api public
|
61
|
+
def to_ast
|
62
|
+
identifier = Proc === fn ? fn : fn.name
|
63
|
+
[identifier, args]
|
64
|
+
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
|
20
111
|
end
|
21
112
|
end
|
data/lib/transproc/hash.rb
CHANGED
@@ -1,57 +1,234 @@
|
|
1
|
+
require 'transproc/coercions'
|
2
|
+
|
1
3
|
module Transproc
|
2
|
-
|
3
|
-
|
4
|
-
|
4
|
+
# Transformation functions for Hash objects
|
5
|
+
#
|
6
|
+
# @example
|
7
|
+
# require 'transproc/hash'
|
8
|
+
#
|
9
|
+
# include Transproc::Helper
|
10
|
+
#
|
11
|
+
# fn = t(:symbolize_keys) >> t(:nest, :address, [:street, :zipcode])
|
12
|
+
#
|
13
|
+
# fn["street" => "Street 1", "zipcode" => "123"]
|
14
|
+
# # => {:address => {:street => "Street 1", :zipcode => "123"}}
|
15
|
+
#
|
16
|
+
# @api public
|
17
|
+
module HashTransformations
|
18
|
+
extend Functions
|
5
19
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
20
|
+
# Map all keys in a hash with the provided transformation function
|
21
|
+
#
|
22
|
+
# @example
|
23
|
+
# Transproc(:map_keys, -> s { s.upcase })['name' => 'Jane']
|
24
|
+
# # => {"NAME" => "Jane"}
|
25
|
+
#
|
26
|
+
# @param [Hash]
|
27
|
+
#
|
28
|
+
# @return [Hash]
|
29
|
+
#
|
30
|
+
# @api public
|
31
|
+
def map_keys(hash, fn)
|
32
|
+
map_keys!(Hash[hash], fn)
|
33
|
+
end
|
10
34
|
|
11
|
-
|
12
|
-
|
13
|
-
|
35
|
+
# Same as `:map_keys` but mutates the hash
|
36
|
+
#
|
37
|
+
# @see HashTransformations.map_keys
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def map_keys!(hash, fn)
|
41
|
+
hash.keys.each { |key| hash[fn[key]] = hash.delete(key) }
|
42
|
+
hash
|
43
|
+
end
|
14
44
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
45
|
+
# Symbolize all keys in a hash
|
46
|
+
#
|
47
|
+
# @example
|
48
|
+
# Transproc(:symbolize_keys)['name' => 'Jane']
|
49
|
+
# # => {:name => "Jane"}
|
50
|
+
#
|
51
|
+
# @param [Hash]
|
52
|
+
#
|
53
|
+
# @return [Hash]
|
54
|
+
#
|
55
|
+
# @api public
|
56
|
+
def symbolize_keys(hash)
|
57
|
+
symbolize_keys!(Hash[hash])
|
58
|
+
end
|
19
59
|
|
20
|
-
|
21
|
-
|
22
|
-
|
60
|
+
# Same as `:symbolize_keys` but mutates the hash
|
61
|
+
#
|
62
|
+
# @see HashTransformations.symbolize_keys!
|
63
|
+
#
|
64
|
+
# @api public
|
65
|
+
def symbolize_keys!(hash)
|
66
|
+
map_keys!(hash, Transproc(:to_symbol).fn)
|
67
|
+
end
|
23
68
|
|
24
|
-
|
25
|
-
|
26
|
-
|
69
|
+
# Stringify all keys in a hash
|
70
|
+
#
|
71
|
+
# @example
|
72
|
+
# Transproc(:stringify_keys)[:name => 'Jane']
|
73
|
+
# # => {"name" => "Jane"}
|
74
|
+
#
|
75
|
+
# @param [Hash]
|
76
|
+
#
|
77
|
+
# @return [Hash]
|
78
|
+
#
|
79
|
+
# @api public
|
80
|
+
def stringify_keys(hash)
|
81
|
+
stringify_keys!(Hash[hash])
|
82
|
+
end
|
27
83
|
|
28
|
-
|
29
|
-
|
30
|
-
|
84
|
+
# Same as `:stringify_keys` but mutates the hash
|
85
|
+
#
|
86
|
+
# @see HashTransformations.stringify_keys
|
87
|
+
#
|
88
|
+
# @api public
|
89
|
+
def stringify_keys!(hash)
|
90
|
+
map_keys!(hash, Transproc(:to_string).fn)
|
91
|
+
end
|
31
92
|
|
32
|
-
|
33
|
-
|
93
|
+
# Map all values in a hash using transformation function
|
94
|
+
#
|
95
|
+
# @example
|
96
|
+
# Transproc(:map_values, -> v { v.upcase })[:name => 'Jane']
|
97
|
+
# # => {"name" => "JANE"}
|
98
|
+
#
|
99
|
+
# @param [Hash]
|
100
|
+
#
|
101
|
+
# @return [Hash]
|
102
|
+
#
|
103
|
+
# @api public
|
104
|
+
def map_values(hash, fn)
|
105
|
+
map_values!(Hash[hash], fn)
|
106
|
+
end
|
34
107
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
108
|
+
# Same as `:map_values` but mutates the hash
|
109
|
+
#
|
110
|
+
# @see HashTransformations.map_values
|
111
|
+
#
|
112
|
+
# @param [Hash]
|
113
|
+
#
|
114
|
+
# @return [Hash]
|
115
|
+
#
|
116
|
+
# @api public
|
117
|
+
def map_values!(hash, fn)
|
118
|
+
hash.each { |key, value| hash[key] = fn[value] }
|
119
|
+
hash
|
40
120
|
end
|
41
|
-
end
|
42
121
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
122
|
+
# Rename all keys in a hash using provided mapping hash
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# Transproc(:rename_keys, user_name: :name)[user_name: 'Jane']
|
126
|
+
# # => {:name => "Jane"}
|
127
|
+
#
|
128
|
+
# @param [Hash] hash The input hash
|
129
|
+
# @param [Hash] mapping The key-rename mapping
|
130
|
+
#
|
131
|
+
# @return [Hash]
|
132
|
+
#
|
133
|
+
# @api public
|
134
|
+
def rename_keys(hash, mapping)
|
135
|
+
rename_keys!(Hash[hash], mapping)
|
136
|
+
end
|
137
|
+
|
138
|
+
# Same as `:rename_keys` but mutates the hash
|
139
|
+
#
|
140
|
+
# @see HashTransformations.rename_keys
|
141
|
+
#
|
142
|
+
# @api public
|
143
|
+
def rename_keys!(hash, mapping)
|
144
|
+
mapping.each { |k, v| hash[v] = hash.delete(k) }
|
145
|
+
hash
|
146
|
+
end
|
147
|
+
|
148
|
+
# Map a key in a hash with the provided transformation function
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# Transproc(:map_value, -> s { s.upcase })['name' => 'jane']
|
152
|
+
# # => {"name" => "JANE"}
|
153
|
+
#
|
154
|
+
# @param [Hash]
|
155
|
+
#
|
156
|
+
# @return [Hash]
|
157
|
+
#
|
158
|
+
# @api public
|
159
|
+
def map_value(hash, key, fn)
|
160
|
+
hash.merge(key => fn[hash[key]])
|
161
|
+
end
|
162
|
+
|
163
|
+
# Same as `:map_value` but mutates the hash
|
164
|
+
#
|
165
|
+
# @see HashTransformations.map_value!
|
166
|
+
#
|
167
|
+
# @api public
|
168
|
+
def map_value!(hash, key, fn)
|
169
|
+
hash.update(key => fn[hash[key]])
|
170
|
+
end
|
47
171
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
172
|
+
# Nest values from specified keys under a new key
|
173
|
+
#
|
174
|
+
# @example
|
175
|
+
# Transproc(:nest, :address, [:street, :zipcode])[street: 'Street', zipcode: '123']
|
176
|
+
# # => {address: {street: "Street", zipcode: "123"}}
|
177
|
+
#
|
178
|
+
# @param [Hash]
|
179
|
+
#
|
180
|
+
# @return [Hash]
|
181
|
+
#
|
182
|
+
# @api public
|
183
|
+
def nest(hash, key, keys)
|
184
|
+
nest!(Hash[hash], key, keys)
|
53
185
|
end
|
54
186
|
|
55
|
-
hash
|
187
|
+
# Same as `:nest` but mutates the hash
|
188
|
+
#
|
189
|
+
# @see HashTransformations.nest
|
190
|
+
#
|
191
|
+
# @api public
|
192
|
+
def nest!(hash, root, keys)
|
193
|
+
nest_keys = hash.keys & keys
|
194
|
+
|
195
|
+
if nest_keys.size > 0
|
196
|
+
child = Hash[nest_keys.zip(nest_keys.map { |key| hash.delete(key) })]
|
197
|
+
hash.update(root => child)
|
198
|
+
else
|
199
|
+
hash.update(root => {})
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Collapse a nested hash from a specified key
|
204
|
+
#
|
205
|
+
# @example
|
206
|
+
# Transproc(:unwrap, :address, [:street, :zipcode])[address: { street: 'Street', zipcode: '123' }]
|
207
|
+
# # => {street: "Street", zipcode: "123"}
|
208
|
+
#
|
209
|
+
# @param [Hash]
|
210
|
+
#
|
211
|
+
# @return [Hash]
|
212
|
+
#
|
213
|
+
# @api public
|
214
|
+
def unwrap(hash, root, keys)
|
215
|
+
copy = Hash[hash].merge(root => Hash[hash[root]])
|
216
|
+
unwrap!(copy, root, keys)
|
217
|
+
end
|
218
|
+
|
219
|
+
# Same as `:unwrap` but mutates the hash
|
220
|
+
#
|
221
|
+
# @see HashTransformations.unwrap
|
222
|
+
#
|
223
|
+
# @api public
|
224
|
+
def unwrap!(hash, root, keys = nil)
|
225
|
+
if nested_hash = hash[root]
|
226
|
+
keys ||= nested_hash.keys
|
227
|
+
hash.update(Hash[keys.zip(keys.map { |key| nested_hash.delete(key) })])
|
228
|
+
hash.delete(root) if nested_hash.empty?
|
229
|
+
end
|
230
|
+
|
231
|
+
hash
|
232
|
+
end
|
56
233
|
end
|
57
234
|
end
|