remap 2.0.3 → 2.1.0

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