dry-transformer 0.1.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.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +12 -0
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +66 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +16 -0
  10. data/.rspec +4 -0
  11. data/.rubocop.yml +95 -0
  12. data/CHANGELOG.md +3 -0
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +19 -0
  16. data/LICENSE +20 -0
  17. data/README.md +29 -0
  18. data/Rakefile +6 -0
  19. data/docsite/source/built-in-transformations.html.md +47 -0
  20. data/docsite/source/index.html.md +15 -0
  21. data/docsite/source/transformation-objects.html.md +32 -0
  22. data/docsite/source/using-standalone-functions.html.md +82 -0
  23. data/dry-transformer.gemspec +22 -0
  24. data/lib/dry-transformer.rb +3 -0
  25. data/lib/dry/transformer.rb +23 -0
  26. data/lib/dry/transformer/all.rb +11 -0
  27. data/lib/dry/transformer/array.rb +183 -0
  28. data/lib/dry/transformer/array/combine.rb +65 -0
  29. data/lib/dry/transformer/class.rb +56 -0
  30. data/lib/dry/transformer/coercions.rb +196 -0
  31. data/lib/dry/transformer/compiler.rb +47 -0
  32. data/lib/dry/transformer/composite.rb +54 -0
  33. data/lib/dry/transformer/conditional.rb +76 -0
  34. data/lib/dry/transformer/constants.rb +7 -0
  35. data/lib/dry/transformer/error.rb +16 -0
  36. data/lib/dry/transformer/function.rb +109 -0
  37. data/lib/dry/transformer/hash.rb +453 -0
  38. data/lib/dry/transformer/pipe.rb +75 -0
  39. data/lib/dry/transformer/pipe/class_interface.rb +115 -0
  40. data/lib/dry/transformer/pipe/dsl.rb +58 -0
  41. data/lib/dry/transformer/proc.rb +46 -0
  42. data/lib/dry/transformer/recursion.rb +121 -0
  43. data/lib/dry/transformer/registry.rb +150 -0
  44. data/lib/dry/transformer/store.rb +128 -0
  45. data/lib/dry/transformer/version.rb +7 -0
  46. data/spec/spec_helper.rb +31 -0
  47. data/spec/unit/array/combine_spec.rb +224 -0
  48. data/spec/unit/array_transformations_spec.rb +233 -0
  49. data/spec/unit/class_transformations_spec.rb +50 -0
  50. data/spec/unit/coercions_spec.rb +132 -0
  51. data/spec/unit/conditional_spec.rb +48 -0
  52. data/spec/unit/function_not_found_error_spec.rb +12 -0
  53. data/spec/unit/function_spec.rb +193 -0
  54. data/spec/unit/hash_transformations_spec.rb +490 -0
  55. data/spec/unit/proc_transformations_spec.rb +20 -0
  56. data/spec/unit/recursion_spec.rb +145 -0
  57. data/spec/unit/registry_spec.rb +202 -0
  58. data/spec/unit/store_spec.rb +198 -0
  59. data/spec/unit/transformer/class_interface_spec.rb +350 -0
  60. data/spec/unit/transformer/dsl_spec.rb +15 -0
  61. data/spec/unit/transformer/instance_methods_spec.rb +25 -0
  62. metadata +119 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/transformer/pipe/class_interface'
