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