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,16 +2,27 @@
2
2
 
3
3
  module Remap
4
4
  class Constructor
5
- class Argument < Concrete
6
- using State::Extension
5
+ using State::Extension
7
6
 
7
+ # Allows a class (target) to be called with a regular argument
8
+ class Argument < Concrete
9
+ # @return [:argument]
8
10
  attribute :strategy, Value(:argument), default: :argument
9
11
 
10
- # Uses the {#method} method to initialize {#target} with {state}
11
- # Target is only called if {state} is defined
12
+ # Uses the {#method} method to initialize {#target} with state
13
+ # Target is only called if state is defined
14
+ #
15
+ # Used by {Remap::Base} to define constructors for mapped data
12
16
  #
13
17
  # Fails if {#target} does not respond to {#method}
14
- # Fails if {#target} cannot be called with {state}
18
+ # Fails if {#target} cannot be called with state
19
+ #
20
+ # @example Initialize a target with a state
21
+ # target = ::Struct.new(:foo)
22
+ # constructor = Remap::Constructor.call(strategy: :argument, target: target, method: :new)
23
+ # state = Remap::State.call(:bar)
24
+ # new_state = constructor.call(state)
25
+ # new_state.fetch(:value).foo # => :bar
15
26
  #
16
27
  # @param state [State]
17
28
  #
@@ -20,7 +31,10 @@ module Remap
20
31
  super.fmap do |input|
21
32
  target.public_send(id, input)
22
33
  rescue ArgumentError => e
23
- raise e.exception("Could not load target [#{target}] using the argument strategy with [#{input}] (#{input.class})")
34
+ raise e.exception("Failed to create [%p] with input [%s] (%s)" % [
35
+ target, input,
36
+ input.class
37
+ ])
24
38
  end
25
39
  end
26
40
  end
@@ -2,15 +2,26 @@
2
2
 
3
3
  module Remap
4
4
  class Constructor
5
- class Keyword < Concrete
6
- using State::Extension
5
+ using State::Extension
7
6
 
7
+ # Allows a class (target) to be called with keyword arguments
8
+ class Keyword < Concrete
9
+ # @return [:keyword]
8
10
  attribute :strategy, Value(:keyword)
9
11
 
10
12
  # Calls {#target} as with keyword arguments
11
13
  #
12
14
  # Fails if {#target} does not respond to {#method}
13
- # Fails if {#target} cannot be called with {state}
15
+ # Fails if {#target} cannot be called with state
16
+ #
17
+ # Used by {Remap::Base} to define constructors for mapped data
18
+ #
19
+ # @example Initialize a target with a state
20
+ # target = OpenStruct
21
+ # constructor = Remap::Constructor.call(strategy: :keyword, target: target, method: :new)
22
+ # state = Remap::State.call({ foo: :bar })
23
+ # new_state = constructor.call(state)
24
+ # new_state.fetch(:value).foo # => :bar
14
25
  #
15
26
  # @param state [State]
16
27
  #
@@ -23,7 +34,12 @@ module Remap
23
34
 
24
35
  target.public_send(id, **input)
25
36
  rescue ArgumentError => e
26
- raise e.exception("Could not load target [#{target}] using the keyword strategy using [#{input}] (#{input.class})")
37
+ raise e.exception("Failed to create [%p] with input [%s] (%s}) using method %s" % [
38
+ target,
39
+ input,
40
+ input.class,
41
+ id
42
+ ])
27
43
  end
28
44
  end
29
45
  end
@@ -2,15 +2,14 @@
2
2
 
3
3
  module Remap
4
4
  class Constructor
5
+ # Default type used by {Remap::Base}
5
6
  class None < Concrete
6
7
  attribute :target, Types::Nothing
7
8
  attribute :strategy, Types::Any
8
9
  attribute :method, Types::Any
9
10
 
10
- # Just returns the input state
11
- #
12
- # Fails if {#target} does not respond to {#method}
13
- # Fails if {#target} cannot be called with {state}
11
+ # Used by {Remap::Base} as a default constructor
12
+ # Using it does nothing but return its input state
14
13
  #
