dry-transformer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. checksums.yaml +7 -0
  2. data/.codeclimate.yml +12 -0
  3. data/.github/ISSUE_TEMPLATE/----please-don-t-ask-for-support-via-issues.md +10 -0
  4. data/.github/ISSUE_TEMPLATE/---bug-report.md +30 -0
  5. data/.github/ISSUE_TEMPLATE/---feature-request.md +18 -0
  6. data/.github/workflows/custom_ci.yml +66 -0
  7. data/.github/workflows/docsite.yml +34 -0
  8. data/.github/workflows/sync_configs.yml +34 -0
  9. data/.gitignore +16 -0
  10. data/.rspec +4 -0
  11. data/.rubocop.yml +95 -0
  12. data/CHANGELOG.md +3 -0
  13. data/CODE_OF_CONDUCT.md +13 -0
  14. data/CONTRIBUTING.md +29 -0
  15. data/Gemfile +19 -0
  16. data/LICENSE +20 -0
  17. data/README.md +29 -0
  18. data/Rakefile +6 -0
  19. data/docsite/source/built-in-transformations.html.md +47 -0
  20. data/docsite/source/index.html.md +15 -0
  21. data/docsite/source/transformation-objects.html.md +32 -0
  22. data/docsite/source/using-standalone-functions.html.md +82 -0
  23. data/dry-transformer.gemspec +22 -0
  24. data/lib/dry-transformer.rb +3 -0
  25. data/lib/dry/transformer.rb +23 -0
  26. data/lib/dry/transformer/all.rb +11 -0
  27. data/lib/dry/transformer/array.rb +183 -0
  28. data/lib/dry/transformer/array/combine.rb +65 -0
  29. data/lib/dry/transformer/class.rb +56 -0
  30. data/lib/dry/transformer/coercions.rb +196 -0
  31. data/lib/dry/transformer/compiler.rb +47 -0
  32. data/lib/dry/transformer/composite.rb +54 -0
  33. data/lib/dry/transformer/conditional.rb +76 -0
  34. data/lib/dry/transformer/constants.rb +7 -0
  35. data/lib/dry/transformer/error.rb +16 -0
  36. data/lib/dry/transformer/function.rb +109 -0
  37. data/lib/dry/transformer/hash.rb +453 -0
  38. data/lib/dry/transformer/pipe.rb +75 -0
  39. data/lib/dry/transformer/pipe/class_interface.rb +115 -0
  40. data/lib/dry/transformer/pipe/dsl.rb +58 -0
  41. data/lib/dry/transformer/proc.rb +46 -0
  42. data/lib/dry/transformer/recursion.rb +121 -0
  43. data/lib/dry/transformer/registry.rb +150 -0
  44. data/lib/dry/transformer/store.rb +128 -0
  45. data/lib/dry/transformer/version.rb +7 -0
  46. data/spec/spec_helper.rb +31 -0
  47. data/spec/unit/array/combine_spec.rb +224 -0
  48. data/spec/unit/array_transformations_spec.rb +233 -0
  49. data/spec/unit/class_transformations_spec.rb +50 -0
  50. data/spec/unit/coercions_spec.rb +132 -0
  51. data/spec/unit/conditional_spec.rb +48 -0
  52. data/spec/unit/function_not_found_error_spec.rb +12 -0
  53. data/spec/unit/function_spec.rb +193 -0
  54. data/spec/unit/hash_transformations_spec.rb +490 -0
  55. data/spec/unit/proc_transformations_spec.rb +20 -0
  56. data/spec/unit/recursion_spec.rb +145 -0
  57. data/spec/unit/registry_spec.rb +202 -0
  58. data/spec/unit/store_spec.rb +198 -0
  59. data/spec/unit/transformer/class_interface_spec.rb +350 -0
  60. data/spec/unit/transformer/dsl_spec.rb +15 -0
  61. data/spec/unit/transformer/instance_methods_spec.rb +25 -0
  62. 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,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