remap 2.0.3 → 2.1.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/lib/remap/base.rb +229 -75
  3. data/lib/remap/compiler.rb +403 -37
  4. data/lib/remap/constructor/argument.rb +20 -6
  5. data/lib/remap/constructor/keyword.rb +20 -4
  6. data/lib/remap/constructor/none.rb +3 -4
  7. data/lib/remap/constructor.rb +12 -5
  8. data/lib/remap/contract.rb +27 -0
  9. data/lib/remap/extensions/enumerable.rb +48 -0
  10. data/lib/remap/extensions/hash.rb +13 -0
  11. data/lib/remap/extensions/object.rb +37 -0
  12. data/lib/remap/failure.rb +25 -15
  13. data/lib/remap/iteration/array.rb +20 -11
  14. data/lib/remap/iteration/hash.rb +21 -13
  15. data/lib/remap/iteration/other.rb +7 -7
  16. data/lib/remap/iteration.rb +8 -2
  17. data/lib/remap/mapper/and.rb +29 -7
  18. data/lib/remap/mapper/binary.rb +3 -6
  19. data/lib/remap/mapper/or.rb +29 -6
  20. data/lib/remap/mapper/support/operations.rb +40 -0
  21. data/lib/remap/mapper/xor.rb +29 -7
  22. data/lib/remap/mapper.rb +1 -48
  23. data/lib/remap/notice/traced.rb +19 -0
  24. data/lib/remap/notice/untraced.rb +11 -0
  25. data/lib/remap/notice.rb +34 -0
  26. data/lib/remap/operation.rb +26 -13
  27. data/lib/remap/path/input.rb +37 -0
  28. data/lib/remap/path/output.rb +26 -0
  29. data/lib/remap/path.rb +22 -0
  30. data/lib/remap/path_error.rb +13 -0
  31. data/lib/remap/proxy.rb +18 -0
  32. data/lib/remap/rule/each.rb +25 -24
  33. data/lib/remap/rule/embed.rb +33 -28
  34. data/lib/remap/rule/map/optional.rb +42 -0
  35. data/lib/remap/rule/map/required.rb +35 -0
  36. data/lib/remap/rule/map.rb +176 -55
  37. data/lib/remap/rule/set.rb +23 -33
  38. data/lib/remap/rule/support/collection/empty.rb +7 -7
  39. data/lib/remap/rule/support/collection/filled.rb +21 -8
  40. data/lib/remap/rule/support/collection.rb +11 -3
  41. data/lib/remap/rule/support/enum.rb +44 -21
  42. data/lib/remap/rule/void.rb +17 -18
  43. data/lib/remap/rule/wrap.rb +25 -17
  44. data/lib/remap/rule.rb +8 -1
  45. data/lib/remap/selector/all.rb +29 -7
  46. data/lib/remap/selector/index.rb +24 -16
  47. data/lib/remap/selector/key.rb +31 -16
  48. data/lib/remap/selector.rb +17 -0
  49. data/lib/remap/state/extension.rb +182 -208
  50. data/lib/remap/state/schema.rb +1 -1
  51. data/lib/remap/state.rb +30 -4
  52. data/lib/remap/static/fixed.rb +14 -3
  53. data/lib/remap/static/option.rb +21 -6
  54. data/lib/remap/static.rb +13 -0
  55. data/lib/remap/struct.rb +1 -0
  56. data/lib/remap/types.rb +13 -28
  57. data/lib/remap.rb +15 -19
  58. metadata +91 -89
  59. data/lib/remap/result.rb +0 -11
  60. data/lib/remap/rule/support/path.rb +0 -45
  61. data/lib/remap/state/types.rb +0 -11
  62. data/lib/remap/success.rb +0 -29
  63. data/lib/remap/version.rb +0 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: feae800c3a05859a5cf0cefd1c080f3afa92109856b504aba89f81975f21dfc4
4
- data.tar.gz: 54c5b5b7dd28b67b67aed40019867d6293141edca668ea8d4de9b5b670dedaa4
3
+ metadata.gz: f155fc9609bac21c4f0c0d25567f5d214bd7f3e5417a71c06e44cea01f0fa409
4
+ data.tar.gz: e736316c91ed5763a9d028a457246f61628a1f2cd497df9dc86aabe0224c3f9c
5
5
  SHA512:
