remap 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/lib/remap/base.rb +146 -0
  3. data/lib/remap/compiler.rb +171 -0
  4. data/lib/remap/constructor/argument.rb +28 -0
  5. data/lib/remap/constructor/keyword.rb +31 -0
  6. data/lib/remap/constructor/none.rb +23 -0
  7. data/lib/remap/constructor.rb +30 -0
  8. data/lib/remap/error.rb +7 -0
  9. data/lib/remap/failure.rb +27 -0
  10. data/lib/remap/iteration/array.rb +29 -0
  11. data/lib/remap/iteration/hash.rb +30 -0
  12. data/lib/remap/iteration/other.rb +18 -0
  13. data/lib/remap/iteration.rb +20 -0
  14. data/lib/remap/mapper/and.rb +29 -0
  15. data/lib/remap/mapper/binary.rb +16 -0
  16. data/lib/remap/mapper/or.rb +21 -0
  17. data/lib/remap/mapper/xor.rb +27 -0
  18. data/lib/remap/mapper.rb +56 -0
  19. data/lib/remap/nothing.rb +7 -0
  20. data/lib/remap/operation.rb +26 -0
  21. data/lib/remap/result.rb +11 -0
  22. data/lib/remap/rule/each.rb +37 -0
  23. data/lib/remap/rule/embed.rb +42 -0
  24. data/lib/remap/rule/map.rb +109 -0
  25. data/lib/remap/rule/set.rb +43 -0
  26. data/lib/remap/rule/support/collection/empty.rb +23 -0
  27. data/lib/remap/rule/support/collection/filled.rb +27 -0
  28. data/lib/remap/rule/support/collection.rb +11 -0
  29. data/lib/remap/rule/support/enum.rb +63 -0
  30. data/lib/remap/rule/support/path.rb +45 -0
  31. data/lib/remap/rule/void.rb +29 -0
  32. data/lib/remap/rule/wrap.rb +30 -0
  33. data/lib/remap/rule.rb +8 -0
  34. data/lib/remap/selector/all.rb +21 -0
  35. data/lib/remap/selector/index.rb +39 -0
  36. data/lib/remap/selector/key.rb +38 -0
  37. data/lib/remap/selector.rb +14 -0
  38. data/lib/remap/state/extension.rb +393 -0
  39. data/lib/remap/state/schema.rb +21 -0
  40. data/lib/remap/state/types.rb +11 -0
  41. data/lib/remap/state.rb +22 -0
  42. data/lib/remap/static/fixed.rb +20 -0
  43. data/lib/remap/static/option.rb +22 -0
  44. data/lib/remap/static.rb +6 -0
  45. data/lib/remap/struct.rb +7 -0
  46. data/lib/remap/success.rb +29 -0
  47. data/lib/remap/types.rb +47 -0
  48. data/lib/remap/version.rb +5 -0
  49. data/lib/remap.rb +28 -0
  50. metadata +329 -0
