remap 2.0.2

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 (50) hide show
  1. checksums.yaml +7 -0
  2. data/lib/remap/base.rb +146 -0
  3. data/lib/remap/compiler.rb +171 -0
  4. data/lib/remap/constructor/argument.rb +28 -0
  5. data/lib/remap/constructor/keyword.rb +31 -0
  6. data/lib/remap/constructor/none.rb +23 -0
  7. data/lib/remap/constructor.rb +30 -0
  8. data/lib/remap/error.rb +7 -0
  9. data/lib/remap/failure.rb +27 -0
  10. data/lib/remap/iteration/array.rb +29 -0
  11. data/lib/remap/iteration/hash.rb +30 -0
  12. data/lib/remap/iteration/other.rb +18 -0
  13. data/lib/remap/iteration.rb +20 -0
  14. data/lib/remap/mapper/and.rb +29 -0
  15. data/lib/remap/mapper/binary.rb +16 -0
  16. data/lib/remap/mapper/or.rb +21 -0
  17. data/lib/remap/mapper/xor.rb +27 -0
  18. data/lib/remap/mapper.rb +56 -0
  19. data/lib/remap/nothing.rb +7 -0
  20. data/lib/remap/operation.rb +26 -0
  21. data/lib/remap/result.rb +11 -0
  22. data/lib/remap/rule/each.rb +37 -0
  23. data/lib/remap/rule/embed.rb +42 -0
  24. data/lib/remap/rule/map.rb +109 -0
  25. data/lib/remap/rule/set.rb +43 -0
  26. data/lib/remap/rule/support/collection/empty.rb +23 -0
  27. data/lib/remap/rule/support/collection/filled.rb +27 -0
  28. data/lib/remap/rule/support/collection.rb +11 -0
  29. data/lib/remap/rule/support/enum.rb +63 -0
  30. data/lib/remap/rule/support/path.rb +45 -0
  31. data/lib/remap/rule/void.rb +29 -0
  32. data/lib/remap/rule/wrap.rb +30 -0
  33. data/lib/remap/rule.rb +8 -0
  34. data/lib/remap/selector/all.rb +21 -0
  35. data/lib/remap/selector/index.rb +39 -0
  36. data/lib/remap/selector/key.rb +38 -0
  37. data/lib/remap/selector.rb +14 -0
  38. data/lib/remap/state/extension.rb +393 -0
  39. data/lib/remap/state/schema.rb +21 -0
  40. data/lib/remap/state/types.rb +11 -0
  41. data/lib/remap/state.rb +22 -0
  42. data/lib/remap/static/fixed.rb +20 -0
  43. data/lib/remap/static/option.rb +22 -0
  44. data/lib/remap/static.rb +6 -0
  45. data/lib/remap/struct.rb +7 -0
  46. data/lib/remap/success.rb +29 -0
  47. data/lib/remap/types.rb +47 -0
  48. data/lib/remap/version.rb +5 -0
  49. data/lib/remap.rb +28 -0
  50. metadata +329 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e5df1ff079e3e2d14f9f35541efed74d87bbebe54deb39e8571998a2bdb11850
