dry-transformer 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ module Transformer
5
+ Undefined = Object.new.freeze
6
+ end
7
+ 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