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