transproc 1.0.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.codeclimate.yml +15 -0
- data/.gitignore +3 -0
- data/.travis.yml +14 -10
- data/CHANGELOG.md +61 -17
- data/Gemfile +9 -18
- data/README.md +17 -19
- data/Rakefile +1 -0
- data/lib/transproc.rb +3 -4
- data/lib/transproc/all.rb +2 -0
- data/lib/transproc/array.rb +9 -51
- data/lib/transproc/array/combine.rb +61 -0
- data/lib/transproc/class.rb +2 -0
- data/lib/transproc/coercions.rb +2 -0
- data/lib/transproc/compiler.rb +45 -0
- data/lib/transproc/composer.rb +2 -0
- data/lib/transproc/composite.rb +2 -0
- data/lib/transproc/conditional.rb +2 -0
- data/lib/transproc/constants.rb +5 -0
- data/lib/transproc/error.rb +2 -0
- data/lib/transproc/function.rb +4 -1
- data/lib/transproc/functions.rb +2 -0
- data/lib/transproc/hash.rb +105 -45
- data/lib/transproc/proc.rb +2 -0
- data/lib/transproc/recursion.rb +2 -0
- data/lib/transproc/registry.rb +4 -2
- data/lib/transproc/store.rb +1 -0
- data/lib/transproc/support/deprecations.rb +2 -0
- data/lib/transproc/transformer.rb +7 -1
- data/lib/transproc/transformer/class_interface.rb +32 -65
- data/lib/transproc/transformer/deprecated/class_interface.rb +81 -0
- data/lib/transproc/transformer/dsl.rb +51 -0
- data/lib/transproc/version.rb +3 -1
- data/spec/spec_helper.rb +21 -10
- data/spec/unit/array/combine_spec.rb +224 -0
- data/spec/unit/array_transformations_spec.rb +1 -52
- data/spec/unit/class_transformations_spec.rb +10 -7
- data/spec/unit/coercions_spec.rb +1 -1
- data/spec/unit/composer_spec.rb +1 -1
- data/spec/unit/conditional_spec.rb +1 -1
- data/spec/unit/function_not_found_error_spec.rb +1 -1
- data/spec/unit/function_spec.rb +16 -1
- data/spec/unit/hash_transformations_spec.rb +12 -1
- data/spec/unit/proc_transformations_spec.rb +3 -1
- data/spec/unit/recursion_spec.rb +1 -1
- data/spec/unit/registry_spec.rb +9 -1
- data/spec/unit/store_spec.rb +2 -1
- data/spec/unit/transformer/class_interface_spec.rb +364 -0
- data/spec/unit/transformer/dsl_spec.rb +15 -0
- data/spec/unit/transformer/instance_methods_spec.rb +25 -0
- data/spec/unit/transformer_spec.rb +128 -40
- data/spec/unit/transproc_spec.rb +1 -1
- data/transproc.gemspec +1 -5
- metadata +19 -54
- data/.rubocop.yml +0 -66
- data/.rubocop_todo.yml +0 -11
- data/rakelib/mutant.rake +0 -16
- data/rakelib/rubocop.rake +0 -18
- data/spec/support/mutant.rb +0 -10
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transproc
|
4
|
+
module ArrayTransformations
|
5
|
+
class Combine
|
6
|
+
EMPTY_ARRAY = [].freeze
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def combine(array, mappings)
|
10
|
+
root, nodes = array
|
11
|
+
return EMPTY_ARRAY if root.nil?
|
12
|
+
return root if nodes.nil?
|
13
|
+
groups = group_nodes(nodes, mappings)
|
14
|
+
|
15
|
+
root.map do |element|
|
16
|
+
element.dup.tap { |copy| add_groups_to_element(copy, groups, mappings) }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def add_groups_to_element(element, groups, mappings)
|
23
|
+
groups.each_with_index do |candidates, index|
|
24
|
+
mapping = mappings[index]
|
25
|
+
resource_key = mapping[0]
|
26
|
+
element[resource_key] = element_candidates(element, candidates, mapping[1].keys)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def element_candidates(element, candidates, keys)
|
31
|
+
candidates[element_candidates_key(element, keys)] || EMPTY_ARRAY
|
32
|
+
end
|
33
|
+
|
34
|
+
def group_nodes(nodes, mappings)
|
35
|
+
nodes.each_with_index.map do |candidates, index|
|
36
|
+
mapping = mappings[index]
|
37
|
+
group_candidates(candidates, mapping)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def group_candidates(candidates, mapping)
|
42
|
+
nested_mapping = mapping[2]
|
43
|
+
candidates = combine(candidates, nested_mapping) unless nested_mapping.nil?
|
44
|
+
group_candidates_by_keys(candidates, mapping[1].values)
|
45
|
+
end
|
46
|
+
|
47
|
+
def group_candidates_by_keys(candidates, keys)
|
48
|
+
return candidates.group_by { |a| a.values_at(*keys) } if keys.size > 1
|
49
|
+
|
50
|
+
key = keys.first
|
51
|
+
candidates.group_by { |a| a[key] }
|
52
|
+
end
|
53
|
+
|
54
|
+
def element_candidates_key(element, keys)
|
55
|
+
return element.values_at(*keys) if keys.size > 1
|
56
|
+
element[keys.first]
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
data/lib/transproc/class.rb
CHANGED
data/lib/transproc/coercions.rb
CHANGED
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transproc
|
4
|
+
# @api private
|
5
|
+
class Compiler
|
6
|
+
InvalidFunctionNameError = Class.new(StandardError)
|
7
|
+
|
8
|
+
attr_reader :registry, :transformer
|
9
|
+
|
10
|
+
def initialize(registry, transformer = nil)
|
11
|
+
@registry = registry
|
12
|
+
@transformer = transformer
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(ast)
|
16
|
+
ast.map(&method(:visit)).reduce(:>>)
|
17
|
+
end
|
18
|
+
|
19
|
+
def visit(node)
|
20
|
+
id, *rest = node
|
21
|
+
public_send(:"visit_#{id}", *rest)
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit_fn(node)
|
25
|
+
name, rest = node
|
26
|
+
args = rest.map { |arg| visit(arg) }
|
27
|
+
|
28
|
+
if registry.contain?(name)
|
29
|
+
registry[name, *args]
|
30
|
+
elsif transformer.respond_to?(name)
|
31
|
+
Function.new(transformer.method(name), name: name, args: args)
|
32
|
+
else
|
33
|
+
raise InvalidFunctionNameError, "function name +#{name}+ is not valid"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_arg(arg)
|
38
|
+
arg
|
39
|
+
end
|
40
|
+
|
41
|
+
def visit_t(node)
|
42
|
+
call(node)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/transproc/composer.rb
CHANGED
data/lib/transproc/composite.rb
CHANGED
data/lib/transproc/error.rb
CHANGED
data/lib/transproc/function.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'transproc/composite'
|
2
4
|
|
3
5
|
module Transproc
|
@@ -85,7 +87,8 @@ module Transproc
|
|
85
87
|
#
|
86
88
|
# @api public
|
87
89
|
def to_ast
|
88
|
-
|
90
|
+
args_ast = args.map { |arg| arg.respond_to?(:to_ast) ? arg.to_ast : arg }
|
91
|
+
[name, args_ast]
|
89
92
|
end
|
90
93
|
|
91
94
|
# Converts a transproc to a simple proc
|
data/lib/transproc/functions.rb
CHANGED
data/lib/transproc/hash.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'transproc/coercions'
|
2
4
|
|
3
5
|
module Transproc
|
@@ -17,20 +19,28 @@ module Transproc
|
|
17
19
|
module HashTransformations
|
18
20
|
extend Registry
|
19
21
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
22
|
+
EMPTY_HASH = {}.freeze
|
23
|
+
|
24
|
+
if RUBY_VERSION >= '2.5'
|
25
|
+
# Map all keys in a hash with the provided transformation function
|
26
|
+
#
|
27
|
+
# @example
|
28
|
+
# Transproc(:map_keys, -> s { s.upcase })['name' => 'Jane']
|
29
|
+
# # => {"NAME" => "Jane"}
|
30
|
+
#
|
31
|
+
# @param [Hash]
|
32
|
+
#
|
33
|
+
# @return [Hash]
|
34
|
+
#
|
35
|
+
# @api public
|
36
|
+
def self.map_keys(source_hash, fn)
|
37
|
+
Hash[source_hash].transform_keys!(&fn)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
def self.map_keys(source_hash, fn)
|
41
|
+
Hash[source_hash].tap do |hash|
|
42
|
+
hash.keys.each { |key| hash[fn[key]] = hash.delete(key) }
|
43
|
+
end
|
34
44
|
end
|
35
45
|
end
|
36
46
|
|
@@ -94,20 +104,55 @@ module Transproc
|
|
94
104
|
map_keys(hash, Coercions[:to_string].fn)
|
95
105
|
end
|
96
106
|
|
97
|
-
#
|
107
|
+
# Stringify keys in a hash recursively
|
98
108
|
#
|
99
109
|
# @example
|
100
|
-
#
|
101
|
-
#
|
110
|
+
# input = { :foo => "bar", :baz => [{ :one => 1 }] }
|
111
|
+
#
|
112
|
+
# t(:deep_stringify_keys)[input]
|
113
|
+
# # => { "foo" => "bar", "baz" => [{ "one" => 1 }] }
|
102
114
|
#
|
103
115
|
# @param [Hash]
|
104
116
|
#
|
105
117
|
# @return [Hash]
|
106
118
|
#
|
107
119
|
# @api public
|
108
|
-
def self.
|
109
|
-
|
110
|
-
|
120
|
+
def self.deep_stringify_keys(hash)
|
121
|
+
hash.each_with_object({}) do |(key, value), output|
|
122
|
+
output[key.to_s] =
|
123
|
+
case value
|
124
|
+
when Hash
|
125
|
+
deep_stringify_keys(value)
|
126
|
+
when Array
|
127
|
+
value.map { |item|
|
128
|
+
item.is_a?(Hash) ? deep_stringify_keys(item) : item
|
129
|
+
}
|
130
|
+
else
|
131
|
+
value
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
if RUBY_VERSION >= '2.4'
|
137
|
+
# Map all values in a hash using transformation function
|
138
|
+
#
|
139
|
+
# @example
|
140
|
+
# Transproc(:map_values, -> v { v.upcase })[:name => 'Jane']
|
141
|
+
# # => {"name" => "JANE"}
|
142
|
+
#
|
143
|
+
# @param [Hash]
|
144
|
+
#
|
145
|
+
# @return [Hash]
|
146
|
+
#
|
147
|
+
# @api public
|
148
|
+
def self.map_values(source_hash, fn)
|
149
|
+
Hash[source_hash].transform_values!(&fn)
|
150
|
+
end
|
151
|
+
else
|
152
|
+
def self.map_values(source_hash, fn)
|
153
|
+
Hash[source_hash].tap do |hash|
|
154
|
+
hash.each { |key, value| hash[key] = fn[value] }
|
155
|
+
end
|
111
156
|
end
|
112
157
|
end
|
113
158
|
|
@@ -167,20 +212,26 @@ module Transproc
|
|
167
212
|
Hash[hash].reject { |k, _| keys.include?(k) }
|
168
213
|
end
|
169
214
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
215
|
+
if RUBY_VERSION >= '2.5'
|
216
|
+
# Accepts specified keys from a hash
|
217
|
+
#
|
218
|
+
# @example
|
219
|
+
# Transproc(:accept_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
|
220
|
+
# # => {:name=>"Jane"}
|
221
|
+
#
|
222
|
+
# @param [Hash] hash The input hash
|
223
|
+
# @param [Array] keys The keys to be accepted
|
224
|
+
#
|
225
|
+
# @return [Hash]
|
226
|
+
#
|
227
|
+
# @api public
|
228
|
+
def self.accept_keys(hash, keys)
|
229
|
+
Hash[hash].slice(*keys)
|
230
|
+
end
|
231
|
+
else
|
232
|
+
def self.accept_keys(hash, keys)
|
233
|
+
reject_keys(hash, hash.keys - keys)
|
234
|
+
end
|
184
235
|
end
|
185
236
|
|
186
237
|
# Map a key in a hash with the provided transformation function
|
@@ -209,18 +260,26 @@ module Transproc
|
|
209
260
|
# @return [Hash]
|
210
261
|
#
|
211
262
|
# @api public
|
212
|
-
def self.nest(
|
213
|
-
|
263
|
+
def self.nest(hash, root, keys)
|
264
|
+
child = {}
|
214
265
|
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
266
|
+
keys.each do |key|
|
267
|
+
child[key] = hash[key] if hash.key?(key)
|
268
|
+
end
|
269
|
+
|
270
|
+
output = Hash[hash]
|
271
|
+
|
272
|
+
child.each_key { |key| output.delete(key) }
|
273
|
+
|
274
|
+
old_root = hash[root]
|
275
|
+
|
276
|
+
if old_root.is_a?(Hash)
|
277
|
+
output[root] = old_root.merge(child)
|
221
278
|
else
|
222
|
-
|
279
|
+
output[root] = child
|
223
280
|
end
|
281
|
+
|
282
|
+
output
|
224
283
|
end
|
225
284
|
|
226
285
|
# Collapse a nested hash from a specified key
|
@@ -239,8 +298,9 @@ module Transproc
|
|
239
298
|
# @return [Hash]
|
240
299
|
#
|
241
300
|
# @api public
|
242
|
-
def self.unwrap(source_hash, root, selected = nil,
|
301
|
+
def self.unwrap(source_hash, root, selected = nil, options = EMPTY_HASH)
|
243
302
|
return source_hash unless source_hash[root]
|
303
|
+
options, selected = selected, nil if options.empty? && selected.is_a?(::Hash)
|
244
304
|
|
245
305
|
add_prefix = ->(key) do
|
246
306
|
combined = [root, key].join('_')
|
@@ -251,7 +311,7 @@ module Transproc
|
|
251
311
|
nested_hash = hash[root]
|
252
312
|
keys = nested_hash.keys
|
253
313
|
keys &= selected if selected
|
254
|
-
new_keys = prefix ? keys.map(&add_prefix) : keys
|
314
|
+
new_keys = options[:prefix] ? keys.map(&add_prefix) : keys
|
255
315
|
|
256
316
|
hash.update(Hash[new_keys.zip(keys.map { |key| nested_hash.delete(key) })])
|
257
317
|
hash.delete(root) if nested_hash.empty?
|
data/lib/transproc/proc.rb
CHANGED
data/lib/transproc/recursion.rb
CHANGED
data/lib/transproc/registry.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
# frozen_string_literal: true
|
2
3
|
|
3
4
|
module Transproc
|
4
5
|
# Container to define transproc functions in, and access them via `[]` method
|
@@ -46,8 +47,9 @@ module Transproc
|
|
46
47
|
#
|
47
48
|
def [](fn, *args)
|
48
49
|
fetched = fetch(fn)
|
49
|
-
|
50
|
-
Function.new(fetched, args: args, name: fn)
|
50
|
+
|
51
|
+
return Function.new(fetched, args: args, name: fn) unless already_wrapped?(fetched)
|
52
|
+
args.empty? ? fetched : fetched.with(*args)
|
51
53
|
end
|
52
54
|
alias_method :t, :[]
|
53
55
|
|
data/lib/transproc/store.rb
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'transproc/transformer/class_interface'
|
4
|
+
require 'transproc/transformer/deprecated/class_interface'
|
2
5
|
|
3
6
|
module Transproc
|
4
7
|
# Transfomer class for defining transprocs with a class DSL.
|
@@ -45,6 +48,9 @@ module Transproc
|
|
45
48
|
# @api public
|
46
49
|
class Transformer
|
47
50
|
extend ClassInterface
|
51
|
+
extend Deprecated::ClassInterface
|
52
|
+
|
53
|
+
attr_reader :transproc
|
48
54
|
|
49
55
|
# Execute the transformation pipeline with the given input.
|
50
56
|
#
|
@@ -63,7 +69,7 @@ module Transproc
|
|
63
69
|
#
|
64
70
|
# @api public
|
65
71
|
def call(input)
|
66
|
-
|
72
|
+
transproc.call(input)
|
67
73
|
end
|
68
74
|
end
|
69
75
|
end
|