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