6
- metadata.gz: ecabf17439228869a9e14a2686cc38d92cd495a529efa2fc6114ffb10abf40837fe3fccf858123134f10c6a54f314e2464d8d309f854183b69c2875733cd48f4
7
- data.tar.gz: cc27691d7b1710d9642368559a25b7da4009c5a4346b5b44aa6166b81a8ec3ea053276ebd20b5d976548bf65290e9d2b4395395c2932edaaea8cea08bbdd285f
6
+ metadata.gz: b47bf7bd8de17faf5dfaa2425b566244fa54fa31fe448277e1d1ea2dedbb42f98e53840c7f5eedce2c3fe8036a94330f8723661d55a6a2b232ade3cdbc0a18cd
7
+ data.tar.gz: 86ff7e32e26f1a06f8c5840dcff37610e2991a79ef75f8495881c2ca629160bdc3c48af1580884f515d317021ae38d3b6cc7942d2fcbd8020b219c386a1806af
data/lib/remap/base.rb CHANGED
@@ -1,146 +1,300 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "dry/monads/all"
3
+ require "active_support/configurable"
4
+ require "active_support/core_ext/object/with_options"
4
5
 
5
6
  module Remap
7
+ # @example Select all elements
8
+ # class Mapper < Remap::Base
9
+ # define do
10
+ # map [all, :name]
11
+ # end
12
+ # end
13
+ #
14
+ # Mapper.call([{ name: "John" }, { name: "Jane" }]) # => ["John", "Jane"]
15
+ #
16
+ # @example Given an option
17
+ # class Mapper < Remap::Base
18
+ # option :name
19
+ #
20
+ # define do
21
+ # set [:person, :name], to: option(:name)
22
+ # end
23
+ # end
24
+ #
25
+ # Mapper.call({}, name: "John") # => { person: { name: "John" } }
26
+ #
27
+ # @example Given a value
28
+ # class Mapper < Remap::Base
29
+ # define do
30
+ # set [:api_key], to: value("ABC-123")
31
+ # end
32
+ # end
33
+ #
34
+ # Mapper.call({}) # => { api_key: "ABC-123" }
35
+ #
36
+ # @example Maps ["A", "B", "C"] to ["A", "C"]
37
+ # class Mapper < Remap::Base
38
+ # define do
39
+ # each do
40
+ # map.if_not do
41
+ # value.include?("B")
42
+ # end
43
+ # end
44
+ # end
45
+ # end
46
+ #
47
+ # Mapper.call(["A", "B", "C"]) # => ["A", "C"]
48
+ #
49
+ # @example Maps ["A", "B", "C"] to ["B"]
50
+ # class Mapper < Remap::Base
51
+ # define do
52
+ # each do
53
+ # map.if do
54
+ # value.include?("B")
55
+ # end
56
+ # end
57
+ # end
58
+ # end
59
+ #
60
+ # Mapper.call(["A", "B", "C"]) # => ["B"]
61
+ #
62
+ # @example Maps { a: { b: "A" } } to "A"
63
+ # class Mapper < Remap::Base
64
+ # define do
65
+ # map(:a, :b).enum do
66
+ # value "A", "B"
67
+ # end
68
+ # end
69
+ # end
70
+ #
71
+ # Mapper.call({ a: { b: "A" } }) # => "A"
72
+ # Mapper.call({ a: { b: "B" } }) # => "B"
73
+ #
74
+ # @example Map { people: [{ name: "John" }] } to { names: ["John"] }
75
+ # class Mapper < Remap::Base
76
+ # define do
77
+ # map :people, to: :names do
78
+ # each do
79
+ # map :name
80
+ # end
81
+ # end
82
+ # end
83
+ # end
84
+ #
85
+ # Mapper.call({ people: [{ name: "John" }] }) # => { names: ["John"] }
86
+ #
87
+ # @example Map "Hello" to "Hello!"
88
+ # class Mapper < Remap::Base
89
+ # define do
90
+ # map.adjust do
91
+ # "#{value}!"
92
+ # end
93
+ # end
94
+ # end
95
+ #
96
+ # Mapper.call("Hello") # => "Hello!"
97
+ #
98
+ # @example Select the second element from an array
99
+ # class Mapper < Remap::Base
100
+ # define do
101
+ # map [at(1)]
102
+ # end
103
+ # end
104
+ #
105
+ # Mapper.call([1, 2, 3]) # => 2
6
106
  class Base < Mapper
7
- include Dry::Core::Memoizable
107
+ include ActiveSupport::Configurable
8
108
  include Dry::Core::Constants
9
- extend Dry::Monads[:result]
10
-
11
- extend Dry::Configurable
12
- extend Forwardable
13
-
14
109
  using State::Extension
