remap 2.0.2

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.
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