15
14
  # @param state [State]
16
15
  #
@@ -2,8 +2,11 @@
2
2
 
3
3
  module Remap
4
4
  class Constructor < Dry::Interface
5
+ # @return [Any]
6
+ attribute :target, Types::Any, not_eql: Nothing
7
+
8
+ # @return [Symbol]
5
9
  attribute :method, Symbol, default: :new
6
- attribute :target, Types::Any.constrained(not_eql: Nothing)
7
10
 
8
11
  # Ensures {#target} responds to {#method}
9
12
  # Returns an error state unless above is true
@@ -19,12 +22,16 @@ module Remap
19
22
  end
20
23
  end
21
24
 
22
- def id
23
- attributes.fetch(:method)
24
- end
25
-
25
+ # @return [Proc]
26
26
  def to_proc
27
27
  method(:call).to_proc
28
28
  end
29
+
30
+ private
31
+
32
+ # @return [Symbol]
33
+ def id
34
+ attributes.fetch(:method)
35
+ end
29
36
  end
30
37
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Contract < Dry::Validation::Contract
5
+ # Constructs a contract used to validate mapper input
6
+ #
7
+ # @param rules [Array<Proc>]
8
+ # @param options [Hash]
9
+ # @param contract [Proc]
10
+ # @param attributes [Hash]
11
+ #
12
+ # @return [Contract]
13
+ def self.call(rules:, options:, contract:, attributes:)
14
+ Class.new(self) do
15
+ rules.each do |rule|
16
+ class_eval(&rule)
17
+ end
18
+
19
+ options.each do |option|
20
+ class_eval(&option)
21
+ end
22
+
23
+ schema(contract)
24
+ end.new(**attributes)
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ module Extensions
5
+ using Object
6
+
7
+ module Enumerable
8
+ refine ::Enumerable do
9
+ # Creates a hash using {self} as the {path} and {value} as the hash value
10
+ #
11
+ # @param value [Any] Hash value
12
+ #
13
+ # @example A hash from path
14
+ # [:a, :b].hide('value') # => { a: { b: 'value' } }
15
+ #
16
+ # @return [Hash]
17
+ def hide(value)
18
+ reverse.reduce(value) do |element, key|
19
+ { key => element }
20
+ end
21
+ end
22
+
23
+ # Fetches value at {path}
24
+ #
25
+ # @example Fetch value at path
26
+ # [[:a, :b], [:c, :d]].get(0, 1) # => :b
27
+ #
28
+ # @return [Any]
29
+ #
30
+ # @raise When path cannot be found
31
+ def get(*path, &error)
32
+ _, result = path.reduce([
33
+ EMPTY_ARRAY,
34
+ self
35
+ ]) do |(current_path, element), key|
36
+ value = element.fetch(key) do
37
+ raise PathError, current_path + [key]
38
+ end
39
+
40
+ [current_path + [key], value]
41
+ end
42
+
43
+ result
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ module Extensions
5
+ module Hash
6
+ refine ::Hash do
7
+ def formated
8
+ JSON.neat_generate(self, sort: true, wrap: 40, aligned: true, around_colon: 1)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ module Extensions
5
+ module Object
6
+ refine ::Object do
7
+ # Fallback validation method
8
+ #
9
+ # @yield if block is provided
10
+ #
11
+ # @raise unless block is provided
12
+ def _(&block)
13
+ unless block
14
+ return _ { raise _1 }
15
+ end
16
+
17
+ block["Expected a state, got [#{self}] (#{self.class})"]
18
+ end
19
+
20
+ # Fallback method used when #get is called on an object that does not respond to #get
21
+ #
22
+ # Block is invoked, if provided
23
+ # Otherwise a symbol is thrown
24
+ #
25
+ # @param path [Array<Key>]
26
+ def get(*path, &block)
27
+ raise PathError, []
28
+ end
29
+ alias_method :fetch, :get
30
+
31
+ def formated
32
+ self
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/remap/failure.rb CHANGED
@@ -1,27 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Remap
4
- class Failure < Result
5
- attribute :reasons, Types::Hash
4
+ using Extensions::Hash
6
5
 