15
- extend State
110
+ extend Operation
16
111
 
17
- CONTRACT = Dry::Schema.JSON do
18
- # NOP
112
+ with_options instance_accessor: true do |scope|
113
+ scope.config_accessor(:contract) { Dry::Schema.JSON {} }
114
+ scope.config_accessor(:constructor) { IDENTITY }
115
+ scope.config_accessor(:options) { EMPTY_ARRAY }
116
+ scope.config_accessor(:option) { EMPTY_HASH }
117
+ scope.config_accessor(:rules) { EMPTY_ARRAY }
118
+ scope.config_accessor(:context) { IDENTITY }
19
119
  end
20
120
 
21
- setting :constructor, default: IDENTITY
22
- setting :options, default: EMPTY_ARRAY
23
- setting :rules, default: EMPTY_ARRAY
24
- setting :contract, default: CONTRACT
25
- setting :context, default: IDENTITY
26
-
27
- delegate [:config] => self
28
-
29
121
  schema schema.strict(false)
30
122
 
31
- # Holds the current context
32
- # @private
123
+ # Defines a schema for the mapper
124
+ # If the schema fail, the mapper will fail
125
+ #
126
+ # @example Guard against missing values
127
+ # class MapperWithAge < Remap::Base
128
+ # contract do
129
+ # required(:age).filled(:integer)
130
+ # end
131
+ #
132
+ # define do
133
+ # map :age, to: [:person, :age]
134
+ # end
135
+ # end
136
+ #
137
+ # MapperWithAge.call({age: 50}) # => { person: { age: 50 } }
138
+ # MapperWithAge.call({age: '10'}) do |failure|
139
+ # # ...
140
+ # end
141
+ #
142
+ # @see https://dry-rb.org/gems/dry-schema/1.5/
143
+ #
144
+ # @return [void]
33
145
  def self.contract(&context)
34
- config.contract = Dry::Schema.JSON(&context)
146
+ self.contract = Dry::Schema.JSON(&context)
35
147
  end
36
148
 
37
- # @see Dry::Validation::Contract.rule
149
+ # Defines a rule for the mapper
150
+ # If the rule fail, the mapper will fail
151
+ #
152
+ # @example Guard against values
153
+ # class MapperWithRule < Remap::Base
154
+ # contract do
155
+ # required(:age)
156
+ # end
157
+ #
158
+ # rule(:age) do
159
+ # unless value >= 18
160
+ # key.failure("must be at least 18 years old")
161
+ # end
162
+ # end
163
+ #
164
+ # define do
165
+ # map :age, to: [:person, :age]
166
+ # end
167
+ # end
168
+ #
169
+ # MapperWithRule.call({age: 50}) # => { person: { age: 50 } }
170
+ # MapperWithRule.call({age: 10}) do |failure|
171
+ # # ...
172
+ # end
173
+ #
174
+ # @see https://dry-rb.org/gems/dry-validation/1.6/rules/
175
+ #
176
+ # @return [void]
38
177
  def self.rule(...)
39
- config.rules << ->(*) { rule(...) }
178
+ self.rules = rules + [-> * { rule(...) }]
40
179
  end
41
180
 
42
- # Defines a a constructor argument for the mapper
181
+ # Defines a required option for the mapper
182
+ #
183
+ # @example A mapper that takes an argument name
184
+ # class MapperWithOption < Remap::Base
185
+ # option :name
186
+ #
187
+ # define do
188
+ # set :name, to: option(:name)
189
+ # end
190
+ # end
43
191
  #
44
- # @param name [Symbol]
45
- # @param type [#call]
192
+ # MapperWithOption.call({}, name: "John") # => { name: "John" }
193
+ #
194
+ # @param field [Symbol]
195
+ # @option type (Types::Any) [#call]
196
+ #
197
+ # @return [void]
46
198
  def self.option(field, type: Types::Any)
47
199
  attribute(field, type)
48
200
 
49
201
  unless (key = schema.keys.find { _1.name == field })
50
- raise ArgumentError, "Could not locate [#{field}] in [#{self}]"
202
+ raise ArgumentError, "[BUG] Could not locate [#{field}] in [#{self}]"
51
203
  end
52
204
 
53
- config.options << ->(*) { option(field, type: key) }
205
+ self.options = options + [-> * { option(field, type: key) }]
54
206
  end
55
207
 
56
- # Pretty print the mapper
208
+ # Defines a mapper rules and possible constructor
57
209
  #
