remap 2.0.3 → 2.1.0

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 +127 -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
@@ -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
@@ -2,41 +2,31 @@
2
2
 
3
3
  module Remap
4
4
  class Rule
5
- class Set < self
6
- using State::Extension
5
+ using State::Extension
7
6
 
8
- attribute :value, Types.Interface(:call)
9
- attribute :path, Path
7
+ # Set path to a static value
8
+ #
9
+ # @example Set path [:a, :b] to value "C"
10
+ # value = Remap::Static::Fixed.new(value: "a value")
11
+ # set = Remap::Rule::Set.new(value: value, path: [:a, :b])
12
+ # state = Remap::State.call("ANY VALUE")
13
+ # set.call(state).fetch(:value) # => { a: { b: "a value" } }
14
+ #
15
+ # @example Set path [:a, :b] to option :c
16
+ # value = Remap::Static::Option.new(name: :c)
17
+ # set = Remap::Rule::Set.new(value: value, path: [:a, :b])
18
+ # state = Remap::State.call("ANY VALUE", options: { c: "C" })
19
+ # set.call(state).fetch(:value) # => { a: { b: "C" } }
20
+ class Set < Concrete
21
+ # @return [Static]
22
+ attribute :value, Static, alias: :rule
10
23
 
11
- # Returns {value} mapped to {path} regardless of input
12
- #
13
- # @param state [State]
14
- #
15
- # @example Given an option
16
- # class Mapper < Remap::Base
17
- # option :name
18
- #
19
- # define do
20
- # set [:person, :name], to: option(:name)
21
- # end
22
- # end
23
- #
24
- # Mapper.call(input, name: "John") # => { person: { name: "John" } }
25
- #
26
- # @example Given a value
27
- # class Mapper < Remap::Base
28
- # define do
29
- # set [:api_key], to: value("ABC-123")
30
- # end
31
- # end
32
- #
33
- # Mapper.call(input) # => { api_key: "ABC-123" }
34
- #
35
- # @return [State]
36
- def call(state)
37
- path.call(state) do
38
- value.call(state)
39
- end
24
+ # @return [Path::Output]
25
+ attribute :path, Path::Output
26
+
27
+ # @see Rule#call
28
+ def call(...)
29
+ rule.call(...).then(&path)
40
30
  end
41
31
  end
42
32
  end
@@ -3,19 +3,19 @@
3
3
  module Remap
4
4
  class Rule
5
5
  class Collection
6
- class Empty < Unit
7
- using State::Extension
6
+ using State::Extension
8
7
 
9
- attribute? :rules, Value(EMPTY_ARRAY).default { EMPTY_ARRAY }
8
+ # Represents an empty rule block
9
+ class Empty < Unit
10
+ attribute? :rules, Value(EMPTY_ARRAY), default: EMPTY_ARRAY
10
11
 
11
12
  # Represents an empty define block, without any rules
12
13
  #
13
- # @param input [Any]
14
- # @param state [State]
14
+ # @param state [State<T>]
15
15
  #
16
- # @return [Monad::Failure]
16
+ # @return [State<T>]
17
17
  def call(state)
18
- state.problem("No rules, empty block")
18
+ state.notice!("No rules, empty block")
19
19
  end
20
20
  end
21
21
  end
@@ -3,23 +3,36 @@
3
3
  module Remap
4
4
  class Rule
5
5
  class Collection
6
- class Filled < Unit
7
- using State::Extension
6
+ using State::Extension
8
7
 
8
+ # Represents a non-empty rule block
9
+ #
10
+ # @example A collection containing a single rule
11
+ # state = Remap::State.call("A")
12
+ # void = Remap::Rule::Void.call({})
13
+ # rule = Remap::Rule::Collection.call([void])
14
+ # error = -> failure { raise failure.exception }
15
+ # rule.call(state, &error).fetch(:value) # => "A"
16
+ class Filled < Unit
17
+ # @return [Array<Rule>]
9
18
  attribute :rules, [Types.Interface(:call)], min_size: 1
