remap 2.0.2

Sign up to get free protection for your applications and to get access to all the features.
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