@@ -0,0 +1,393 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/deep_transform_values"
4
+
5
+ module Remap
6
+ module State
7
+ module Extension
8
+ refine Object do
9
+ def _(&block)
10
+ unless block
11
+ return _ { raise _1 }
12
+ end
13
+
14
+ block["Expected a state, got [#{self}] (#{self.class})"]
15
+ end
16
+
17
+ def paths
18
+ EMPTY_ARRAY
19
+ end
20
+
21
+ def get(*path)
22
+ throw :missing, path
23
+ end
24
+ end
25
+
26
+ refine Array do
27
+ def hide(value)
28
+ reverse.reduce(value) do |element, key|
29
+ { key => element }
30
+ end
31
+ end
32
+
33
+ def get(*path, last)
34
+ if path.empty?
35
+ return fetch(last) do
36
+ throw :missing, path + [last]
37
+ end
38
+ end
39
+
40
+ dig(*path).fetch(last) do
41
+ throw :missing, path + [last]
42
+ end
43
+ end
44
+ end
45
+
46
+ refine Hash do
47
+ def _(&block)
48
+ unless block
49
+ return _ { raise "Input: #{self} output: #{JSON.pretty_generate(_1)}" }
50
+ end
51
+
52
+ unless (result = Schema.call(self)).success?
53
+ return block[result.errors.to_h]
54
+ end
55
+
56
+ self
57
+ end
58
+
59
+ def paths
60
+ reduce(EMPTY_ARRAY) do |acc, (path, leaves)|
61
+ if (paths = leaves.paths).empty?
62
+ next acc + [[path]]
63
+ end
64
+
65
+ acc + paths.map { |inner| [path] + inner }
66
+ end
67
+ end
68
+
69
+ def paths_pair
70
+ paths.map do |path|
71
+ [dig(*path), path]
72
+ end
73
+ end
74
+
75
+ # Makes the state iterable
76
+ #
77
+ # @yieldparam value [Any]
78
+ # @yieldoption key [Symbol]
79
+ # @yieldoption index [Integer]
80
+ #
81
+ # @yieldreturn [State]
82
+ #
83
+ # @return [State]
84
+ def map(&block)
85
+ bind do |value, state|
86
+ Iteration.call(state: state, value: value).call do |other, **options|
87
+ state.set(other, **options)._.then(&block)._
88
+ end
89
+ end
90
+ end
91
+
92
+ # @return [String]
93
+ def inspect
94
+ reject { _2.blank? }.then do |cleaned|
95
+ format("#<State %<json>s>", json: JSON.pretty_generate(cleaned))
96
+ end
97
+ end
98
+
99
+ # Merges {self} with {other} and returns a new state
100
+ #
101
+ # @param other [State]
102
+ #
103
+ # @return [State]
104
+ def merged(other)
105
+ all_problems = problems.deep_merge(other.problems) do |key, a, b|
106
+ case [a, b]
107
+ in [Array, Array]
108
+ a + b
109
+ else
110
+ raise ArgumentError, "Can't merge #{a.inspect} with #{b.inspect} @ #{key}"
111
+ end
112
+ end
113
+
114
+ catch :undefined do
115
+ value = recursive_merge(other) do |reason|
116
+ return merge(problems: all_problems).problem(reason)
117
+ end
118
+
119
+ return set(value, problems: all_problems)
120
+ end
121
+
122
+ set(problems: all_problems)
123
+ end
124
+
125
+ # Resolves conflicts unsovable by ActiveSupport#deep_merge
126
+ #
127
+ # @param key [Symbol] the key that cannot be merged
128
+ # @param left [Any] the left value that cannot be merged
129
+ # @param right [Any] the right value that cannot be merged
130
+ #
131
+ # @yieldparam reason [String] if {left} and {right} cannot be merged
132
+ # @yieldreturn [State]
133
+ #
134
+ # @return [Any]
135
+ def conflicts(key, left, right, &error)
136
+ case [left, right]
137
+ in [Array, Array]
138
+ left + right
139
+ in [value, ^value]
140
+ value
141
+ in [left, right]
142
+ reason(left, right) do |reason|
143
+ [reason, "[#{key}]"].join(" @ ").then(&error)
144
+ end
145
+ end
146
+ end
147
+
148
+ def get(*path, last)
149
+ if path.empty?
150
+ return fetch(last) do
151
+ throw :missing, path + [last]
152
+ end
153
+ end
154
+
155
+ dig(*path).fetch(last) do
156
+ throw :missing, path + [last]
157
+ end
158
+ end
159
+
160
+ # Recursively merges {self} with {other}
161
+ # Invokes {error} when a conflict is detected
162
+ #
163
+ # @param other [State]
164
+ #
165
+ # @yieldparam key [Symbol]
166
+ # @yieldparam left [Any]
167
+ # @yieldparam right [Any]
168
+ # @yieldparam error [Proc]
169
+ #
170
+ # @yieldreturn [Any]
171
+ #
172
+ # @return [Any] Merge result (not a state)
173
+ def recursive_merge(other, &error)
174
+ case [self, other]
175
+ in [{value: Hash => left}, {value: Hash => right}]
176
+ left.deep_merge(right) { |*args| conflicts(*args, &error) }
177
+ in [{value: Array => left}, {value: Array => right}]
178
+ left + right
179
+ in [{value: left}, {value: right}]
180
+ reason(left, right, &error)
181
+ in [{value: left}, _]
182
+ left
183
+ in [_, {value: right}]
184
+ right
185
+ in [_, _]
186
+ throw :undefined
187
+ end
188
+ end
189
+
190
+ def reason(left, right, &error)
191
+ params = { left: left, cleft: left.class, right: right, cright: right.class }
192
+ message = format("Could not merge [%<left>p] (%<cleft>s) with [%<right>p] (%<cright>s)", params)
193
+ error[message]
194
+ end
195
+
196
+ # Creates a new state with params
197
+ #
198
+ # @param value [Any, Undefined] Used as {value:}
199
+ # @options [Hash] To be merged into {self}
200
+ #
201
+ # @return [State]
202
+ def set(value = Undefined, **options)
203
+ if value != Undefined
204
+ return set(**options, value: value)
205
+ end
206
+
207
+ case [self, options]
208
+ in [{path:}, {quantifier:, **rest}]
209
+ merge(path: path + [quantifier]).set(**rest)
210
+ in [_, {mapper:, value:, **rest}]
211
+ merge(scope: value, value: value, mapper: mapper).set(**rest)
212
+ in [{value:}, {mapper:, **rest}]
213
+ merge(scope: value, mapper: mapper).set(**rest)
214
+ in [{path:}, {key:, **rest}]
215
+ merge(path: path + [key], key: key).set(**rest)
216
+ in [{path:}, {index:, value:, **rest}]
217
+ merge(path: path + [index], element: value, index: index, value: value).set(**rest)
218
+ in [{path:}, {index:, **rest}]
219
+ merge(path: path + [index], index: index).set(**rest)
220
+ else
221
+ merge(options)
222
+ end
223
+ end
224
+
225
+ # Passes {#value} to block, if defined
226
+ # The return value is then wrapped in a state
227
+ # and returned with {options} merged into the state
228
+ #
229
+ # @yieldparam value [T]
230
+ # @yieldparam self [State]
231
+ # @yieldparam error [Proc]
232
+ #
233
+ # @yieldreturn [Y]
234
+ #
235
+ # @return [State<Y>]
236
+ def fmap(**options, &block)
237
+ bind(**options) do |input, state, &error|
238
+ state.set(block[input, state, &error])
239
+ end
240
+ end
241
+
242
+ def failure(reason)
243
+ explaination(reason)
244
+ end
245
+
246
+ def explaination(reason, explainations = EMPTY_HASH)
247
+ Remap::Types::Report::Self[explainations]
248
+
249
+ report = ->(message) { [{}.merge(reason: message)] }
250
+
251
+ explaination = case [self, reason]
252
+ in [{path: []}, String]
253
+ { base: report[reason] }
254
+ in [{path:}, String]
255
+ path.hide(report[reason])
256
+ in [{path:}, Hash]
257
+ reason.paths_pair.reduce(EMPTY_HASH) do |acc, (item, suffix)|
258
+ Array.wrap(item).map { (path + suffix).hide(report[_1]) }.reduce(acc, &:deep_merge)
259
+ end
260
+ end
261
+
262
+ output = explainations.deep_merge(explaination) do |key, left, right|
263
+ case [left, right]
264
+ in [Array, Array]
265
+ left + right
266
+ else
267
+ raise ArgumentError, "Cannot merge #{left} with #{right} @ #{key}"
268
+ end
269
+ end
270
+
271
+ # Remap::Types::Report::Self[output] do
272
+ # binding.pry
273
+ # end
274
+ end
275
+
276
+ def no_of_problems
277
+ a = problems.except(:base).paths.count
278
+ b = problems.fetch(:base, []).count
279
+ a + b
280
+ end
281
+
282
+ # Passes {#value} to block, if defined
283
+ # {options} are merged into the final state
284
+ #
285
+ # @yieldparam value [T]
286
+ # @yieldparam self [State]
287
+ # @yieldparam error [Proc]
288
+ #
289
+ # @yieldreturn [Y]
290
+ #
291
+ # @return [Y]
292
+ def bind(**options, &block)
293
+ unless block
294
+ raise ArgumentError, "no block given"
295
+ end
296
+
297
+ fetch(:value) { return self }.then do |value|
298
+ block[value, self] do |reason, **other|
299
+ return set(**options, **other).problem(reason)._
300
+ end
301
+ end
302
+ end
303
+
304
+ # Execute {block} in the current context
305
+ # Only calls {block} if {#value} is defined
306
+ #
307
+ # @yieldparam value [T]
308
+ # @yieldreturn [U]
309
+ #
310
+ # @return [State<U>]
311
+ def execute(&block)
312
+ bind do |value, &error|
313
+ catch :success do
314
+ path = catch :missing do
315
+ throw :success, set(context(value, &error).instance_exec(value, &block))._
316
+ end
317
+
318
+ return error["Could not fetch value at", path: path]
319
+ end
320
+ rescue NoMethodError => e
321
+ e.name == :fetch ? error["Fetch not defined on value: #{e}"] : raise
322
+ rescue NameError => e
323
+ e.name == :Undefined ? error["Undefined returned, skipping!: #{e}"] : raise
324
+ rescue KeyError, IndexError => e
325
+ error[e.message]
326
+ end
327
+ end
328
+
329
+ # Passes {#value} to block and returns {self}
330
+ #
331
+ # @return [self]
332
+ def tap(&block)
333
+ super { fmap(&block) }
334
+ end
335
+
336
+ # Ensures {value:} is not a state
337
+ #
338
+ # @param options [Hash]
339
+ #
340
+ # @return [Hash]
341
+ def merge(options)
342
+ case options
343
+ in {value:}
344
+ value._ { return super }
345
+ else
346
+ return super
347
+ end
348
+
349
+ raise ArgumentError, "Expected State#value not to be a State [#{value}] (#{value.class})"
350
+ end
351
+
352
+ def to_hash
353
+ except(:options, :mapper, :problems, :value)
354
+ end
355
+
356
+ def value
357
+ fetch(:value)
358
+ end
359
+
360
+ def problem(message)
361
+ merge(problems: explaination(message, problems)).except(:value)
362
+ end
363
+
364
+ def problems
365
+ fetch(:problems)
366
+ end
367
+
368
+ def options
369
+ fetch(:options)
370
+ end
371
+
372
+ # Creates a context containing {options} and {self}
373
+ #
374
+ # @param value [Any]
375
+ #
376
+ # @yieldparam reason [T]
377
+ #
378
+ # @return [Struct]
379
+ def context(value, state: self, &error)
380
+ ::Struct.new(*keys, *options.keys, keyword_init: true) do
381
+ define_method :method_missing do |name, *|
382
+ error["Method [#{name}] not defined"]
383
+ end
384
+
385
+ define_method :skip! do |message = "Manual skip!"|
386
+ error[message]
387
+ end
388
+ end.new(**to_hash, **options, value: value)
389
+ end
390
+ end
391
+ end
392
+ end
393
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ module State
5
+ Schema = Dry::Schema.define do
6
+ required(:input)
7
+
8
+ required(:mapper).filled(Remap::Types::Mapper)
9
+ required(:problems).hash
10
+ required(:options).value(:hash)
11
+ required(:path).array(Types::Key)
12
+
13
+ optional(:index).filled(:integer)
14
+ optional(:element).filled
15
+ optional(:key).filled
16
+
17
+ optional(:scope)
18
+ optional(:value)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ module State
5
+ module Types
6
+ include Dry.Types()
7
+
8
+ Key = String | Symbol | Integer
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/schema"
4
+ require "dry/validation"
5
+ require "dry/core/constants"
6
+ require "factory_bot"
7
+ require "dry/logic"
8
+ require "dry/logic/operations"
9
+ require "dry/logic/predicates"
10
+ require "json"
11
+ require "pry"
12
+
13
+ module Remap
14
+ module State
15
+ include Dry::Core::Constants
16
+ using Extension
17
+
18
+ def state(value, mapper:, options: {})
19
+ { value: value, input: value, mapper: mapper, problems: {}, path: [], options: options, values: value }._
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Static
5
+ class Fixed < Concrete
6
+ using State::Extension
7
+
8
+ attribute :value, Types::Any
9
+
10
+ # Set {state#value} to {#value}
11
+ #
12
+ # @param state [State]
13
+ #
14
+ # @return [State]
15
+ def call(state)
16
+ state.set(value)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Static
5
+ class Option < Concrete
6
+ using State::Extension
7
+
8
+ attribute :name, Symbol
9
+
10
+ # Selects {#value} from {state#params}
11
+ #
12
+ # @param state [State]
13
+ #
14
+ # @return [State]
15
+ def call(state)
16
+ state.set state.options.fetch(name) {
17
+ return state.problem("Option [#{name}] not found in [#{state.options.inspect}]")
18
+ }
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Static < Dry::Interface
5
+ end
6
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Struct < Dry::Struct
5
+ schema schema.strict(true)
6
+ end
7
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Success < Result
5
+ attribute :result, Types::Any
6
+
7
+ def inspect
8
+ format("Success<[%<result>s]>", result: JSON.pretty_generate(to_h))
9
+ end
10
+
11
+ def to_hash
12
+ { success: result, problems: problems }
13
+ end
14
+
15
+ def failure?
16
+ false
17
+ end
18
+
19
+ def success?(value = Undefined)
20
+ return true if value.equal?(Undefined)
21
+
22
+ result == value
23
+ end
24
+
25
+ def fmap(&block)
26
+ block[result]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads/maybe"
4
+ require "dry/logic/operations/negation"
5
+ require "dry/logic"
6
+
7
+ module Remap
8
+ module Types
9
+ include Dry::Types()
10
+ include Dry::Logic::Operations
11
+
12
+ using State::Extension
13
+
14
+ Enumerable = Any.constrained(type: Enumerable)
15
+ Mapper = Interface(:call!) # Class.constrained(lt: Remap::Mapper) | Instance(Remap::Mapper).constructor { |v, &e| Instance(Remap::Mapper::Binary).call(v, &e) }
16
+ Nothing = Constant(Remap::Nothing)
17
+ Maybe = Instance(Dry::Monads::Maybe).fallback(&Dry::Monads::Maybe)
18
+ # Remap = Class.constrained(lt: Remap)
19
+ Proc = Instance(Proc)
20
+ Key = Interface(:hash) | Integer
21
+ Value = Any # .constrained(not_eql: nil)
22
+
23
+ State = Hash.constructor do |value, type, &error|
24
+ type[value, &error]._(&error)
25
+ end
26
+
27
+ Selectors = Array.of(Remap::Selector)
28
+
29
+ Dry::Types.define_builder(:not) do |type, owner = Object|
30
+ type.constrained_type.new(Instance(owner), rule: Negation.new(type.rule))
31
+ end
32
+
33
+ module Report
34
+ include Dry.Types()
35
+
36
+ Problem = Hash.schema(value?: Any, reason: String)
37
+
38
+ Key = String | Symbol | Integer
39
+
40
+ Value = Any.constructor do |value, &error|
41
+ (Array(Problem) | Self).call(value, &error)
42
+ end
43
+
44
+ Self = Hash.map(Key, Value) | Hash.schema(base: Array(Problem))
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ VERSION = "0.1.0"
5
+ end
data/lib/remap.rb ADDED
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/struct"
4
+ require "dry/validation"
5
+
6
+ require "active_support/core_ext/module/delegation"
7
+ require "dry/core/class_builder"
8
+ require "dry/core/memoizable"
9
+ require "dry/core/deprecations"
10
+ require "dry/logic/builder"
11
+ require "dry-configurable"
12
+ require "dry/schema"
13
+ require "dry/types"
14
+ require "dry/monads"
15
+ require "dry/logic"
16
+ require "zeitwerk"
17
+ require "dry/interface"
18
+
19
+ Dry::Types.load_extensions(:maybe)
20
+
21
+ loader = Zeitwerk::Loader.for_gem
22
+ loader.collapse("#{__dir__}/remap/rule/support")
23
+ loader.setup
24
+
25
+ module Remap
26
+ end
27
+
28
+ loader.eager_load