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,196 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'date'
|
4
|
+
require 'time'
|
5
|
+
require 'bigdecimal'
|
6
|
+
require 'bigdecimal/util'
|
7
|
+
|
8
|
+
module Dry
|
9
|
+
module Transformer
|
10
|
+
# Coercion functions for common types
|
11
|
+
#
|
12
|
+
# @api public
|
13
|
+
module Coercions
|
14
|
+
extend Registry
|
15
|
+
|
16
|
+
TRUE_VALUES = [true, 1, '1', 'on', 't', 'true', 'y', 'yes'].freeze
|
17
|
+
FALSE_VALUES = [false, 0, '0', 'off', 'f', 'false', 'n', 'no', nil].freeze
|
18
|
+
|
19
|
+
BOOLEAN_MAP = Hash[
|
20
|
+
TRUE_VALUES.product([true]) + FALSE_VALUES.product([false])
|
21
|
+
].freeze
|
22
|
+
|
23
|
+
# Does nothing and returns a value
|
24
|
+
#
|
25
|
+
# @example
|
26
|
+
# fn = Coercions[:identity]
|
27
|
+
# fn[:foo] # => :foo
|
28
|
+
#
|
29
|
+
# @param [Object] value
|
30
|
+
#
|
31
|
+
# @return [Object]
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def self.identity(value = nil)
|
35
|
+
value
|
36
|
+
end
|
37
|
+
|
38
|
+
# Coerce value into a string
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# Dry::Transformer(:to_string)[1]
|
42
|
+
# # => "1"
|
43
|
+
#
|
44
|
+
# @param [Object] value The input value
|
45
|
+
#
|
46
|
+
# @return [String]
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def self.to_string(value)
|
50
|
+
value.to_s
|
51
|
+
end
|
52
|
+
|
53
|
+
# Coerce value into a symbol
|
54
|
+
#
|
55
|
+
# @example
|
56
|
+
# Dry::Transformer(:to_symbol)['foo']
|
57
|
+
# # => :foo
|
58
|
+
#
|
59
|
+
# @param [#to_s] value The input value
|
60
|
+
#
|
61
|
+
# @return [Symbol]
|
62
|
+
#
|
63
|
+
# @api public
|
64
|
+
def self.to_symbol(value)
|
65
|
+
value.to_s.to_sym
|
66
|
+
end
|
67
|
+
|
68
|
+
# Coerce value into a integer
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# Dry::Transformer(:to_integer)['1']
|
72
|
+
# # => 1
|
73
|
+
#
|
74
|
+
# @param [Object] value The input value
|
75
|
+
#
|
76
|
+
# @return [Integer]
|
77
|
+
#
|
78
|
+
# @api public
|
79
|
+
def self.to_integer(value)
|
80
|
+
value.to_i
|
81
|
+
end
|
82
|
+
|
83
|
+
# Coerce value into a float
|
84
|
+
#
|
85
|
+
# @example
|
86
|
+
# Dry::Transformer(:to_float)['1.2']
|
87
|
+
# # => 1.2
|
88
|
+
#
|
89
|
+
# @param [Object] value The input value
|
90
|
+
#
|
91
|
+
# @return [Float]
|
92
|
+
#
|
93
|
+
# @api public
|
94
|
+
def self.to_float(value)
|
95
|
+
value.to_f
|
96
|
+
end
|
97
|
+
|
98
|
+
# Coerce value into a decimal
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# Dry::Transformer(:to_decimal)[1.2]
|
102
|
+
# # => #<BigDecimal:7fca32acea50,'0.12E1',18(36)>
|
103
|
+
#
|
104
|
+
# @param [Object] value The input value
|
105
|
+
#
|
106
|
+
# @return [Decimal]
|
107
|
+
#
|
108
|
+
# @api public
|
109
|
+
def self.to_decimal(value)
|
110
|
+
value.to_d
|
111
|
+
end
|
112
|
+
|
113
|
+
# Coerce value into a boolean
|
114
|
+
#
|
115
|
+
# @example
|
116
|
+
# Dry::Transformer(:to_boolean)['true']
|
117
|
+
# # => true
|
118
|
+
# Dry::Transformer(:to_boolean)['f']
|
119
|
+
# # => false
|
120
|
+
#
|
121
|
+
# @param [Object] value The input value
|
122
|
+
#
|
123
|
+
# @return [TrueClass,FalseClass]
|
124
|
+
#
|
125
|
+
# @api public
|
126
|
+
def self.to_boolean(value)
|
127
|
+
BOOLEAN_MAP.fetch(value)
|
128
|
+
end
|
129
|
+
|
130
|
+
# Coerce value into a date
|
131
|
+
#
|
132
|
+
# @example
|
133
|
+
# Dry::Transformer(:to_date)['2015-04-14']
|
134
|
+
# # => #<Date: 2015-04-14 ((2457127j,0s,0n),+0s,2299161j)>
|
135
|
+
#
|
136
|
+
# @param [Object] value The input value
|
137
|
+
#
|
138
|
+
# @return [Date]
|
139
|
+
#
|
140
|
+
# @api public
|
141
|
+
def self.to_date(value)
|
142
|
+
Date.parse(value)
|
143
|
+
end
|
144
|
+
|
145
|
+
# Coerce value into a time
|
146
|
+
#
|
147
|
+
# @example
|
148
|
+
# Dry::Transformer(:to_time)['2015-04-14 12:01:45']
|
149
|
+
# # => 2015-04-14 12:01:45 +0200
|
150
|
+
#
|
151
|
+
# @param [Object] value The input value
|
152
|
+
#
|
153
|
+
# @return [Time]
|
154
|
+
#
|
155
|
+
# @api public
|
156
|
+
def self.to_time(value)
|
157
|
+
Time.parse(value)
|
158
|
+
end
|
159
|
+
|
160
|
+
# Coerce value into a datetime
|
161
|
+
#
|
162
|
+
# @example
|
163
|
+
# Dry::Transformer(:to_datetime)['2015-04-14 12:01:45']
|
164
|
+
# # => #<DateTime: 2015-04-14T12:01:45+00:00 ((2457127j,43305s,0n),+0s,2299161j)>
|
165
|
+
#
|
166
|
+
# @param [Object] value The input value
|
167
|
+
#
|
168
|
+
# @return [DateTime]
|
169
|
+
#
|
170
|
+
# @api public
|
171
|
+
def self.to_datetime(value)
|
172
|
+
DateTime.parse(value)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Coerce value into an array containing tuples only
|
176
|
+
#
|
177
|
+
# If the source is not an array, or doesn't contain a tuple, returns
|
178
|
+
# an array with one empty tuple
|
179
|
+
#
|
180
|
+
# @example
|
181
|
+
# Dry::Transformer(:to_tuples)[:foo] # => [{}]
|
182
|
+
# Dry::Transformer(:to_tuples)[[]] # => [{}]
|
183
|
+
# Dry::Transformer(:to_tuples)[[{ foo: :FOO, :bar }]] # => [{ foo: :FOO }]
|
184
|
+
#
|
185
|
+
# @param [Object] value
|
186
|
+
#
|
187
|
+
# @return [Array<Hash>]
|
188
|
+
#
|
189
|
+
def self.to_tuples(value)
|
190
|
+
array = value.is_a?(Array) ? Array[*value] : [{}]
|
191
|
+
array.select! { |item| item.is_a?(Hash) }
|
192
|
+
array.any? ? array : [{}]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transformer
|
5
|
+
# @api private
|
6
|
+
class Compiler
|
7
|
+
InvalidFunctionNameError = Class.new(StandardError)
|
8
|
+
|
9
|
+
attr_reader :registry, :transformer
|
10
|
+
|
11
|
+
def initialize(registry, transformer = nil)
|
12
|
+
@registry = registry
|
13
|
+
@transformer = transformer
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(ast)
|
17
|
+
ast.map(&method(:visit)).reduce(:>>)
|
18
|
+
end
|
19
|
+
|
20
|
+
def visit(node)
|
21
|
+
id, *rest = node
|
22
|
+
public_send(:"visit_#{id}", *rest)
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit_fn(node)
|
26
|
+
name, rest = node
|
27
|
+
args = rest.map { |arg| visit(arg) }
|
28
|
+
|
29
|
+
if registry.contain?(name)
|
30
|
+
registry[name, *args]
|
31
|
+
elsif transformer.respond_to?(name)
|
32
|
+
Function.new(transformer.method(name), name: name, args: args)
|
33
|
+
else
|
34
|
+
raise InvalidFunctionNameError, "function name +#{name}+ is not valid"
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def visit_arg(arg)
|
39
|
+
arg
|
40
|
+
end
|
41
|
+
|
42
|
+
def visit_t(node)
|
43
|
+
call(node)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transformer
|
5
|
+
# Composition of two functions
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Composite
|
9
|
+
# @return [Proc]
|
10
|
+
#
|
11
|
+
# @api private
|
12
|
+
attr_reader :left
|
13
|
+
|
14
|
+
# @return [Proc]
|
15
|
+
#
|
16
|
+
# @api private
|
17
|
+
attr_reader :right
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
def initialize(left, right)
|
21
|
+
@left = left
|
22
|
+
@right = right
|
23
|
+
end
|
24
|
+
|
25
|
+
# Call right side with the result from the left side
|
26
|
+
#
|
27
|
+
# @param [Object] value The input value
|
28
|
+
#
|
29
|
+
# @return [Object]
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def call(value)
|
33
|
+
right.call(left.call(value))
|
34
|
+
end
|
35
|
+
alias_method :[], :call
|
36
|
+
|
37
|
+
# @see Function#compose
|
38
|
+
#
|
39
|
+
# @api public
|
40
|
+
def compose(other)
|
41
|
+
self.class.new(self, other)
|
42
|
+
end
|
43
|
+
alias_method :+, :compose
|
44
|
+
alias_method :>>, :compose
|
45
|
+
|
46
|
+
# @see Function#to_ast
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def to_ast
|
50
|
+
left.to_ast << right.to_ast
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transformer
|
5
|
+
# Conditional transformation functions
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# require 'dry/transformer/conditional'
|
9
|
+
#
|
10
|
+
# include Dry::Transformer::Helper
|
11
|
+
#
|
12
|
+
# fn = t(:guard, -> s { s.is_a?(::String) }, -> s { s.to_sym })
|
13
|
+
#
|
14
|
+
# [fn[2], fn['Jane']]
|
15
|
+
# # => [2, :Jane]
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
module Conditional
|
19
|
+
extend Registry
|
20
|
+
|
21
|
+
# Negates the result of transformation
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# fn = Conditional[:not, -> value { value.is_a? ::String }]
|
25
|
+
# fn[:foo] # => true
|
26
|
+
# fn["foo"] # => false
|
27
|
+
#
|
28
|
+
# @param [Object] value
|
29
|
+
# @param [Proc] fn
|
30
|
+
#
|
31
|
+
# @return [Boolean]
|
32
|
+
#
|
33
|
+
# @api public
|
34
|
+
def self.not(value, fn)
|
35
|
+
!fn[value]
|
36
|
+
end
|
37
|
+
|
38
|
+
# Apply the transformation function to subject if the predicate returns true, or return un-modified
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# [2, 'Jane'].map do |subject|
|
42
|
+
# Dry::Transformer(:guard, -> s { s.is_a?(::String) }, -> s { s.to_sym })[subject]
|
43
|
+
# end
|
44
|
+
# # => [2, :Jane]
|
45
|
+
#
|
46
|
+
# @param [Mixed]
|
47
|
+
#
|
48
|
+
# @return [Mixed]
|
49
|
+
#
|
50
|
+
# @api public
|
51
|
+
def self.guard(value, predicate, fn)
|
52
|
+
predicate[value] ? fn[value] : value
|
53
|
+
end
|
54
|
+
|
55
|
+
# Calls a function when type-check passes
|
56
|
+
#
|
57
|
+
# @example
|
58
|
+
# fn = Dry::Transformer(:is, Array, -> arr { arr.map(&:upcase) })
|
59
|
+
# fn.call(['a', 'b', 'c']) # => ['A', 'B', 'C']
|
60
|
+
#
|
61
|
+
# fn = Dry::Transformer(:is, Array, -> arr { arr.map(&:upcase) })
|
62
|
+
# fn.call('foo') # => "foo"
|
63
|
+
#
|
64
|
+
# @param [Object]
|
65
|
+
# @param [Class]
|
66
|
+
# @param [Proc]
|
67
|
+
#
|
68
|
+
# @return [Object]
|
69
|
+
#
|
70
|
+
# @api public
|
71
|
+
def self.is(value, type, fn)
|
72
|
+
guard(value, -> v { v.is_a?(type) }, fn)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transformer
|
5
|
+
Error = Class.new(StandardError)
|
6
|
+
FunctionAlreadyRegisteredError = Class.new(Error)
|
7
|
+
|
8
|
+
class FunctionNotFoundError < Error
|
9
|
+
def initialize(function, source = nil)
|
10
|
+
return super "No registered function #{source}[:#{function}]" if source
|
11
|
+
|
12
|
+
super "No globally registered function for #{function}"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'dry/transformer/composite'
|
4
|
+
|
5
|
+
module Dry
|
6
|
+
module Transformer
|
7
|
+
# Transformation proc wrapper allowing composition of multiple procs into
|
8
|
+
# a data-transformation pipeline.
|
9
|
+
#
|
10
|
+
# This is used by Dry::Transformer to wrap registered methods.
|
11
|
+
#
|
12
|
+
# @api private
|
13
|
+
class Function
|
14
|
+
# Wrapped proc or another composite function
|
15
|
+
#
|
16
|
+
# @return [Proc,Composed]
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
attr_reader :fn
|
20
|
+
|
21
|
+
# Additional arguments that will be passed to the wrapped proc
|
22
|
+
#
|
23
|
+
# @return [Array]
|
24
|
+
#
|
25
|
+
# @api private
|
26
|
+
attr_reader :args
|
27
|
+
|
28
|
+
# @!attribute [r] name
|
29
|
+
#
|
30
|
+
# @return [<type] The name of the function
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
attr_reader :name
|
34
|
+
|
35
|
+
# @api private
|
36
|
+
def initialize(fn, options = {})
|
37
|
+
@fn = fn
|
38
|
+
@args = options.fetch(:args, [])
|
39
|
+
@name = options.fetch(:name, fn)
|
40
|
+
end
|
41
|
+
|
42
|
+
# Call the wrapped proc
|
43
|
+
#
|
44
|
+
# @param [Object] value The input value
|
45
|
+
#
|
46
|
+
# @alias []
|
47
|
+
#
|
48
|
+
# @api public
|
49
|
+
def call(*value)
|
50
|
+
fn.call(*value, *args)
|
51
|
+
end
|
52
|
+
alias_method :[], :call
|
53
|
+
|
54
|
+
# Compose this function with another function or a proc
|
55
|
+
#
|
56
|
+
# @param [Proc,Function]
|
57
|
+
#
|
58
|
+
# @return [Composite]
|
59
|
+
#
|
60
|
+
# @alias :>>
|
61
|
+
#
|
62
|
+
# @api public
|
63
|
+
def compose(other)
|
64
|
+
Composite.new(self, other)
|
65
|
+
end
|
66
|
+
alias_method :+, :compose
|
67
|
+
alias_method :>>, :compose
|
68
|
+
|
69
|
+
# Return a new fn with curried args
|
70
|
+
#
|
71
|
+
# @return [Function]
|
72
|
+
#
|
73
|
+
# @api private
|
74
|
+
def with(*args)
|
75
|
+
self.class.new(fn, name: name, args: args)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @api public
|
79
|
+
def ==(other)
|
80
|
+
return false unless other.instance_of?(self.class)
|
81
|
+
|
82
|
+
[fn, name, args] == [other.fn, other.name, other.args]
|
83
|
+
end
|
84
|
+
alias_method :eql?, :==
|
85
|
+
|
86
|
+
# Return a simple AST representation of this function
|
87
|
+
#
|
88
|
+
# @return [Array]
|
89
|
+
#
|
90
|
+
# @api public
|
91
|
+
def to_ast
|
92
|
+
args_ast = args.map { |arg| arg.respond_to?(:to_ast) ? arg.to_ast : arg }
|
93
|
+
[name, args_ast]
|
94
|
+
end
|
95
|
+
|
96
|
+
# Converts a transproc to a simple proc
|
97
|
+
#
|
98
|
+
# @return [Proc]
|
99
|
+
#
|
100
|
+
def to_proc
|
101
|
+
if !args.empty?
|
102
|
+
proc { |*value| fn.call(*value, *args) }
|
103
|
+
else
|
104
|
+
fn.to_proc
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|