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
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ # @abstract
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
+ 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
55
+ end
56
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Nothing
5
+ # NOP
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ module Operation
5
+ using State::Extension
6
+ include State
7
+
8
+ 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)
13
+ end
14
+
15
+ if error
16
+ return error[new_state]
17
+ end
18
+
19
+ value = new_state.fetch(:value) do
20
+ return Failure.new(reasons: new_state.failure("No mapped data"), problems: new_state.problems)
21
+ end
22
+
23
+ Success.new(problems: new_state.problems, result: value)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Result < Dry::Struct
5
+ attribute :problems, Types::Hash
6
+
7
+ def has_problem?
8
+ !problems.blank?
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Each < Value
6
+ using State::Extension
7
+
8
+ attribute :rule, Types.Interface(:call)
9
+
10
+ # Iterates over {state} and passes each value to {rule}
11
+ # Restores {path} before returning state
12
+ #
13
+ #
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)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Embed < Value
6
+ using State::Extension
7
+
8
+ attribute :mapper, Types::Mapper
9
+
10
+ # Evaluates {input} against {mapper} and returns the result
11
+ #
12
+ # @param state [State]
13
+ #
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)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Map < self
6
+ using State::Extension
7
+
8
+ attribute :rule, Types.Interface(:call)
9
+ attribute :path, Path
10
+
11
+ # Maps {input} in 4 steps
12
+ #
13
+ # 1. Extract value from {input} using {path}
14
+ # 2. For each yielded value
15
+ # 2.1. Map value using {#rule}
16
+ # 2.2. Map value using {#fn}
17
+
18
+ # @example Map :a, to :b and add 5
19
+ # map = Map.new({
20
+ # path: { map: :a, to: :b },
21
+ # rule: Void.new,
22
+ # })
23
+ #
24
+ # map.adjust do |value|
25
+ # value + 5
26
+ # end
27
+ #
28
+ # map.call(a: 10) # => Success({ b: 15 })
29
+ #
30
+ # @param input [Any] Value to be mapped
31
+ # @param state [State] Current state
32
+ #
33
+ # @return [Monad::Result]
34
+ def call(state)
35
+ path.call(state) do |inner_state|
36
+ rule.call(inner_state).then do |init|
37
+ fn.reduce(init) do |inner, fn|
38
+ fn[inner]
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ # Post-processor for {#call}
45
+ #
46
+ # @example Add 5 to mapped value
47
+ # class Mapper < Remap
48
+ # define do
49
+ # map.adjust do |value|
50
+ # value + 5
51
+ # end
52
+ # end
53
+ # end
54
+ #
55
+ # Mapper.call(10) # => 15
56
+ #
57
+ # @yieldparam value [Any] Mapped value
58
+ # @yieldreturn [Monad::Result, Any]
59
+ #
60
+ # @return [void]
61
+ def adjust(&block)
62
+ add do |state|
63
+ state.execute(&block)
64
+ end
65
+ end
66
+ alias then adjust
67
+
68
+ def pending(reason = "Pending mapping")
69
+ add do |state|
70
+ state.problem(reason)
71
+ end
72
+ end
73
+
74
+ def enum(&block)
75
+ add do |state|
76
+ state.fmap do |id, &error|
77
+ Enum.call(&block).get(id, &error)
78
+ end
79
+ end
80
+ end
81
+
82
+ def if(&block)
83
+ add do |state|
84
+ state.execute(&block).fmap do |bool, &error|
85
+ bool ? state.value : error["#if returned false"]
86
+ end
87
+ end
88
+ end
89
+
90
+ def if_not(&block)
91
+ add do |state|
92
+ state.execute(&block).fmap do |bool, &error|
93
+ bool ? error["#if_not returned true"] : state.value
94
+ end
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def add(&block)
101
+ tap { fn << block }
102
+ end
103
+
104
+ def fn
105
+ @fn ||= []
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Set < self
6
+ using State::Extension
7
+
8
+ attribute :value, Types.Interface(:call)
9
+ attribute :path, Path
10
+
11
+ # Returns {value} mapped to {path} regardless of input
12
+ #
13
+ # @param state [State]
14
+ #
15
+ # @example Given an option
16
+ # class Mapper < Remap::Base
17
+ # option :name
18
+ #
19
+ # define do
20
+ # set [:person, :name], to: option(:name)
21
+ # end
22
+ # end
23
+ #
24
+ # Mapper.call(input, name: "John") # => { person: { name: "John" } }
25
+ #
26
+ # @example Given a value
27
+ # class Mapper < Remap::Base
28
+ # define do
29
+ # set [:api_key], to: value("ABC-123")
30
+ # end
31
+ # end
32
+ #
33
+ # Mapper.call(input) # => { api_key: "ABC-123" }
34
+ #
35
+ # @return [State]
36
+ def call(state)
37
+ path.call(state) do
38
+ value.call(state)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Collection
6
+ class Empty < Unit
7
+ using State::Extension
8
+
9
+ attribute? :rules, Value(EMPTY_ARRAY).default { EMPTY_ARRAY }
10
+
11
+ # Represents an empty define block, without any rules
12
+ #
13
+ # @param input [Any]
14
+ # @param state [State]
15
+ #
16
+ # @return [Monad::Failure]
17
+ def call(state)
18
+ state.problem("No rules, empty block")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Collection
6
+ class Filled < Unit
7
+ using State::Extension
8
+
9
+ attribute :rules, [Types.Interface(:call)], min_size: 1
10
+
11
+ # Represents a non-empty define block with one or more rules
12
+ # Calls every {#rules} with {input} and merges the output
13
+ #
14
+ # @param state [State]
15
+ #
16
+ # @return [State]
17
+ def call(state)
18
+ rules.map do |rule|
19
+ Thread.new(rule, state) { _1.call(_2) }
20
+ end.map(&:value).reduce do |acc, inner_state|
21
+ acc.merged(inner_state)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Collection < Dry::Interface
6
+ def to_proc
7
+ method(:call).to_proc
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Enum
6
+ include Dry::Core::Constants
7
+ include Dry::Monads[:maybe]
8
+
9
+ extend Dry::Initializer
10
+
11
+ option :mappings, default: -> { Hash.new { default } }
12
+ option :default, default: -> { None() }
13
+
14
+ alias execute instance_eval
15
+
16
+ def self.call(&block)
17
+ unless block
18
+ raise ArgumentError, "no block given"
19
+ end
20
+
21
+ new.tap { _1.execute(&block) }
22
+ end
23
+
24
+ def [](key)
25
+ mappings[key]
26
+ end
27
+
28
+ def get(key, &error)
29
+ unless error
30
+ return get(key) { raise Error, _1 }
31
+ end
32
+
33
+ mappings[key].bind { return _1 }.or do
34
+ error["Enum key [#{key}] not found among [#{mappings.keys.inspect}]"]
35
+ end
36
+ end
37
+ alias call get
38
+
39
+ # Map all keys in {keys} to {to}
40
+ #
41
+ # @return [VOID]
42
+ def from(*keys, to:)
43
+ keys.each do |key|
44
+ mappings[key] = Some(to)
45
+ end
46
+ end
47
+
48
+ # Maps {var} to {var}
49
+ #
50
+ # @return [VOID]
51
+ def value(id)
52
+ from(id, to: id)
53
+ end
54
+
55
+ # Fallback value when {#call} fails
56
+ #
57
+ # @return [Void]
58
+ def otherwise(value)
59
+ mappings.default = Some(value)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Path < Struct
6
+ using State::Extension
7
+
8
+ # @example [:a, :b, :c]
9
+ attribute :to, Types.Array(Types::Key)
10
+
11
+ # @example [:a, 0, :b, ALL]
12
+ attribute :map, Types.Array(Selector)
13
+
14
+ # Maps {state} from {map} to {block} to {to}
15
+ #
16
+ # @param state [State]
17
+ #
18
+ # @yieldparam [State]
19
+ # @yieldreturn [State<T>]
20
+ #
21
+ # @return [State<T>]
22
+ def call(state, &block)
23
+ unless block
24
+ raise ArgumentError, "block required"
25
+ end
26
+
27
+ selector(state).then(&block).fmap do |value|
28
+ to.reverse.reduce(value) do |val, key|
29
+ { key => val }
30
+ end
31
+ end._
32
+ end
33
+
34
+ private
35
+
36
+ def selector(state)
37
+ stack = map.reverse.reduce(IDENTITY) do |fn, selector|
38
+ ->(st) { selector.call(st, &fn) }
39
+ end
40
+
41
+ stack.call(state)
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule
5
+ class Void < self
6
+ using State::Extension
7
+
8
+ # Returns its input
9
+ #
10
+ # @param state [State]
11
+ #
12
+ # @example An empty rule
13
+ # class Mapper < Remap::Base
14
+ # define do
15
+ # map do
16
+ # # Empty ...
17
+ # end
18
+ # end
19
+ # end
20
+ #
21
+ # Mapper.call(input) # => input
22
+ #
23
+ # @return [State]
24
+ def call(state)
25
+ state.bind { _2.set(_1) }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/array/wrap"
4
+
5
+ module Remap
6
+ class Rule
7
+ class Wrap < self
8
+ using State::Extension
9
+
10
+ attribute :type, Value(:array)
11
+ attribute :rule, Types::Any
12
+
13
+ # Wraps the output from {#rule} in a {#type}
14
+ #
15
+ # @param state [State]
16
+ #
17
+ # @example mapps { car: "Volvo" } to { cars: ["Volvo"] }
18
+ # to :cars do
19
+ # wrap(:array) do
20
+ # map :car
21
+ # end
22
+ # end
23
+ #
24
+ # @return [State]
25
+ def call(state)
26
+ rule.call(state).fmap { Array.wrap(_1) }
27
+ end
28
+ end
29
+ end
30
+ end
data/lib/remap/rule.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Rule < Dry::Concrete
5
+ defines :requirement
6
+ requirement Types::Any
7
+ end
8
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Selector
5
+ class All < Concrete
6
+ using State::Extension
7
+
8
+ requirement Types::Enumerable
9
+
10
+ def call(state, &block)
11
+ state.bind(quantifier: "*") do |enumerable, inner_state, &error|
12
+ requirement[enumerable] do
13
+ return error["Expected an enumeration"]
14
+ end
15
+
16
+ inner_state.map(&block)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Selector
5
+ class Index < Unit
6
+ using State::Extension
7
+
8
+ attribute :index, Integer
9
+
10
+ requirement Types::Array
11
+
12
+ # Fetches {#input[value]} and passes it to {block}
13
+ #
14
+ # @param [State] state
15
+ #
16
+ # @yieldparam [State]
17
+ # @yieldreturn [State<T>]
18
+
19
+ # @return [State<T>]
20
+ def call(state, &block)
21
+ unless block
22
+ raise ArgumentError, "no block given"
23
+ end
24
+
25
+ state.bind(index: index) do |array, inner_state, &error|
26
+ requirement[array] do
27
+ return error["Expected an array"]
28
+ end
29
+
30
+ element = array.fetch(index) do
31
+ return error["No element on index at index #{index}"]
32
+ end
33
+
34
+ block[inner_state.set(element, index: index)]
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Selector
5
+ class Key < Unit
6
+ using State::Extension
7
+
8
+ attribute :key, Types::Hash.not
9
+ requirement Types::Hash.constrained(min_size: 1)
10
+
11
+ # Fetches {#input[value]} and passes it to {block}
12
+ #
13
+ # @param [State] state
14
+ #
15
+ # @yieldparam [State]
16
+ # @yieldreturn [State<T>]
17
+
18
+ # @return [State<T>]
19
+ def call(state, &block)
20
+ unless block
21
+ return call(state, &:itself)
22
+ end
23
+
24
+ state.bind(key: key) do |hash, inner_state, &error|
25
+ requirement[hash] do
26
+ return error["Expected a hash"]
27
+ end
28
+
29
+ value = hash.fetch(key) do
30
+ return error["Key [#{key}] not found"]
31
+ end
32
+
33
+ block[inner_state.set(value, key: key)]
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Remap
4
+ class Selector < Dry::Interface
5
+ defines :requirement, type: Types::Any.constrained(type: Dry::Types::Type)
6
+ requirement Types::Any
7
+
8
+ private
9
+
10
+ def requirement
11
+ self.class.requirement
12
+ end
13
+ end
14
+ end