10
19
 
11
20
  # Represents a non-empty define block with one or more rules
12
- # Calls every {#rules} with {input} and merges the output
21
+ # Calls every {#rules} with state and merges the output
13
22
  #
14
23
  # @param state [State]
15
24
  #
16
25
  # @return [State]
17
- def call(state)
18
- rules.map do |rule|
19
- Thread.new(rule, state) { _1.call(_2) }
20
- end.map(&:value).reduce do |acc, inner_state|
21
- acc.merged(inner_state)
26
+ def call(state, &error)
27
+ unless error
28
+ raise ArgumentError, "Collection::Filled#call(state, &error) requires a block"
22
29
  end
30
+
31
+ rules.map do |rule|
32
+ rule.call(state) do |failure|
33
+ return error[failure]
34
+ end
35
+ end.reduce(&:combine)
23
36
  end
24
37
  end
25
38
  end
@@ -2,9 +2,17 @@
2
2
 
3
3
  module Remap
4
4
  class Rule
5
- class Collection < Dry::Interface
6
- def to_proc
7
- method(:call).to_proc
5
+ # Represents a block defined by a rule
6
+ class Collection < Abstract
7
+ attribute :rules, Array
8
+
9
+ # @param state [State]
10
+ #
11
+ # @return [State]
12
+ #
13
+ # @abstract
14
+ def call(state)
15
+ raise NotImplementedError, "#{self.class}#call not implemented"
8
16
  end
9
17
  end
10
18
  end
@@ -2,17 +2,32 @@
2
2
 
3
3
  module Remap
4
4
  class Rule
5
- class Enum
6
- include Dry::Core::Constants
5
+ class Enum < Proxy
7
6
  include Dry::Monads[:maybe]
8
7
 
9
- extend Dry::Initializer
10
-
8
+ # @return [Hash]
11
9
  option :mappings, default: -> { Hash.new { default } }
10
+
11
+ # @return [Maybe]
12
12
  option :default, default: -> { None() }
13
13
 
14
14
  alias execute instance_eval
15
15
 
16
+ # Builds an enumeration using the block as context
17
+ #
18
+ # @example
19
+ # enum = Remap::Rule::Enum.call do
20
+ # from "B", to: "C"
21
+ # value "A"
22
+ # otherwise "D"
23
+ # end
24
+ #
25
+ # enum.get("A") # => "A"
26
+ # enum.get("B") # => "C"
27
+ # enum.get("C") # => "C"
28
+ # enum.get("MISSING") # => "D"
29
+ #
30
+ # @return [Any]
16
31
  def self.call(&block)
17
32
  unless block
18
33
  raise ArgumentError, "no block given"
@@ -21,40 +36,48 @@ module Remap
21
36
  new.tap { _1.execute(&block) }
22
37
  end
23
38
 
24
- def [](key)
25
- mappings[key]
26
- end
27
-
39
+ # Translates key into a value using predefined mappings
40
+ #
41
+ # @param key [#hash]
42
+ #
43
+ # @yield [String]
44
+ # If the key is not found & no default value is set
45
+ #
46
+ # @return [Any]
28
47
  def get(key, &error)
29
48
  unless error
30
49
  return get(key) { raise Error, _1 }
31
50
  end
32
51
 
33
- mappings[key].bind { return _1 }.or do
52
+ self[key].bind { return _1 }.or do
34
53
  error["Enum key [#{key}] not found among [#{mappings.keys.inspect}]"]
35
54
  end
36
55
  end
37
56
  alias call get
38
57
 
39
- # Map all keys in {keys} to {to}
40
- #
41
- # @return [VOID]
58
+ # @return [Maybe]
59
+ def [](key)
60
+ mappings[key]
61
+ end
62
+
63
+ # @return [void]
42
64
  def from(*keys, to:)
