transproc 0.1.3 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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
- attr_reader :fn, :args
9
+ # Wrapped proc or another composite function
10
+ #
11
+ # @return [Proc,Composed]
12
+ #
13
+ # @api private
14
+ attr_reader :fn
4
15
 
5
- def initialize(fn, args = [])
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
- self.class.new(-> *input { other[fn[*input]] }, args)
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
@@ -1,57 +1,234 @@
1
+ require 'transproc/coercions'
2
+
1
3
  module Transproc
2
- register(:symbolize_keys) do |hash|
3
- Transproc(:symbolize_keys!)[Hash[hash]]
4
- end
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
- register(:symbolize_keys!) do |hash|
7
- hash.keys.each { |key| hash[key.to_sym] = hash.delete(key) }
8
- hash
9
- end
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
- register(:map_hash) do |hash, mapping|
12
- Transproc(:map_hash!, mapping)[Hash[hash]]
13
- end
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
- register(:map_hash!) do |hash, mapping|
16
- mapping.each { |k, v| hash[v] = hash.delete(k) }
17
- hash
18
- end
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
- register(:map_key) do |hash, key, fn|
21
- hash.merge(key => fn[hash[key]])
22
- end
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
- register(:map_key!) do |hash, key, fn|
25
- hash.update(key => fn[hash[key]])
26
- end
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
- register(:nest) do |hash, key, keys|
29
- Transproc(:nest!, key, keys)[Hash[hash]]
30
- end
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
- register(:nest!) do |hash, root, keys|
33
- nest_keys = hash.keys & keys
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
- if nest_keys.size > 0
36
- child = Hash[nest_keys.zip(nest_keys.map { |key| hash.delete(key) })]
37
- hash.update(root => child)
38
- else
39
- hash.update(root => {})
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
- register(:unwrap) do |hash, root, keys|
44
- copy = Hash[hash].merge(root => Hash[hash[root]])
45
- Transproc(:unwrap!, root, keys)[copy]
46
- end
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
- register(:unwrap!) do |hash, root, keys|
49
- if nested_hash = hash[root]
50
- keys ||= nested_hash.keys
51
- hash.update(Hash[keys.zip(keys.map { |key| nested_hash.delete(key) })])
52
- hash.delete(root) if nested_hash.empty?
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