transproc 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
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