dry-transformer 0.1.0
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/.codeclimate.yml +12 -0
- data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
- data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
- data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
- data/.github/workflows/custom_ci.yml +66 -0
- data/.github/workflows/docsite.yml +34 -0
- data/.github/workflows/sync_configs.yml +34 -0
- data/.gitignore +16 -0
- data/.rspec +4 -0
- data/.rubocop.yml +95 -0
- data/CHANGELOG.md +3 -0
- data/CODE_OF_CONDUCT.md +13 -0
- data/CONTRIBUTING.md +29 -0
- data/Gemfile +19 -0
- data/LICENSE +20 -0
- data/README.md +29 -0
- data/Rakefile +6 -0
- data/docsite/source/built-in-transformations.html.md +47 -0
- data/docsite/source/index.html.md +15 -0
- data/docsite/source/transformation-objects.html.md +32 -0
- data/docsite/source/using-standalone-functions.html.md +82 -0
- data/dry-transformer.gemspec +22 -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 +453 -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
- data/spec/spec_helper.rb +31 -0
- data/spec/unit/array/combine_spec.rb +224 -0
- data/spec/unit/array_transformations_spec.rb +233 -0
- data/spec/unit/class_transformations_spec.rb +50 -0
- data/spec/unit/coercions_spec.rb +132 -0
- data/spec/unit/conditional_spec.rb +48 -0
- data/spec/unit/function_not_found_error_spec.rb +12 -0
- data/spec/unit/function_spec.rb +193 -0
- data/spec/unit/hash_transformations_spec.rb +490 -0
- data/spec/unit/proc_transformations_spec.rb +20 -0
- data/spec/unit/recursion_spec.rb +145 -0
- data/spec/unit/registry_spec.rb +202 -0
- data/spec/unit/store_spec.rb +198 -0
- data/spec/unit/transformer/class_interface_spec.rb +350 -0
- data/spec/unit/transformer/dsl_spec.rb +15 -0
- data/spec/unit/transformer/instance_methods_spec.rb +25 -0
- metadata +119 -0
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Dry
|
4
|
+
module Transformer
|
5
|
+
# Transformation functions for Classes
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# require 'dry/transformer/class'
|
9
|
+
#
|
10
|
+
# include Dry::Transformer::Helper
|
11
|
+
#
|
12
|
+
# fn = t(:constructor_inject, Struct)
|
13
|
+
#
|
14
|
+
# fn['User', :name, :age]
|
15
|
+
# # => Struct::User
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
module ClassTransformations
|
19
|
+
extend Registry
|
20
|
+
|
21
|
+
# Inject given arguments into the constructor of the class
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# Transproct(:constructor_inject, Struct)['User', :name, :age]
|
25
|
+
# # => Struct::User
|
26
|
+
#
|
27
|
+
# @param [*Mixed] A list of arguments to inject
|
28
|
+
#
|
29
|
+
# @return [Object] An instance of the given klass
|
30
|
+
#
|
31
|
+
# @api public
|
32
|
+
def self.constructor_inject(*args, klass)
|
33
|
+
klass.new(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Set instance variables from the hash argument (key/value pairs) on the object
|
37
|
+
#
|
38
|
+
# @example
|
39
|
+
# Dry::Transformer(:set_ivars, Object)[name: 'Jane', age: 25]
|
40
|
+
# # => #<Object:0x007f411d06a210 @name="Jane", @age=25>
|
41
|
+
#
|
42
|
+
# @param [Object]
|
43
|
+
#
|
44
|
+
# @return [Object]
|
45
|
+
#
|
46
|
+
# @api public
|
47
|
+
def self.set_ivars(ivar_hash, klass)
|
48
|
+
object = klass.allocate
|
49
|
+
ivar_hash.each do |ivar_name, ivar_value|
|
50
|
+
object.instance_variable_set("@#{ivar_name}", ivar_value)
|
51
|
+
end
|
52
|
+
object
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -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
|