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
data/lib/remap/mapper.rb CHANGED
@@ -3,54 +3,7 @@
3
3
  module Remap
4
4
  # @abstract
5
5
  class Mapper < Struct
6
- # Tries {self} and {other} and returns the first successful result
7
- #
8
- # @param other [Mapper]
9
- #
10
- # @return [Mapper::Or]
11
- module Operations
12
- def |(other)
13
- Or.new(left: self, right: other)
14
- rescue Dry::Struct::Error => e
15
- raise ArgumentError, e.message
16
- end
17
-
18
- # Returns a successful result when {self} & {other} are successful
19
- #
20
- # @param other [Mapper]
21
- #
22
- # @return [Mapper::And]
23
- def &(other)
24
- And.new(left: self, right: other)
25
- rescue Dry::Struct::Error => e
26
- raise ArgumentError, e.message
27
- end
28
-
29
- # Returns a successful result when only one of {self} & {other} are successful
30
- #
31
- # @param other [Mapper]
32
- #
33
- # @return [Mapper:Xor]
34
- def ^(other)
35
- Xor.new(left: self, right: other)
36
- rescue Dry::Struct::Error => e
37
- raise ArgumentError, e.message
38
- end
39
- end
40
-
41
- include Operations
42
6
  extend Operations
43
-
44
- # Creates a new mapper using {state}
45
- #
46
- # @param state [State]
47
- #
48
- # @yield [State]
49
- # If the call fails, the block is invoked with the state
50
- # @yieldreturn [State]
51
- #
52
- # @return [State]
53
- #
54
- # @private
7
+ include Operations
55
8
  end
56
9
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Notice
5
+ class Traced < Concrete
6
+ attribute? :backtrace, Types::Backtrace, default: EMPTY_ARRAY
7
+
8
+ def traced(backtrace)
9
+ Notice.call(**attributes, backtrace: backtrace)
10
+ end
11
+
12
+ def exception
13
+ return super if backtrace.blank?
14
+
15
+ super.tap { _1.set_backtrace(backtrace.map(&:to_s)) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Notice
5
+ class Untraced < Concrete
6
+ def traced(backtrace)
7
+ Traced.call(**attributes, backtrace: backtrace)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ using Extensions::Hash
5
+
6
+ class Notice < Dry::Interface
7
+ attribute? :value, Types::Any
8
+ attribute :reason, String
9
+ attribute :path, Array
10
+
11
+ class Error < Remap::Error
12
+ extend Dry::Initializer
13
+
14
+ param :notice, type: Notice
15
+ end
16
+
17
+ def inspect
18
+ "#<%s %s>" % [self.class, to_hash.formated]
19
+ end
20
+ alias to_s inspect
21
+
22
+ # Hash representation of the notice
23
+ #
24
+ # @return [Hash]
25
+ def to_hash
26
+ super.except(:backtrace).compact_blank
27
+ end
28
+
29
+ # @return [Error]
30
+ def exception
31
+ Error.new(self)
32
+ end
33
+ end
34
+ end
@@ -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