4
+
5
+ module Dry
6
+ module Transformer
7
+ # Pipe class for defining transprocs with a class DSL.
8
+ #
9
+ # @example
10
+ # require 'anima'
11
+ # require 'dry/transformer/all'
12
+ #
13
+ # class User
14
+ # include Anima.new(:name, :address)
15
+ # end
16
+ #
17
+ # class Address
18
+ # include Anima.new(:city, :street, :zipcode)
19
+ # end
20
+ #
21
+ # class UsersMapper < Dry::Transformer::Pipe
22
+ # map_array do
23
+ # symbolize_keys
24
+ # rename_keys user_name: :name
25
+ # nest :address, %i(city street zipcode)
26
+ # map_value :address do
27
+ # constructor_inject Address
28
+ # end
29
+ # constructor_inject User
30
+ # end
31
+ # end
32
+ #
33
+ # UsersMapper.new.call(
34
+ # [
35
+ # { 'user_name' => 'Jane',
36
+ # 'city' => 'NYC',
37
+ # 'street' => 'Street 1',
38
+ # 'zipcode' => '123'
39
+ # }
40
+ # ]
41
+ # )
42
+ # # => [
43
+ # #<User
44
+ # name="Jane"
45
+ # address=#<Address city="NYC" street="Street 1" zipcode="123">>
46
+ # ]
47
+ #
48
+ # @api public
49
+ class Pipe
50
+ extend ClassInterface
51
+
52
+ attr_reader :transproc
53
+
54
+ # Execute the transformation pipeline with the given input.
55
+ #
56
+ # @example
57
+ #
58
+ # class SymbolizeKeys < Dry::Transformer
59
+ # symbolize_keys
60
+ # end
61
+ #
62
+ # SymbolizeKeys.new.call('name' => 'Jane')
63
+ # # => {:name=>"Jane"}
64
+ #
65
+ # @param [mixed] input The input to pass to the pipeline
66
+ #
67
+ # @return [mixed] output The output returned from the pipeline
68
+ #
69
+ # @api public
70
+ def call(input)
71
+ transproc.call(input)
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/transformer/pipe/dsl'
4
+
5
+ module Dry
6
+ module Transformer
7
+ class Pipe
8
+ # @api public
9
+ module ClassInterface
10
+ # @api private
11
+ attr_reader :dsl
12
+
13
+ # Return a base Dry::Transformer class with the
14
+ # container configured to the passed argument.
15
+ #
16
+ # @example
17
+ #
18
+ # class MyTransformer < Dry::Transformer[Transproc]
19
+ # end
20
+ #
21
+ # @param [Transproc::Registry] container
22
+ # The container to resolve transprocs from
23
+ #
24
+ # @return [subclass of Dry::Transformer]
25
+ #
26
+ # @api public
27
+ def [](container)
28
+ klass = Class.new(self)
29
+ klass.container(container)
30
+ klass
31
+ end
32
+
33
+ # @api private
34
+ def inherited(subclass)
35
+ super
36
+
37
+ subclass.container(@container) if defined?(@container)
38
+
39
+ subclass.instance_variable_set('@dsl', dsl.dup) if dsl
40
+ end
41
+
42
+ # Get or set the container to resolve transprocs from.
43
+ #
44
+ # @example
45
+ #
46
+ # # Setter
47
+ # Dry::Transformer.container(Transproc)
48
+ # # => Transproc
49
+ #
50
+ # # Getter
51
+ # Dry::Transformer.container
52
+ # # => Transproc
53
+ #
54
+ # @param [Transproc::Registry] container
55
+ # The container to resolve transprocs from
56
+ #
57
+ # @return [Transproc::Registry]
58
+ #
59
+ # @api private
60
+ def container(container = Undefined)
61
+ if container.equal?(Undefined)
62
+ @container ||= Module.new.extend(Dry::Transformer::Registry)
63
+ else
64
+ @container = container
65
+ end
66
+ end
67
+
68
+ # @api public
69
+ def import(*args)
70
+ container.import(*args)
71
+ end
72
+
73
+ # @api public
74
+ def define!(&block)
75
+ @dsl ||= DSL.new(container)
76
+ @dsl.instance_eval(&block)
77
+ self
78
+ end
79
+
80
+ # @api public
81
+ def new(*)
82
+ super.tap do |transformer|
83
+ transformer.instance_variable_set('@transproc', dsl.(transformer)) if dsl
84
+ end
85
+ end
86
+ ruby2_keywords(:new) if respond_to?(:ruby2_keywords, true)
87
+
88
+ # Get a transformation from the container,
89
+ # without adding it to the transformation pipeline
90
+ #
91
+ # @example
92
+ #
93
+ # class Stringify < Dry::Transformer
94
+ # map_values t(:to_string)
95
+ # end
96
+ #
97
+ # Stringify.new.call(a: 1, b: 2)
98
+ # # => {a: '1', b: '2'}
99
+ #
100
+ # @param [Proc, Symbol] fn
101
+ # A proc, a name of the module's own function, or a name of imported
102
+ # procedure from another module
103
+ # @param [Object, Array] args
104
+ # Args to be carried by the transproc
105
+ #
106
+ # @return [Transproc::Function]
107
+ #
108
+ # @api public
109
+ def t(fn, *args)
110
+ container[fn, *args]
111
+ end
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/transformer/compiler'
4
+
5
+ module Dry
6
+ module Transformer
7
+ class Pipe
8
+ # @api public
9
+ class DSL
10
+ # @api private
11
+ attr_reader :container
12
+
13
+ # @api private
14
+ attr_reader :ast
15
+
16
+ # @api private
17
+ def initialize(container, ast: [], &block)
18
+ @container = container
19
+ @ast = ast
20
+ instance_eval(&block) if block
21
+ end
22
+
23
+ # @api public
24
+ def t(name, *args)
25
+ container[name, *args]
26
+ end
27
+
28
+ # @api private
29
+ def dup
30
+ self.class.new(container, ast: ast.dup)
31
+ end
32
+
33
+ # @api private
34
+ def call(transformer)
35
+ Compiler.new(container, transformer).(ast)
36
+ end
37
+
38
+ private
39
+
40
+ # @api private
41
+ def node(&block)
42
+ [:t, self.class.new(container, &block).ast]
43
+ end
44
+
45
+ # @api private
46
+ def respond_to_missing?(method, _include_private = false)
47
+ super || container.contain?(method)
48
+ end
49
+
50
+ # @api private
51
+ def method_missing(meth, *args, &block)
52
+ arg_nodes = *args.map { |a| [:arg, a] }
53
+ ast << [:fn, (block ? [meth, [*arg_nodes, node(&block)]] : [meth, arg_nodes])]
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Transformer
5
+ # Transformation functions for Procs
6
+ #
7
+ # @example
8
+ # require 'ostruct'
9
+ # require 'dry/transformer/proc'
10
+ #
11
+ # include Dry::Transformer::Helper
12
+ #
13
+ # fn = t(
14
+ # :map_value,
15
+ # 'foo_bar',
16
+ # t(:bind, OpenStruct.new(prefix: 'foo'), -> s { [prefix, s].join('_') })
17
+ # )
18
+ #
19
+ # fn["foo_bar" => "bar"]
20
+ # # => {"foo_bar" => "foo_bar"}
21
+ #
22
+ # @api public
23
+ module ProcTransformations
24
+ extend Registry
25
+
26
+ # Change the binding for the given function
27
+ #
28
+ # @example
29
+ # Dry::Transformer(
30
+ # :bind,
31
+ # OpenStruct.new(prefix: 'foo'),
32
+ # -> s { [prefix, s].join('_') }
33
+ # )['bar']
34
+ # # => "foo_bar"
35
+ #
36
+ # @param [Proc]
37
+ #
38
+ # @return [Proc]
39
+ #
40
+ # @api public
41
+ def self.bind(value, binding, fn)
42
+ binding.instance_exec(value, &fn)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'dry/transformer/conditional'
4
+
5
+ module Dry
6
+ module Transformer
7
+ # Recursive transformation functions
8
+ #
9
+ # @example
10
+ # require 'dry/transformer/recursion'
11
+ #
12
+ # include Dry::Transformer::Helper
13
+ #
14
+ # fn = t(:hash_recursion, t(:symbolize_keys))
15
+ #
16
+ # fn["name" => "Jane", "address" => { "street" => "Street 1" }]
17
+ # # => {:name=>"Jane", :address=>{:street=>"Street 1"}}
18
+ #
19
+ # @api public
20
+ module Recursion
21
+ extend Registry
22
+
23
+ IF_ENUMERABLE = -> fn { Conditional[:is, Enumerable, fn] }
24
+
25
+ IF_ARRAY = -> fn { Conditional[:is, Array, fn] }
26
+
27
+ IF_HASH = -> fn { Conditional[:is, Hash, fn] }
28
+
29
+ # Recursively apply the provided transformation function to an enumerable
30
+ #
31
+ # @example
32
+ # Dry::Transformer(:recursion, Dry::Transformer(:is, ::Hash, Dry::Transformer(:symbolize_keys)))[
33
+ # {
34
+ # 'id' => 1,
35
+ # 'name' => 'Jane',
36
+ # 'tasks' => [
37
+ # { 'id' => 1, 'description' => 'Write some code' },
38
+ # { 'id' => 2, 'description' => 'Write some more code' }
39
+ # ]
40
+ # }
41
+ # ]
42
+ # => {
43
+ # :id=>1,
44
+ # :name=>"Jane",
45
+ # :tasks=>[
46
+ # {:id=>1, :description=>"Write some code"},
47
+ # {:id=>2, :description=>"Write some more code"}
48
+ # ]
49
+ # }
50
+ #
51
+ # @param [Enumerable]
52
+ #
53
+ # @return [Enumerable]
54
+ #
55
+ # @api public
56
+ def self.recursion(value, fn)
57
+ result = fn[value]
58
+ guarded = IF_ENUMERABLE[-> v { recursion(v, fn) }]
59
+
60
+ case result
61
+ when ::Hash
62
+ result.keys.each do |key|
63
+ result[key] = guarded[result.delete(key)]
64
+ end
65
+ when ::Array
66
+ result.map! do |item|
67
+ guarded[item]
68
+ end
69
+ end
70
+
71
+ result
72
+ end
73
+
74
+ # Recursively apply the provided transformation function to an array
75
+ #
76
+ # @example
77
+ # Dry::Transformer(:array_recursion, -> s { s.compact })[
78
+ # [['Joe', 'Jane', nil], ['Smith', 'Doe', nil]]
79
+ # ]
80
+ # # => [["Joe", "Jane"], ["Smith", "Doe"]]
81
+ #
82
+ # @param [Array]
83
+ #
84
+ # @return [Array]
85
+ #
86
+ # @api public
87
+ def self.array_recursion(value, fn)
88
+ result = fn[value]
89
+ guarded = IF_ARRAY[-> v { array_recursion(v, fn) }]
90
+
91
+ result.map! do |item|
92
+ guarded[item]
93
+ end
94
+ end
95
+
96
+ # Recursively apply the provided transformation function to a hash
97
+ #
98
+ # @example
99
+ # Dry::Transformer(:hash_recursion, Dry::Transformer(:symbolize_keys))[
100
+ # ["name" => "Jane", "address" => { "street" => "Street 1", "zipcode" => "123" }]
101
+ # ]
102
+ # # => {:name=>"Jane", :address=>{:street=>"Street 1", :zipcode=>"123"}}
103
+ #
104
+ # @param [Hash]
105
+ #
106
+ # @return [Hash]
107
+ #
108
+ # @api public
109
+ def self.hash_recursion(value, fn)
110
+ result = fn[value]
111
+ guarded = IF_HASH[-> v { hash_recursion(v, fn) }]
112
+
113
+ result.keys.each do |key|
114
+ result[key] = guarded[result.delete(key)]
115
+ end
116
+
117
+ result
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Transformer
5
+ # Container to define transproc functions in, and access them via `[]` method
6
+ # from the outside of the module
7
+ #
8
+ # @example
9
+ # module FooMethods
10
+ # extend Dry::Transformer::Registry
11
+ #
12
+ # def self.foo(name, prefix)
13
+ # [prefix, '_', name].join
14
+ # end
15
+ # end
16
+ #
17
+ # fn = FooMethods[:foo, 'baz']
18
+ # fn['qux'] # => 'qux_baz'
19
+ #
20
+ # module BarMethods
21
+ # extend FooMethods
22
+ #
23
+ # def self.bar(*args)
24
+ # foo(*args).upcase
25
+ # end
26
+ # end
27
+ #
28
+ # fn = BarMethods[:foo, 'baz']
29
+ # fn['qux'] # => 'qux_baz'
30
+ #
31
+ # fn = BarMethods[:bar, 'baz']
32
+ # fn['qux'] # => 'QUX_BAZ'
33
+ #
34
+ # @api public
35
+ module Registry
36
+ # Builds the transformation
37
+ #
38
+ # @param [Proc, Symbol] fn
39
+ # A proc, a name of the module's own function, or a name of imported
40
+ # procedure from another module
41
+ # @param [Object, Array] args
42
+ # Args to be carried by the transproc
43
+ #
44
+ # @return [Dry::Transformer::Function]
45
+ #
46
+ # @alias :t
47
+ #
48
+ def [](fn, *args)
49
+ fetched = fetch(fn)
50
+
51
+ return Function.new(fetched, args: args, name: fn) unless already_wrapped?(fetched)
52
+
53
+ args.empty? ? fetched : fetched.with(*args)
54
+ end
55
+ alias_method :t, :[]
56
+
57
+ # Returns wether the registry contains such transformation by its key
58
+ #
59
+ # @param [Symbol] key
60
+ #
61
+ # @return [Boolean]
62
+ #
63
+ def contain?(key)
64
+ respond_to?(key) || store.contain?(key)
65
+ end
66
+
67
+ # Register a new function
68
+ #
69
+ # @example
70
+ # store.register(:to_json, -> v { v.to_json })
71
+
72
+ # store.register(:to_json) { |v| v.to_json }
73
+ #
74
+ def register(name, fn = nil, &block)
75
+ if contain?(name)
76
+ raise FunctionAlreadyRegisteredError, "Function #{name} is already defined"
77
+ end
78
+
79
+ @store = store.register(name, fn, &block)
80
+ self
81
+ end
82
+
83
+ # Imports either a method (converted to a proc) from another module, or
84
+ # all methods from that module.
85
+ #
86
+ # If the external module is a registry, looks for its imports too.
87
+ #
88
+ # @overload import(source)
89
+ # Loads all methods from the source object
90
+ #
91
+ # @param [Object] source
92
+ #
93
+ # @overload import(*names, **options)
94
+ # Loads selected methods from the source object
95
+ #
96
+ # @param [Array<Symbol>] names
97
+ # @param [Hash] options
98
+ # @options options [Object] :from The source object
99
+ #
100
+ # @overload import(name, **options)
101
+ # Loads selected methods from the source object
102
+ #
103
+ # @param [Symbol] name
104
+ # @param [Hash] options
105
+ # @options options [Object] :from The source object
106
+ # @options options [Object] :as The new name for the transformation
107
+ #
108
+ # @return [itself] self
109
+ #
110
+ # @alias :import
111
+ #
112
+ def import(*args)
113
+ @store = store.import(*args)
114
+ self
115
+ end
116
+ alias_method :uses, :import
117
+
118
+ # The store of procedures imported from external modules
119
+ #
120
+ # @return [Dry::Transformer::Store]
121
+ #
122
+ def store
123
+ @store ||= Store.new
124
+ end
125
+
126
+ # Gets the procedure for creating a transproc
127
+ #
128
+ # @param [#call, Symbol] fn
129
+ # Either the procedure, or the name of the method of the current module,
130
+ # or the registered key of imported procedure in a store.
131
+ #
132
+ # @return [#call]
133
+ #
134
+ def fetch(fn)
135
+ return fn unless fn.instance_of? Symbol
136
+
137
+ respond_to?(fn) ? method(fn) : store.fetch(fn)
138
+ rescue StandardError
139
+ raise FunctionNotFoundError.new(fn, self)
140
+ end
141
+
142
+ private
143
+
144
+ # @api private
145
+ def already_wrapped?(func)
146
+ func.is_a?(Dry::Transformer::Function) || func.is_a?(Dry::Transformer::Composite)
147
+ end
148
+ end
149
+ end
150
+ end