7
- def inspect
8
- format("Failure<[%<result>s]>", result: JSON.pretty_generate(to_h))
9
- end
6
+ class Failure < Dry::Concrete
7
+ attribute :failures, [Notice], min_size: 1
8
+ attribute? :notices, [Notice], default: EMPTY_ARRAY
10
9
 
11
- def to_hash
12
- { failure: reasons, problems: problems }
13
- end
10
+ # Merges two failures
11
+ #
12
+ # @param other [Failure]
13
+ #
14
+ # @return [Failure]
15
+ def merge(other)
16
+ unless other.is_a?(self.class)
17
+ raise ArgumentError, "can't merge #{self.class} with #{other.class}"
18
+ end
14
19
 
15
- def failure?(*)
16
- true
17
- end
20
+ failure = attributes.deep_merge(other.attributes) do |_, value1, value2|
21
+ case [value1, value2]
22
+ in [Array, Array]
23
+ value1 + value2
24
+ else
25
+ raise ArgumentError, "can't merge #{self.class} with #{other.class}"
26
+ end
27
+ end
18
28
 
19
- def success?(*)
20
- false
29
+ new(failure)
21
30
  end
22
31
 
23
- def fmap
24
- self
32
+ # @return [String]
33
+ def exception
34
+ Error.new(attributes.formated)
25
35
  end
26
36
  end
27
37
  end
@@ -2,28 +2,37 @@
2
2
 
3
3
  module Remap
4
4
  class Iteration
5
+ using State::Extension
6
+
7
+ # Implements an array iterator which defines index in state
5
8
  class Array < Concrete
6
- using State::Extension
9
+ # @return [Array<T>]
10
+ attribute :value, Types::Array, alias: :array
7
11
 
8
- attribute :value, Types::Array
12
+ # @return [State<Array<T>>]
9
13
  attribute :state, Types::State
10
14
 
11
- def map(&block)
12
- value.each_with_index.reduce(init) do |input_state, (value, index)|
13
- block[value, index: index]._.then do |new_state|
14
- new_state.fmap { [_1] }
15
- end.then do |new_array_state|
16
- input_state.merged(new_array_state)
17
- end
18
- end._
15
+ # @see Iteration#map
16
+ def call(&block)
17
+ array.each_with_index.reduce(init) do |state, (value, index)|
18
+ reduce(state, value, index, &block)
19
+ end
19
20
  end
20
- alias call map
21
21
 
22
22
  private
23
23
 
24
24
  def init
25
25
  state.set(EMPTY_ARRAY)
26
26
  end
27
+
28
+ def reduce(state, value, index, &block)
29
+ notice = catch :ignore do
30
+ other = block[value, index: index]
31
+ return state.combine(other.fmap { [_1] })
32
+ end
33
+
34
+ state.set(notice: notice)
35
+ end
27
36
  end
28
37
  end
29
38
  end
@@ -2,26 +2,34 @@
2
2
 
3
3
  module Remap
4
4
  class Iteration
5
+ using State::Extension
6
+
7
+ # Implements a hash iterator which defines key in state
5
8
  class Hash < Concrete
6
- attribute :value, Types::Hash
9
+ # @return [Hash]
10
+ attribute :value, Types::Hash, alias: :hash
11
+
12
+ # @return [State<Hash>]
7
13
  attribute :state, Types::State
8
14
 
9
- using State::Extension
10
-
11
- # @see Base#map
12
- def map(&block)
13
- value.reduce(init) do |input_state, (key, value)|
14
- block[value, key: key]._.then do |new_state|
15
- new_state.fmap { { key => _1 } }
16
- end.then do |new_hash_state|
17
- input_state.merged(new_hash_state)
18
- end
19
- end._
15
+ # @see Iteration#map
16
+ def call(&block)
17
+ hash.reduce(init) do |state, (key, value)|
18
+ reduce(state, key, value, &block)
19
+ end
20
20
  end
21
- alias call map
22
21
 
23
22
  private
24
23
 
