dry-transformer 0.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +11 -0
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/dry-transformer.gemspec +29 -0
- data/lib/dry-transformer.rb +3 -0
- data/lib/dry/transformer.rb +23 -0
- data/lib/dry/transformer/all.rb +11 -0
- data/lib/dry/transformer/array.rb +183 -0
- data/lib/dry/transformer/array/combine.rb +65 -0
- data/lib/dry/transformer/class.rb +56 -0
- data/lib/dry/transformer/coercions.rb +196 -0
- data/lib/dry/transformer/compiler.rb +47 -0
- data/lib/dry/transformer/composite.rb +54 -0
- data/lib/dry/transformer/conditional.rb +76 -0
- data/lib/dry/transformer/constants.rb +7 -0
- data/lib/dry/transformer/error.rb +16 -0
- data/lib/dry/transformer/function.rb +109 -0
- data/lib/dry/transformer/hash.rb +454 -0
- data/lib/dry/transformer/pipe.rb +75 -0
- data/lib/dry/transformer/pipe/class_interface.rb +115 -0
- data/lib/dry/transformer/pipe/dsl.rb +58 -0
- data/lib/dry/transformer/proc.rb +46 -0
- data/lib/dry/transformer/recursion.rb +121 -0
- data/lib/dry/transformer/registry.rb +150 -0
- data/lib/dry/transformer/store.rb +128 -0
- data/lib/dry/transformer/version.rb +7 -0
- metadata +73 -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
|