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