24
+ def reduce(state, key, value, &block)
25
+ notice = catch :ignore do
26
+ other = block[value, key: key]
27
+ return state.combine(other.fmap { { key => _1 } })
28
+ end
29
+
30
+ state.set(notice: notice)
31
+ end
32
+
25
33
  def init
26
34
  state.set(EMPTY_HASH)
27
35
  end
@@ -2,17 +2,17 @@
2
2
 
3
3
  module Remap
4
4
  class Iteration
5
+ using State::Extension
6
+
7
+ # Default iterator which doesn't do anything
5
8
  class Other < Concrete
9
+ attribute :value, Types::Any, alias: :other
6
10
  attribute :state, Types::State
7
- attribute :value, Types::Any
8
-
9
- using State::Extension
10
11
 
11
- # @see Base#map
12
- def map(&block)
13
- block[value]._
12
+ # @see Iteration#map
13
+ def call(&block)
14
+ state.fatal!("Expected an enumerable, got %s (%s)", value, value.class)
14
15
  end
15
- alias call map
16
16
  end
17
17
  end
18
18
  end
@@ -2,10 +2,13 @@
2
2
 
3
3
  module Remap
4
4
  class Iteration < Dry::Interface
5
- attribute :value, Types::Value
5
+ # @return [State<T>]
6
6
  attribute :state, Types::State
7
7
 
8
- # Maps every element in {#value} to {#block}
8
+ # @return [T]
9
+ attribute :value, Types::Any
10
+
11
+ # Maps every element in {#value}
9
12
  #
10
13
  # @abstract
11
14
  #
@@ -14,6 +17,9 @@ module Remap
14
17
  # @yieldreturn [Array<V>, Hash<V, K>]
15
18
  #
16
19
  # @return [Array<V>, Hash<V, K>]
20
+ def call(state)
21
+ raise NotImplementedError, "#{self.class}#call not implemented"
22
+ end
17
23
 
18
24
  order :Hash, :Array, :Other
19
25
  end
@@ -2,14 +2,36 @@
2
2
 
3
3
  module Remap
4
4
  class Mapper
5
- class And < Binary
6
- using State::Extension
5
+ using State::Extension
7
6
 
7
+ # Represents two mappers that are combined with the & operator
8
+ #
9
+ # @example Combine two mappers
10
+ # class Mapper1 < Remap::Base
11
+ # contract do
12
+ # required(:a1)
13
+ # end
14
+ # end
15
+ #
16
+ # class Mapper2 < Remap::Base
17
+ # contract do
18
+ # required(:a2)
19
+ # end
20
+ # end
21
+ #
22
+ # state = Remap::State.call({ a2: 2, a1: 1 })
23
+ # output = (Mapper1 & Mapper2).call!(state)
24
+ # output.fetch(:value) # => { a2: 2, a1: 1 }
25
+ class And < Binary
26
+ # Succeeds if both left and right succeed
27
+ # Returns the combined result of left and right
28
+ #
29
+ # @param state [State]
30
+ #
31
+ # @yield [Failure] if mapper fails
32
+ #
33
+ # @return [Result]
8
34
  def call!(state, &error)
9
- unless error
10
- return call!(state, &exception)
11
- end
12
-
13
35
  state1 = left.call!(state) do |failure1|
14
36
  right.call!(state) do |failure2|
15
37
  return error[failure1.merge(failure2)]
@@ -22,7 +44,7 @@ module Remap
22
44
  return error[failure]
23
45
  end
24
46
 
25
- state1.merged(state2)
47
+ state1.combine(state2)
26
48
  end
27
49
  end
28
50
  end
@@ -2,15 +2,12 @@
2
2
 
3
3
  module Remap
4
4
  class Mapper
5
+ # @abstract
5
6
  class Binary < self
7
+ include Operation
8
+
6
9
  attribute :left, Types::Mapper
7
10
  attribute :right, Types::Mapper
8
-
9
- def exception
10
- ->(error) { raise error }
11
- end
12
-
13
- include Operation
14
11
  end
15
12
  end
16
13
  end
@@ -2,14 +2,37 @@
2
2
 
3
3
  module Remap
4
4
  class Mapper