4
+ data.tar.gz: ad295f5b2967f8bff292a662fc2cb7db222c724567edd52657f7e88b83560f33
5
+ SHA512:
6
+ metadata.gz: 57b267a4b302721f874ca93ffbf924ec3b1a90ee8d33d7728091bc9bb2f0a4e14b84ca60f037319030f4c9a4e0945d25217a70e772edac161f49f8f5a66797fd
7
+ data.tar.gz: 4e664e07f2edb8865836265d5e36484887b1b206410e45f92d73ed4cfdbd5c0b8201cae1156478ac8503163fe0c65b16a36776138797d7b5682c295c626cb0fb
data/lib/remap/base.rb ADDED
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "dry/monads/all"
4
+
5
+ module Remap
6
+ class Base < Mapper
7
+ include Dry::Core::Memoizable
8
+ include Dry::Core::Constants
9
+ extend Dry::Monads[:result]
10
+
11
+ extend Dry::Configurable
12
+ extend Forwardable
13
+
14
+ using State::Extension
15
+ extend State
16
+
17
+ CONTRACT = Dry::Schema.JSON do
18
+ # NOP
19
+ end
20
+
21
+ setting :constructor, default: IDENTITY
22
+ setting :options, default: EMPTY_ARRAY
23
+ setting :rules, default: EMPTY_ARRAY
24
+ setting :contract, default: CONTRACT
25
+ setting :context, default: IDENTITY
26
+
27
+ delegate [:config] => self
28
+
29
+ schema schema.strict(false)
30
+
31
+ # Holds the current context
32
+ # @private
33
+ def self.contract(&context)
34
+ config.contract = Dry::Schema.JSON(&context)
35
+ end
36
+
37
+ # @see Dry::Validation::Contract.rule
38
+ def self.rule(...)
39
+ config.rules << ->(*) { rule(...) }
40
+ end
41
+
42
+ # Defines a a constructor argument for the mapper
43
+ #
44
+ # @param name [Symbol]
45
+ # @param type [#call]
46
+ def self.option(field, type: Types::Any)
47
+ attribute(field, type)
48
+
49
+ unless (key = schema.keys.find { _1.name == field })
50
+ raise ArgumentError, "Could not locate [#{field}] in [#{self}]"
51
+ end
52
+
53
+ config.options << ->(*) { option(field, type: key) }
54
+ end
55
+
56
+ # Pretty print the mapper
57
+ #
58
+ # @return [String]
59
+ def self.inspect
60
+ "<#{self.class} #{rule}, #{self}>"
61
+ end
62
+
63
+ # Defines a mapper with a constructor used to wrap the output
64
+ #
65
+ # @param constructor [#call]
66
+ #
67
+ # @example A mapper from path :a to path :b
68
+ # class Mapper < Remap
69
+ # define do
70
+ # map :a, to: :b
71
+ # end
72
+ # end
73
+ #
74
+ # Mapper.call(a: 1) # => { b: 1 }
75
+ def self.define(target = Nothing, method: :new, strategy: :argument, &context)
76
+ unless context
77
+ raise ArgumentError, "Missing block"
78
+ end
79
+
80
+ config.context = Compiler.call(&context)
81
+ config.constructor = Constructor.call(method: method, strategy: strategy, target: target)
82
+ rescue Dry::Struct::Error => e
83
+ raise ArgumentError, e.message
84
+ end
85
+
86
+ # Creates a new mapper
87
+ #
88
+ # @param input [Any]
89
+ # @param params [Hash]
90
+
91
+ # @return [Context]
92
+
93
+ extend Operation
94
+
95
+ def self.call!(state, &error)
96
+ new(state.options).call(state._.set(mapper: self), &error)
97
+ rescue Dry::Struct::Error => e
98
+ raise ArgumentError, "Option missing to mapper [#{self}]: #{e}"
99
+ end
100
+
101
+ # Creates a mapper tree using {#context} and uses {#state} as argument
102
+ #
103
+ # @return [State]
104
+ #
105
+ # @see .call!
106
+ #
107
+ # @private
108
+ def call(state, &error)
109
+ unless error
110
+ raise ArgumentError, "Missing block"
111
+ end
112
+
113
+ state.tap do |input|
114
+ contract.call(input, state.options).tap do |result|
115
+ unless result.success?
116
+ return error[state.failure(result.errors.to_h)]
117
+ end
118
+ end
119
+ end
120
+
121
+ state.then(&config.context).then(&config.constructor)
122
+ end
123
+
124
+ private
125
+
126
+ def contract(scope: self)
127
+ Class.new(Dry::Validation::Contract) do |klass|
128
+ config = scope.class.config
129
+
130
+ config.rules.each do |rule|
131
+ klass.class_eval(&rule)
132
+ end
133
+
134
+ config.options.each do |option|
135
+ klass.class_eval(&option)
136
+ end
137
+
138
+ schema(config.contract)
139
+ end.new(**attributes)
140
+ end
141
+
142
+ def config
143
+ self.class.config
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Compiler
5
+ include Dry::Core::Constants
6
+ extend Dry::Initializer
7
+ extend Forwardable
8
+
9
+ param :rules, default: -> { EMPTY_ARRAY.dup }
10
+
11
+ # @return [Rule]
12
+ delegate call: self
13
+
14
+ # Constructs a rule tree given {block}
15
+ #
16
+ # @return [Rule]
17
+ def self.call(&block)
18
+ unless block
19
+ return Rule::Void.new
20
+ end
21
+
22
+ new.tap { _1.instance_eval(&block) }.rule
23
+ end
24
+
25
+ # Maps {path} to {to} with {block} inbetween
26
+ #
27
+ # @param path ([]) [Array<Segment>, Segment]
28
+ # @param to ([]) [Array<Symbol>, Symbol]
29
+ #
30
+ # @return [Rule::Map]
31
+ def map(*path, to: EMPTY_ARRAY, &block)
32
+ add Rule::Map.new(
33
+ path: {
34
+ map: path.flatten,
35
+ to: [to].flatten
36
+ },
37
+ rule: call(&block)
38
+ )
39
+ end
40
+
41
+ # Maps using {mapper}
42
+ #
43
+ # @param mapper [Remap]
44
+ #
45
+ # @return [Rule::Embed]
46
+ def embed(mapper)
47
+ add Rule::Embed.new(mapper: mapper)
48
+ rescue Dry::Struct::Error
49
+ raise ArgumentError, "Embeded mapper must be [Remap::Mapper], got [#{mapper}]"
50
+ end
51
+
52
+ # @param *path ([]) [Symbol, Array<Symbol>]
53
+ # @option to [Remap::Static]
54
+ #
55
+ # @return [Rule::Set]
56
+ # @raise [ArgumentError]
57
+ # if no path given
58
+ # if path is not a Symbol or Array<Symbol>
59
+ def set(*path, to:)
60
+ add Rule::Set.new(path: { to: path.flatten, map: EMPTY_ARRAY }, value: to)
61
+ rescue Dry::Struct::Error => e
62
+ raise ArgumentError, e.message
63
+ end
64
+
65
+ # Maps to {path} from {map} with {block} inbetween
66
+ #
67
+ # @param path [Array<Symbol>, Symbol]
68
+ # @param map [Array<Segment>, Segment]
69
+ #
70
+ # @return [Rule::Map]
71
+ def to(*path, map: EMPTY_ARRAY, &block)
72
+ map(*map, to: path, &block)
73
+ end
74
+
75
+ # Iterates over the input value, passes each value
76
+ # to its block and merges the result back together
77
+ #
78
+ # @return [Rule::Each]]
79
+ # @raise [ArgumentError] if no block given
80
+ def each(&block)
81
+ unless block
82
+ raise ArgumentError, "no block given"
83
+ end
84
+
85
+ add Rule::Each.new(rule: call(&block))
86
+ end
87
+
88
+ # Wraps output in {type}
89
+ #
90
+ # @param type [:array]
91
+ #
92
+ # @yieldreturn [Rule]
93
+ #
94
+ # @return [Rule::Wrap]
95
+ # @raise [ArgumentError] if type is not :array
96
+ def wrap(type, &block)
97
+ unless block
98
+ raise ArgumentError, "no block given"
99
+ end
100
+
101
+ add Rule::Wrap.new(type: type, rule: call(&block))
102
+ rescue Dry::Struct::Error => e
103
+ raise ArgumentError, e.message
104
+ end
105
+
106
+ # Selects all elements
107
+ #
108
+ # @return [Rule::Path::Segment::Quantifier::All]
109
+ def all
110
+ Selector::All.new(EMPTY_HASH)
111
+ end
112
+
113
+ # Static value to be selected
114
+ #
115
+ # @param value [Any]
116
+ #
117
+ # @return [Rule::Static::Fixed]
118
+ def value(value)
119
+ Static::Fixed.new(value: value)
120
+ end
121
+
122
+ # Static option to be selected
123
+ #
124
+ # @param id [Symbol]
125
+ #
126
+ # @return [Rule::Static::Option]
127
+ def option(id)
128
+ Static::Option.new(name: id)
129
+ end
130
+
131
+ # Selects {index} element in input
132
+ #
133
+ # @param index [Integer]
134
+ #
135
+ # @return [Path::Segment::Key]
136
+ # @raise [ArgumentError] if index is not an Integer
137
+ def at(index)
138
+ Selector::Index.new(index: index)
139
+ rescue Dry::Struct::Error
140
+ raise ArgumentError, "Selector at(index) requires an integer argument, got [#{index}] (#{index.class})"
141
+ end
142
+
143
+ # Selects first element in input
144
+ #
145
+ # @return [Path::Segment::Key]]
146
+ def first
147
+ at(0)
148
+ end
149
+ alias any first
150
+
151
+ # Selects last element in input
152
+ #
153
+ # @return [Path::Segment::Key]
154
+ def last
155
+ at(-1)
156
+ end
157
+
158
+ # The final rule
159
+ #
160
+ # @return [Rule]
161
+ def rule
162
+ Rule::Collection.call(rules: rules)
163
+ end
164
+
165
+ private
166
+
167
+ def add(rule)
168
+ rule.tap { rules << rule }
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Constructor
5
+ class Argument < Concrete
6
+ using State::Extension
7
+
8
+ attribute :strategy, Value(:argument), default: :argument
9
+
10
+ # Uses the {#method} method to initialize {#target} with {state}
11
+ # Target is only called if {state} is defined
12
+ #
13
+ # Fails if {#target} does not respond to {#method}
14
+ # Fails if {#target} cannot be called with {state}
15
+ #
16
+ # @param state [State]
17
+ #
18
+ # @return [State]
19
+ def call(state)
20
+ super.fmap do |input|
21
+ target.public_send(id, input)
22
+ rescue ArgumentError => e
23
+ raise e.exception("Could not load target [#{target}] using the argument strategy with [#{input}] (#{input.class})")
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Constructor
5
+ class Keyword < Concrete
6
+ using State::Extension
7
+
8
+ attribute :strategy, Value(:keyword)
9
+
10
+ # Calls {#target} as with keyword arguments
11
+ #
12
+ # Fails if {#target} does not respond to {#method}
13
+ # Fails if {#target} cannot be called with {state}
14
+ #
15
+ # @param state [State]
16
+ #
17
+ # @return [State]
18
+ def call(state)
19
+ super.fmap do |input, &error|
20
+ unless input.is_a?(Hash)
21
+ return error["Input is not a hash"]
22
+ end
23
+
24
+ target.public_send(id, **input)
25
+ rescue ArgumentError => e
26
+ raise e.exception("Could not load target [#{target}] using the keyword strategy using [#{input}] (#{input.class})")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Constructor
5
+ class None < Concrete
6
+ attribute :target, Types::Nothing
7
+ attribute :strategy, Types::Any
8
+ attribute :method, Types::Any
9
+
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}
14
+ #
15
+ # @param state [State]
16
+ #
17
+ # @return [State]
18
+ def call(state)
19
+ state
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Constructor < Dry::Interface
5
+ attribute :method, Symbol, default: :new
6
+ attribute :target, Types::Any.constrained(not_eql: Nothing)
7
+
8
+ # Ensures {#target} responds to {#method}
9
+ # Returns an error state unless above is true
10
+ #
11
+ # @param state [State]
12
+ #
13
+ # @return [State]
14
+ def call(state)
15
+ state.tap do
16
+ unless target.respond_to?(id)
17
+ raise ArgumentError, "Target [#{target}] does not respond to [#{id}]"
18
+ end
19
+ end
20
+ end
21
+
22
+ def id
23
+ attributes.fetch(:method)
24
+ end
25
+
26
+ def to_proc
27
+ method(:call).to_proc
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Error < StandardError
5
+ # NOP
6
+ end
7
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Failure < Result
5
+ attribute :reasons, Types::Hash
6
+
7
+ def inspect
8
+ format("Failure<[%<result>s]>", result: JSON.pretty_generate(to_h))
9
+ end
10
+
11
+ def to_hash
12
+ { failure: reasons, problems: problems }
13
+ end
14
+
15
+ def failure?(*)
16
+ true
17
+ end
18
+
19
+ def success?(*)
20
+ false
21
+ end
22
+
23
+ def fmap
24
+ self
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Iteration
5
+ class Array < Concrete
6
+ using State::Extension
7
+
8
+ attribute :value, Types::Array
9
+ attribute :state, Types::State
10
+
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._
19
+ end
20
+ alias call map
21
+
22
+ private
23
+
24
+ def init
25
+ state.set(EMPTY_ARRAY)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Iteration
5
+ class Hash < Concrete
6
+ attribute :value, Types::Hash
7
+ attribute :state, Types::State
8
+
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._
20
+ end
21
+ alias call map
22
+
23
+ private
24
+
25
+ def init
26
+ state.set(EMPTY_HASH)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Iteration
5
+ class Other < Concrete
6
+ attribute :state, Types::State
7
+ attribute :value, Types::Any
8
+
9
+ using State::Extension
10
+
11
+ # @see Base#map
12
+ def map(&block)
13
+ block[value]._
14
+ end
15
+ alias call map
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Iteration < Dry::Interface
5
+ attribute :value, Types::Value
6
+ attribute :state, Types::State
7
+
8
+ # Maps every element in {#value} to {#block}
9
+ #
10
+ # @abstract
11
+ #
12
+ # @yieldparam element [V]
13
+ # @yieldparam key [K, Integer]
14
+ # @yieldreturn [Array<V>, Hash<V, K>]
15
+ #
16
+ # @return [Array<V>, Hash<V, K>]
17
+
18
+ order :Hash, :Array, :Other
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Mapper
5
+ class And < Binary
6
+ using State::Extension
7
+
8
+ def call!(state, &error)
9
+ unless error
10
+ return call!(state, &exception)
11
+ end
12
+
13
+ state1 = left.call!(state) do |failure1|
14
+ right.call!(state) do |failure2|
15
+ return error[failure1.merge(failure2)]
16
+ end
17
+
18
+ return error[failure1]
19
+ end
20
+
21
+ state2 = right.call!(state) do |failure|
22
+ return error[failure]
23
+ end
24
+
25
+ state1.merged(state2)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Mapper
5
+ class Binary < self
6
+ attribute :left, Types::Mapper
7
+ attribute :right, Types::Mapper
8
+
9
+ def exception
10
+ ->(error) { raise error }
11
+ end
12
+
13
+ include Operation
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Mapper
5
+ class Or < Binary
6
+ using State::Extension
7
+
8
+ def call!(state, &error)
9
+ unless error
10
+ return call!(state, &exception)
11
+ end
12
+
13
+ left.call!(state) do |failure1|
14
+ return right.call!(state) do |failure2|
15
+ return error[failure1.merge(failure2)]
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Mapper
5
+ class Xor < Binary
6
+ using State::Extension
7
+
8
+ def call!(state, &error)
9
+ unless error
10
+ return call!(state, &exception)
11
+ end
12
+
13
+ state1 = left.call!(state) do |failure1|
14
+ return right.call!(state) do |failure2|
15
+ return error[failure1.merge(failure2)]
16
+ end
17
+ end
18
+
19
+ state2 = right.call!(state) do
20
+ return state1
21
+ end
22
+
23
+ error[state1.merged(state2).failure("Both left and right passed in xor")]
24
+ end
25
+ end
26
+ end
27
+ end