dry-transformer 0.1.0

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