remap 2.0.2 → 2.1.5

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 (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 +95 -93
  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
@@ -1,26 +1,39 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Remap
4
- module Operation
5
- using State::Extension
6
- include State
4
+ using State::Extension
7
5
 
6
+ # Class interface for {Remap::Base} and instance interface for {Mapper}
7
+ module Operation
8
+ # Public interface for mappers
9
+ #
10
+ # @param input [Any] Data to be mapped
11
+ # @param options [Hash] Mapper arguments
12
+ #
13
+ # @yield [Failure] if mapper fails
14
+ #
15
+ # @return [Success] if mapper succeeds
8
16
  def call(input, **options, &error)
9
- new_state = state(input, options: options, mapper: self)
10
-
11
- new_state = call!(new_state) do |failure|
12
- return Failure.new(reasons: failure, problems: new_state.problems)
17
+ unless error
18
+ return call(input, **options) do |failure|
19
+ raise failure.exception
20
+ end
13
21
  end
14
22
 
15
- if error
16
- return error[new_state]
23
+ other = State.call(input, options: options, mapper: self).then do |state|
24
+ call!(state) do |failure|
25
+ return error[failure]
26
+ end
17
27
  end
18
28
 
19
- value = new_state.fetch(:value) do
20
- return Failure.new(reasons: new_state.failure("No mapped data"), problems: new_state.problems)
29
+ case other
30
+ in { value: }
31
+ value
32
+ in { notices: [] }
33
+ error[other.failure("No return value")]
34
+ in { notices: }
35
+ error[Failure.call(failures: notices)]
21
36
  end
22
-
23
- Success.new(problems: new_state.problems, result: value)
24
37
  end
25
38
  end
26
39
  end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Path
5
+ using State::Extension
6
+
7
+ # Returns the value at a given path
8
+ #
9
+ # @example Select "A" from { a: { b: { c: ["A"] } } }
10
+ # state = Remap::State.call({ a: { b: { c: ["A"] } } })
11
+ # first = Remap::Selector::Index.new(index: 0)
12
+ # path = Remap::Path::Input.new([:a, :b, :c, first])
13
+ #
14
+ # path.call(state) do |state|
15
+ # state.fetch(:value)
16
+ # end
17
+ class Input < Unit
18
+ # @return [Array<Selector>]
19
+ attribute :segments, [Selector]
20
+
21
+ # Selects the value at the path {#segments}
22
+ #
23
+ # @param state [State]
24
+ #
25
+ # @return [State]
26
+ def call(state, &iterator)
27
+ unless block_given?
28
+ raise ArgumentError, "Input path requires an iterator block"
29
+ end
30
+
31
+ segments.reverse.reduce(iterator) do |inner_iterator, selector|
32
+ -> inner_state { selector.call(inner_state, &inner_iterator) }
33
+ end.call(state)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Path
5
+ using Extensions::Enumerable
6
+ using State::Extension
7
+
8
+ # Sets the value to a given path
9
+ #
10
+ # @example Maps "A" to { a: { b: { c: "A" } } }
11
+ # state = Remap::State.call("A")
12
+ # result = Remap::Path::Output.new([:a, :b, :c]).call(state)
13
+ #
14
+ # result.fetch(:value) # => { a: { b: { c: "A" } } }
15
+ class Output < Unit
16
+ attribute :segments, [Types::Key]
17
+
18
+ # @return [State]
19
+ def call(state)
20
+ state.fmap do |value|
21
+ segments.hide(value)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/remap/path.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ # Represents a sequence of keys and selects or maps a value given a path
5
+ class Path < Dry::Interface
6
+ attribute :segments, Types::Array
7
+
8
+ delegate :>>, to: :to_proc
9
+
10
+ # @return [State]
11
+ #
12
+ # @abstract
13
+ def call(state)
14
+ raise NotImplementedError, "#{self.class}#call not implemented"
15
+ end
16
+
17
+ # @return [Proc]
18
+ def to_proc
19
+ method(:call).to_proc
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class PathError < Error
5
+ # @return [Array<Key>]
6
+ attr_reader :path
7
+
8
+ def initialize(path)
9
+ super(path.join("."))
10
+ @path = path
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Proxy < ActiveSupport::ProxyObject
5
+ def self.const_missing(name)
6
+ ::Object.const_get(name)
7
+ end
8
+
9
+ include Dry::Core::Constants
10
+ extend Dry::Initializer
11
+
12
+ # See Object#tap
13
+ def tap(&block)
14
+ block[self]
15
+ self
16
+ end
17
+ end
18
+ end
@@ -2,34 +2,35 @@
2
2
 
3
3
  module Remap
4
4
  class Rule
5
- class Each < Value
6
- using State::Extension
5
+ using State::Extension
7
6
 
8
- attribute :rule, Types.Interface(:call)
7
+ # Iterates over a rule, even if the rule is not a collection
8
+ #
9
+ # @example Upcase each value in an array
10
+ # state = Remap::State.call(["John", "Jane"])
11
+ # upcase = Remap::Rule::Map.call({}).then(&:upcase)
12
+ # each = Remap::Rule::Each.call(rule: upcase)
13
+ # error = -> failure { raise failure.exception }
14
+ # each.call(state, &error).fetch(:value) # => ["JOHN", "JANE"]
15
+ class Each < Unit
16
+ # @return [Rule]
17
+ attribute :rule, Types::Rule
9
18
 
10
- # Iterates over {state} and passes each value to {rule}
11
- # Restores {path} before returning state
19
+ # Iterates over state and passes each value to rule
20
+ # Restores element, key & index before returning state
12
21
  #
22
+ # @param state [State<Enumerable>]
13
23
  #
14
- # # @example
15
- # class Mapper < Remap::Base
16
- # define do
17
- # map :people, to: :names do
18
- # each do
19
- # map(:name)
20
- # end
21
- # end
22
- # end
23
- # end
24
- #
25
- # Mapper.call(people: [{ name: "John" }, { name: "Jane" }]) # => { names: ["John", "Jane"] }
26
- #
27
- # @param state [State]
28
- #
29
- # @return [State]
30
- def call(state)
31
- state.map do |state|
32
- rule.call(state)
24
+ # @return [State<Enumerable>]
25
+ def call(state, &error)
26
+ unless error
27
+ raise ArgumentError, "Each#call(state, &error) requires a block"
28
+ end
29
+
30
+ state.map do |inner_state|
31
+ rule.call(inner_state) do |failure|
32
+ return error[failure]
33
+ end
33
34
  end
34
35
  end
35
36
  end
@@ -2,40 +2,45 @@
2
2
 
3
3
  module Remap
4
4
  class Rule
5
- class Embed < Value
6
- using State::Extension
5
+ using State::Extension
7
6
 
7
+ # Embed mappers into each other
8
+ #
9
+ # @example Embed Mapper A into B
10
+ # class Car < Remap::Base
11
+ # define do
12
+ # map :name, to: :model
13
+ # end
14
+ # end
15
+ #
16
+ # class Person < Remap::Base
17
+ # define do
18
+ # to :person do
19
+ # to :car do
20
+ # embed Car
21
+ # end
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ # Person.call({name: "Volvo"}) # => { person: { car: { model: "Volvo" } } }
27
+ class Embed < Unit
28
+ # @return [#call!]
8
29
  attribute :mapper, Types::Mapper
9
30
 
10
- # Evaluates {input} against {mapper} and returns the result
31
+ # Evaluates input against mapper and returns the result
11
32
  #
12
- # @param state [State]
33
+ # @param state [State<T>]
13
34
  #
14
- # @example Embed Mapper A into B
15
- # class Car < Remap::Base
16
- # define do
17
- # map :name, to: :model
18
- # end
19
- # end
20
- #
21
- # class Person < Remap::Base
22
- # define do
23
- # to :person do
24
- # to :car do
25
- # embed Car
26
- # end
27
- # end
28
- # end
29
- # end
30
- #
31
- # Person.call(name: "Volvo") # => { person: { car: { name: "Volvo" } } }
32
- #
33
- #
34
- # @return [State]
35
- def call(state)
36
- mapper.call!(state.set(mapper: mapper)) do |error|
37
- return state.problem(error)
35
+ # @return [State<U>]
36
+ def call(state, &error)
37
+ unless error
38
+ raise ArgumentError, "A block is required to evaluate the embed"
38
39
  end
40
+
41
+ mapper.call!(state.set(mapper: mapper)) do |failure|
42
+ return error[failure]
43
+ end.except(:mapper, :scope)
39
44
  end
40
45
  end
41
46
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Map
6
+ using State::Extension
7
+
8
+ class Optional < Concrete
9
+ # Represents an optional mapping rule
10
+ # When the mapping fails, the value is ignored
11
+ #
12
+ # @param state [State]
13
+ #
14
+ # @return [State]
15
+ def call(state, &error)
16
+ unless error
17
+ raise ArgumentError, "map.call(state, &error) requires a block"
18
+ end
19
+
20
+ fatal(state) do
21
+ return ignore(state) do
22
+ return notice(state) do
23
+ return super
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ # Catches :ignore exceptions and re-package them as a state
32
+ #
33
+ # @param state [State]
34
+ #
35
+ # @return [State]
36
+ def ignore(state, &block)
37
+ state.set(notice: catch(:ignore, &block).traced(backtrace)).except(:value)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Map
6
+ using State::Extension
7
+
8
+ class Required < Concrete
9
+ attribute :backtrace, Types::Backtrace
10
+
11
+ # Represents a required mapping rule
12
+ # When it fails, the entire mapping is marked as failed
13
+ #
14
+ # @param state [State]
15
+ #
16
+ # @return [State]
17
+ def call(state, &error)
18
+ unless block_given?
19
+ raise ArgumentError, "Required.call(state, &error) requires a block"
20
+ end
21
+
22
+ notice = catch :ignore do
23
+ return fatal(state) do
24
+ return notice(state) do
25
+ return super
26
+ end
27
+ end
28
+ end
29
+
30
+ error[state.failure(notice)]
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -2,62 +2,72 @@
2
2
 
3
3
  module Remap
4
4
  class Rule
5
- class Map < self
6
- using State::Extension
5
+ using Extensions::Enumerable
6
+ using State::Extension
7
7
 
8
- attribute :rule, Types.Interface(:call)
9
- attribute :path, Path
8
+ # Maps an input path to an output path
9
+ #
10
+ # @example Map { name: "Ford" } to { person: { name: "Ford" } }
11
+ # class Mapper < Remap::Base
12
+ # define do
13
+ # map :name, to: [:person, :name]
14
+ # end
15
+ # end
16
+ #
17
+ # Mapper.call({ name: "Ford" }) # => { person: { name: "Ford" } }
18
+ class Map < Abstract
19
+ class Path < Struct
20
+ Output = Remap::Path::Output
21
+ Input = Remap::Path::Input
10
22
 
11
- # Maps {input} in 4 steps
12
- #
13
- # 1. Extract value from {input} using {path}
14
- # 2. For each yielded value
15
- # 2.1. Map value using {#rule}
16
- # 2.2. Map value using {#fn}
23
+ attribute :output, Output.default { Output.call(EMPTY_ARRAY) }
24
+ attribute :input, Input.default { Input.call(EMPTY_ARRAY) }
25
+ end
17
26
 
18
- # @example Map :a, to :b and add 5
19
- # map = Map.new({
20
- # path: { map: :a, to: :b },
21
- # rule: Void.new,
22
- # })
23
- #
24
- # map.adjust do |value|
25
- # value + 5
26
- # end
27
+ # @return [Hash]
28
+ attribute? :path, Path.default { Path.call(EMPTY_HASH) }
29
+
30
+ # @return [Rule]
31
+ attribute :rule, Rule.default { Void.call(EMPTY_HASH) }
32
+
33
+ # @return [Array<String>]
34
+ attribute? :backtrace, Types::Backtrace, default: EMPTY_ARRAY
35
+
36
+ order :Optional, :Required
37
+
38
+ # Represents a required or optional mapping rule
27
39
  #
28
- # map.call(a: 10) # => Success({ b: 15 })
40
+ # @param state [State]
29
41
  #
30
- # @param input [Any] Value to be mapped
31
- # @param state [State] Current state
42
+ # @return [State]
32
43
  #
33
- # @return [Monad::Result]
34
- def call(state)
35
- path.call(state) do |inner_state|
36
- rule.call(inner_state).then do |init|
37
- fn.reduce(init) do |inner, fn|
38
- fn[inner]
39
- end
40
- end
44
+ # @abstract
45
+ def call(state, &error)
46
+ unless error
47
+ raise ArgumentError, "Map#call(state, &error) requires error handler block"
48
+ end
49
+
50
+ notice = catch :fatal do
51
+ return path.input.call(state) do |inner_state|
52
+ rule.call(inner_state) do |failure|
53
+ return error[failure]
54
+ end.then(&callback)
55
+ end.then(&path.output)
41
56
  end
57
+
58
+ raise notice.traced(backtrace).exception
42
59
  end
43
60
 
44
- # Post-processor for {#call}
45
- #
46
- # @example Add 5 to mapped value
47
- # class Mapper < Remap
48
- # define do
49
- # map.adjust do |value|
50
- # value + 5
51
- # end
52
- # end
53
- # end
54
- #
55
- # Mapper.call(10) # => 15
61
+ # A post-processor method
56
62
  #
57
- # @yieldparam value [Any] Mapped value
58
- # @yieldreturn [Monad::Result, Any]
63
+ # @example Upcase mapped value
64
+ # state = Remap::State.call("Hello World")
65
+ # map = Remap::Rule::Map.call({})
66
+ # upcase = map.adjust(&:upcase)
67
+ # error = -> failure { raise failure.exception }
68
+ # upcase.call(state, &error).fetch(:value) # => "HELLO WORLD"
59
69
  #
60
- # @return [void]
70
+ # @return [Map]
61
71
  def adjust(&block)
62
72
  add do |state|
63
73
  state.execute(&block)
@@ -65,45 +75,156 @@ module Remap
65
75
  end
66
76
  alias then adjust
67
77
 
78
+ # A pending rule
79
+ #
80
+ # @param reason [String]
81
+ #
82
+ # @example Pending mapping
83
+ # state = Remap::State.call(:value)
84
+ # map = Remap::Rule::Map.call({})
85
+ # pending = map.pending("this is pending")
86
+ # error = -> failure { raise failure.exception }
87
+ # pending.call(state, &error).key?(:value) # => false
88
+ #
89
+ # @return [Map]
68
90
  def pending(reason = "Pending mapping")
69
91
  add do |state|
70
- state.problem(reason)
92
+ state.notice!(reason)
71
93
  end
72
94
  end
73
95
 
96
+ # An enumeration processor
97
+ #
98
+ # @example A mapped enum
99
+ # enum = Remap::Rule::Map.call({}).enum do
100
+ # value "A", "B"
101
+ # otherwise "C"
102
+ # end
103
+ #
104
+ # error = -> failure { raise failure.exception }
105
+ #
106
+ # a = Remap::State.call("A")
107
+ # enum.call(a, &error).fetch(:value) # => "A"
108
+ #
109
+ # b = Remap::State.call("B")
110
+ # enum.call(b, &error).fetch(:value) # => "B"
111
+ #
112
+ # c = Remap::State.call("C")
113
+ # enum.call(c, &error).fetch(:value) # => "C"
114
+ #
115
+ # d = Remap::State.call("D")
116
+ # enum.call(d, &error).fetch(:value) # => "C"
117
+ #
118
+ # @return [Map]
74
119
  def enum(&block)
75
- add do |state|
76
- state.fmap do |id, &error|
77
- Enum.call(&block).get(id, &error)
120
+ add do |outer_state|
121
+ outer_state.fmap do |id, state|
122
+ Enum.call(&block).get(id) do
123
+ state.ignore!("Enum value %p (%s) not defined", id, id.class)
124
+ end
78
125
  end
79
126
  end
80
127
  end
81
128
 
129
+ # Keeps map, only if block is true
130
+ #
131
+ # @example Keep if value contains "A"
132
+ # map = Remap::Rule::Map.call({}).if do
133
+ # value.include?("A")
134
+ # end
135
+ #
136
+ # error = -> failure { raise failure.exception }
137
+ #
138
+ # a = Remap::State.call("A")
139
+ # map.call(a, &error).fetch(:value) # => "A"
140
+ #
141
+ # b = Remap::State.call("BA")
142
+ # map.call(b, &error).fetch(:value) # => "BA"
143
+ #
144
+ # c = Remap::State.call("C")
145
+ # map.call(c, &error).key?(:value) # => false
146
+ #
147
+ # @return [Map]
82
148
  def if(&block)
83
- add do |state|
84
- state.execute(&block).fmap do |bool, &error|
85
- bool ? state.value : error["#if returned false"]
149
+ add do |outer_state|
150
+ outer_state.execute(&block).fmap do |bool, state|
151
+ bool ? outer_state.value : state.notice!("#if returned false")
86
152
  end
87
153
  end
88
154
  end
89
155
 
156
+ # Keeps map, only if block is false
157
+ #
158
+
159
+ # @example Keep unless value contains "A"
160
+ # map = Remap::Rule::Map.call({}).if_not do
161
+ # value.include?("A")
162
+ # end
163
+ #
164
+ # error = -> failure { raise failure.exception }
165
+ #
166
+ # a = Remap::State.call("A")
167
+ # map.call(a, &error).key?(:value) # => false
168
+ #
169
+ # b = Remap::State.call("BA")
170
+ # map.call(b, &error).key?(:value) # => false
171
+ #
172
+ # c = Remap::State.call("C")
173
+ # map.call(c, &error).fetch(:value) # => "C"
174
+ #
175
+ # @return [Map]
90
176
  def if_not(&block)
91
- add do |state|
92
- state.execute(&block).fmap do |bool, &error|
93
- bool ? error["#if_not returned true"] : state.value
177
+ add do |outer_state|
178
+ outer_state.execute(&block).fmap do |bool, state|
179
+ bool ? state.notice!("#if_not returned false") : outer_state.value
94
180
  end
95
181
  end
96
182
  end
97
183
 
98
184
  private
99
185
 
186
+ # @return [self]
100
187
  def add(&block)
101
188
  tap { fn << block }
102
189
  end
103
190
 
191
+ # @return [Array<Proc>]
104
192
  def fn
105
193
  @fn ||= []
106
194
  end
195
+
196
+ # @return [Proc]
197
+ def callback
198
+ -> state do
199
+ fn.reduce(state) do |inner, fn|
200
+ fn[inner]
201
+ end
202
+ end
203
+ end
204
+
205
+ # Catches :fatal and raises {Notice::Error}
206
+ #
207
+ # @param state [State]
208
+ # @param id (:fatal) [:fatal, :notice, :ignore]
209
+ #
210
+ # raise [Notice::Error]
211
+ def fatal(state, id: :fatal, &block)
212
+ raise catch(id, &block).traced(backtrace).exception
213
+ end
214
+
215
+ # Catches :notice exceptions and repackages them as a state
216
+ #
217
+ # @param state [State]
218
+ #
219
+ # @return [State]
220
+ def notice(state, &block)
221
+ state.set(notice: catch(:notice, &block).traced(backtrace)).except(:value)
222
+ end
223
+
224
+ # @abstract
225
+ def ignore(...)
226
+ raise NotImplementedError, "#{self.class}#ignore"
227
+ end
107
228
  end
108
229
  end
109
230
  end