65
+ value = Some(to)
66
+
43
67
  keys.each do |key|
44
- mappings[key] = Some(to)
68
+ mappings[key] = value
69
+ mappings[to] = value
45
70
  end
46
71
  end
47
72
 
48
- # Maps {var} to {var}
49
- #
50
- # @return [VOID]
51
- def value(id)
52
- from(id, to: id)
73
+ # @return [void]
74
+ def value(*ids)
75
+ ids.each do |id|
76
+ from(id, to: id)
77
+ end
53
78
  end
54
79
 
55
- # Fallback value when {#call} fails
56
- #
57
- # @return [Void]
80
+ # @return [void]
58
81
  def otherwise(value)
59
82
  mappings.default = Some(value)
60
83
  end
@@ -2,27 +2,26 @@
2
2
 
3
3
  module Remap
4
4
  class Rule
5
- class Void < self
6
- using State::Extension
5
+ using State::Extension
7
6
 
8
- # Returns its input
7
+ # Represents a mapping without block
8
+ #
9
+ # @example Maps "A" to "A"
10
+ # class Mapper < Remap::Base
11
+ # define do
12
+ # map
13
+ # end
14
+ # end
15
+ #
16
+ # Mapper.call("A") # => "A"
17
+ class Void < Concrete
18
+ # @param state [State<T>]
9
19
  #
10
- # @param state [State]
11
- #
12
- # @example An empty rule
13
- # class Mapper < Remap::Base
14
- # define do
15
- # map do
16
- # # Empty ...
17
- # end
18
- # end
19
- # end
20
- #
21
- # Mapper.call(input) # => input
22
- #
23
- # @return [State]
20
+ # @return [State<T>]
24
21
  def call(state)
25
- state.bind { _2.set(_1) }
22
+ state.bind do |value, inner_state|
23
+ inner_state.set(value)
24
+ end
26
25
  end
27
26
  end
28
27
  end
@@ -1,29 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/array/wrap"
4
-
5
3
  module Remap
6
4
  class Rule
7
- class Wrap < self
8
- using State::Extension
5
+ using State::Extension
9
6
 
7
+ # Wraps rule in a type
8
+ #
9
+ # @example Maps { name: "Ford" } to { cars: ["Ford"] }
10
+ # class Mapper < Remap::Base
11
+ # define do
12
+ # to :cars do
13
+ # wrap(:array) do
14
+ # map :name
15
+ # end
16
+ # end
17
+ # end
18
+ # end
19
+ #
20
+ # Mapper.call({ name: "Ford" }) # => { cars: ["Ford"] }
21
+ class Wrap < Concrete
22
+ # @return [:array]
10
23
  attribute :type, Value(:array)
11
- attribute :rule, Types::Any
24
+
25
+ # @return [Rule]
26
+ attribute :rule, Types::Rule
12
27
 
13
28
  # Wraps the output from {#rule} in a {#type}
14
29
  #
15
- # @param state [State]
16
- #
17
- # @example mapps { car: "Volvo" } to { cars: ["Volvo"] }
18
- # to :cars do
19
- # wrap(:array) do
20
- # map :car
21
- # end
22
- # end
23
- #
24
- # @return [State]
25
- def call(state)
26
- rule.call(state).fmap { Array.wrap(_1) }
30
+ # @see Rule#call
31
+ def call(...)
32
+ rule.call(...).fmap do |value|
33
+ Array.wrap(value)
34
+ end
27
35
  end
28
36
  end
29
37
  end
data/lib/remap/rule.rb CHANGED
@@ -1,8 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Remap
4
- class Rule < Dry::Concrete
4
+ class Rule < Dry::Interface
5
5
  defines :requirement
6
6
  requirement Types::Any
7
+
8
+ # @param state [State]
9
+ #
10
+ # @abstract
11
+ def call(state)
12
+ raise NotImplementedError, "#{self.class}#call not implemented"
13
+ end
7
14
  end
8
15
  end