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