transproc 0.1.3 → 0.2.0
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/.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
|