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.
@@ -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