transproc 1.0.0 → 1.1.1

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.
Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +15 -0
  3. data/.gitignore +3 -0
  4. data/.travis.yml +14 -10
  5. data/CHANGELOG.md +61 -17
  6. data/Gemfile +9 -18
  7. data/README.md +17 -19
  8. data/Rakefile +1 -0
  9. data/lib/transproc.rb +3 -4
  10. data/lib/transproc/all.rb +2 -0
  11. data/lib/transproc/array.rb +9 -51
  12. data/lib/transproc/array/combine.rb +61 -0
  13. data/lib/transproc/class.rb +2 -0
  14. data/lib/transproc/coercions.rb +2 -0
  15. data/lib/transproc/compiler.rb +45 -0
  16. data/lib/transproc/composer.rb +2 -0
  17. data/lib/transproc/composite.rb +2 -0
  18. data/lib/transproc/conditional.rb +2 -0
  19. data/lib/transproc/constants.rb +5 -0
  20. data/lib/transproc/error.rb +2 -0
  21. data/lib/transproc/function.rb +4 -1
  22. data/lib/transproc/functions.rb +2 -0
  23. data/lib/transproc/hash.rb +105 -45
  24. data/lib/transproc/proc.rb +2 -0
  25. data/lib/transproc/recursion.rb +2 -0
  26. data/lib/transproc/registry.rb +4 -2
  27. data/lib/transproc/store.rb +1 -0
  28. data/lib/transproc/support/deprecations.rb +2 -0
  29. data/lib/transproc/transformer.rb +7 -1
  30. data/lib/transproc/transformer/class_interface.rb +32 -65
  31. data/lib/transproc/transformer/deprecated/class_interface.rb +81 -0
  32. data/lib/transproc/transformer/dsl.rb +51 -0
  33. data/lib/transproc/version.rb +3 -1
  34. data/spec/spec_helper.rb +21 -10
  35. data/spec/unit/array/combine_spec.rb +224 -0
  36. data/spec/unit/array_transformations_spec.rb +1 -52
  37. data/spec/unit/class_transformations_spec.rb +10 -7
  38. data/spec/unit/coercions_spec.rb +1 -1
  39. data/spec/unit/composer_spec.rb +1 -1
  40. data/spec/unit/conditional_spec.rb +1 -1
  41. data/spec/unit/function_not_found_error_spec.rb +1 -1
  42. data/spec/unit/function_spec.rb +16 -1
  43. data/spec/unit/hash_transformations_spec.rb +12 -1
  44. data/spec/unit/proc_transformations_spec.rb +3 -1
  45. data/spec/unit/recursion_spec.rb +1 -1
  46. data/spec/unit/registry_spec.rb +9 -1
  47. data/spec/unit/store_spec.rb +2 -1
  48. data/spec/unit/transformer/class_interface_spec.rb +364 -0
  49. data/spec/unit/transformer/dsl_spec.rb +15 -0
  50. data/spec/unit/transformer/instance_methods_spec.rb +25 -0
  51. data/spec/unit/transformer_spec.rb +128 -40
  52. data/spec/unit/transproc_spec.rb +1 -1
  53. data/transproc.gemspec +1 -5
  54. metadata +19 -54
  55. data/.rubocop.yml +0 -66
  56. data/.rubocop_todo.yml +0 -11
  57. data/rakelib/mutant.rake +0 -16
  58. data/rakelib/rubocop.rake +0 -18
  59. 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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
4
  # Transformation functions for Classes
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'time'
3
5
  require 'bigdecimal'
@@ -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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'transproc/support/deprecations'
2
4
 
3
5
  module Transproc
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
4
  # Composition of two functions
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
4
  # Conditional transformation functions
3
5
  #
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Transproc
4
+ Undefined = Object.new.freeze
5
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
4
  Error = Class.new(StandardError)
3
5
  FunctionAlreadyRegisteredError = Class.new(Error)
@@ -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
- [name, args]
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
4
  # Function container extension
3
5
  #
@@ -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
- # 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 self.map_keys(source_hash, fn)
32
- Hash[source_hash].tap do |hash|
33
- hash.keys.each { |key| hash[fn[key]] = hash.delete(key) }
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
- # Map all values in a hash using transformation function
107
+ # Stringify keys in a hash recursively
98
108
  #
99
109
  # @example
100
- # Transproc(:map_values, -> v { v.upcase })[:name => 'Jane']
101
- # # => {"name" => "JANE"}
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.map_values(source_hash, fn)
109
- Hash[source_hash].tap do |hash|
110
- hash.each { |key, value| hash[key] = fn[value] }
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
- # Accepts specified keys from a hash
171
- #
172
- # @example
173
- # Transproc(:accept_keys, [:name])[name: 'Jane', email: 'jane@doe.org']
174
- # # => {:name=>"Jane"}
175
- #
176
- # @param [Hash] hash The input hash
177
- # @param [Array] keys The keys to be accepted
178
- #
179
- # @return [Hash]
180
- #
181
- # @api public
182
- def self.accept_keys(hash, keys)
183
- reject_keys(hash, hash.keys - keys)
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(source_hash, root, keys)
213
- nest_keys = source_hash.keys & keys
263
+ def self.nest(hash, root, keys)
264
+ child = {}
214
265
 
215
- if !nest_keys.empty?
216
- hash = Hash[source_hash]
217
- child = Hash[nest_keys.zip(nest_keys.map { |key| hash.delete(key) })]
218
- old_nest = hash[root]
219
- new_nest = old_nest.is_a?(Hash) ? old_nest.merge(child) : child
220
- hash.merge(root => new_nest)
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
- source_hash.merge(root => {})
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, prefix: false)
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?
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
4
  # Transformation functions for Procs
3
5
  #
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'transproc/conditional'
2
4
 
3
5
  module Transproc
@@ -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
- return fetched if already_wrapped?(fetched)
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
 
@@ -1,4 +1,5 @@
1
1
  # encoding: utf-8
2
+ # frozen_string_literal: true
2
3
 
3
4
  module Transproc
4
5
  # Immutable collection of named procedures from external modules
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Transproc
2
4
  module Deprecations
3
5
  def self.announce(name, msg)
@@ -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
- self.class.transproc.call(input)
72
+ transproc.call(input)
67
73
  end
68
74
  end
69
75
  end