58
- # @return [String]
59
- def self.inspect
60
- "<#{self.class} #{rule}, #{self}>"
61
- end
62
-
63
- # Defines a mapper with a constructor used to wrap the output
210
+ # @param target (Nothing) [#call]
64
211
  #
65
- # @param constructor [#call]
212
+ # @option method (:new) [Symbol]
213
+ # @option strategy (:argument) [:argument, :keywords, :none]
66
214
  #
67
- # @example A mapper from path :a to path :b
68
- # class Mapper < Remap
215
+ # @example A mapper, which mapps a value at [:a] to [:b]
216
+ # class Mapper < Remap::Base
69
217
  # define do
70
218
  # map :a, to: :b
71
219
  # end
72
220
  # end
73
221
  #
74
- # Mapper.call(a: 1) # => { b: 1 }
222
+ # Mapper.call({a: 1}) # => { b: 1 }
223
+ #
224
+ # @example A mapper with an output constructor
225
+ # class Person < Dry::Struct
226
+ # attribute :first_name, Dry::Types['strict.string']
227
+ # end
228
+ #
229
+ # class Mapper < Remap::Base
230
+ # define(Person) do
231
+ # map :name, to: :first_name
232
+ # end
233
+ # end
234
+ #
235
+ # Mapper.call({name: "John"}).first_name # => "John"
236
+ #
237
+ # @return [void]
75
238
  def self.define(target = Nothing, method: :new, strategy: :argument, &context)
76
239
  unless context
77
240
  raise ArgumentError, "Missing block"
78
241
  end
79
242
 
80
- config.context = Compiler.call(&context)
81
- config.constructor = Constructor.call(method: method, strategy: strategy, target: target)
243
+ self.context = Compiler.call(&context)
244
+ self.constructor = Constructor.call(method: method, strategy: strategy, target: target)
82
245
  rescue Dry::Struct::Error => e
83
246
  raise ArgumentError, e.message
84
247
  end
85
248
 
86
- # Creates a new mapper
249
+ # Similar to {::call}, but takes a state
87
250
  #
88
- # @param input [Any]
89
- # @param params [Hash]
90
-
91
- # @return [Context]
92
-
93
- extend Operation
94
-
251
+ # @param state [State]
252
+ #
253
+ # @yield [Failure] if mapper fails
254
+ #
255
+ # @return [Result] if mapper succeeds
256
+ #
257
+ # @private
95
258
  def self.call!(state, &error)
96
259
  new(state.options).call(state._.set(mapper: self), &error)
97
- rescue Dry::Struct::Error => e
98
- raise ArgumentError, "Option missing to mapper [#{self}]: #{e}"
99
260
  end
100
261
 
101
- # Creates a mapper tree using {#context} and uses {#state} as argument
262
+ # Mappers state according to the mapper rules
102
263
  #
103
- # @return [State]
264
+ # @param state [State]
104
265
  #
105
- # @see .call!
266
+ # @yield [Failure] if mapper fails
267
+ #
268
+ # @return [State]
106
269
  #
107
270
  # @private
108
271
  def call(state, &error)
109
272
  unless error
110
- raise ArgumentError, "Missing block"
273
+ raise ArgumentError, "Base#call(state, &error) requires block"
111
274
  end
112
275
 
113
276
  state.tap do |input|
114
- contract.call(input, state.options).tap do |result|
277
+ validation.call(input, state.options).tap do |result|
115
278
  unless result.success?
116
279
  return error[state.failure(result.errors.to_h)]
117
280
  end
118
281
  end
119
282
  end
120
283
 
121
- state.then(&config.context).then(&config.constructor)
284
+ notice = catch :fatal do
285
+ return context.call(state) do |failure|
286
+ return error[failure]
287
+ end.then(&constructor)
288
+ end
289
+
290
+ error[state.failure(notice)]
122
291
  end
123
292
 
124
293
  private
125
294
 
126
- def contract(scope: self)
127
- Class.new(Dry::Validation::Contract) do |klass|
128
- config = scope.class.config
129
-
130
- config.rules.each do |rule|
131
- klass.class_eval(&rule)
132
- end
133
-
134
- config.options.each do |option|
135
- klass.class_eval(&option)
136
- end
137
-
138
- schema(config.contract)
139
- end.new(**attributes)
140
- end
141
-
142
- def config
143
- self.class.config
295
+ # @return [Contract]
296
+ def validation
297
+ Contract.call(attributes: attributes, contract: contract, options: options, rules: rules)
144
298
  end
145
299
  end
146
300
  end