5
- class Or < Binary
6
- using State::Extension
5
+ using State::Extension
7
6
 
7
+ # Represents two mappers that are combined with the | operator
8
+ #
9
+ # @example Combine two mappers
10
+ # class Mapper1 < Remap::Base
11
+ # contract do
12
+ # required(:a1)
13
+ # end
14
+ # end
15
+ #
16
+ # class Mapper2 < Remap::Base
17
+ # contract do
18
+ # required(:a2)
19
+ # end
20
+ # end
21
+ #
22
+ # state = Remap::State.call({ a2: 2 })
23
+ # result = (Mapper1 | Mapper2).call!(state)
24
+ # result.fetch(:value) # => { a2: 2 }
25
+ class Or < Binary
26
+ # Succeeds if left or right succeeds
27
+ # Returns which ever succeeds first
28
+ #
29
+ # @param state [State]
30
+ #
31
+ # @yieldparam [Failure] if mapper fails
32
+ # @yieldreturn [Failure]
33
+ #
34
+ # @return [Result]
8
35
  def call!(state, &error)
9
- unless error
10
- return call!(state, &exception)
11
- end
12
-
13
36
  left.call!(state) do |failure1|
14
37
  return right.call!(state) do |failure2|
15
38
  return error[failure1.merge(failure2)]
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Mapper
5
+ module Operations
6
+ # Tries self and other and returns the first successful result
7
+ #
8
+ # @param other [Mapper]
9
+ #
10
+ # @return [Mapper::Or]
11
+ def |(other)
12
+ Or.new(left: self, right: other)
13
+ rescue Dry::Struct::Error => e
14
+ raise ArgumentError, e.message
15
+ end
16
+
17
+ # Returns a successful result when self & other are successful
18
+ #
19
+ # @param other [Mapper]
20
+ #
21
+ # @return [Mapper::And]
22
+ def &(other)
23
+ And.new(left: self, right: other)
24
+ rescue Dry::Struct::Error => e
25
+ raise ArgumentError, e.message
26
+ end
27
+
28
+ # Returns a successful result when only one of self & other are successful
29
+ #
30
+ # @param other [Mapper]
31
+ #
32
+ # @return [Mapper:Xor]
33
+ def ^(other)
34
+ Xor.new(left: self, right: other)
35
+ rescue Dry::Struct::Error => e
36
+ raise ArgumentError, e.message
37
+ end
38
+ end
39
+ end
40
+ end
@@ -2,14 +2,36 @@
2
2
 
3
3
  module Remap
4
4
  class Mapper
5
- class Xor < Binary
6
- using State::Extension
5
+ using State::Extension
7
6
 
7
+ # Represents two mappers that are combined with the ^ operator
8
+ #
9
+ # @example Combine two mappers
10
+ # class Mapper1 < Remap::Base
11
+ # contract do
12
+ # required(:a1)
13
+ # end
14
+ # end
15
+ #
16
+ # class Mapper2 < Remap::Base
17
+ # contract do
18
+ # required(:a2)
19
+ # end
20
+ # end
21
+ #
22
+ # state = Remap::State.call({ a2: 2 })
23
+ # output = (Mapper1 ^ Mapper2).call!(state)
24
+ # output.fetch(:value) # => { a2: 2 }
25
+ class Xor < Binary
26
+ # Succeeds if left or right succeeds, but not both
27
+ #
28
+ # @param state [State]
29
+ #
30
+ # @yieldparam [Failure] if mapper fails
31
+ # @yieldreturn [Failure]
32
+ #
33
+ # @return [Result]
8
34
  def call!(state, &error)
9
- unless error
10
- return call!(state, &exception)
11
- end
12
-
13
35
  state1 = left.call!(state) do |failure1|
14
36
  return right.call!(state) do |failure2|
15
37
  return error[failure1.merge(failure2)]
@@ -20,7 +42,7 @@ module Remap
20
42
  return state1
21
43
  end
22
44
 
23
- error[state1.merged(state2).failure("Both left and right passed in xor")]
45
+ state1.combine(state2).failure("Both left and right passed xor operation").then(&error)
24
46
  end
25
47
  end
26
48
  end