remap 2.0.3 → 2.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/remap/base.rb +229 -75
- data/lib/remap/compiler.rb +127 -37
- data/lib/remap/constructor/argument.rb +20 -6
- data/lib/remap/constructor/keyword.rb +20 -4
- data/lib/remap/constructor/none.rb +3 -4
- data/lib/remap/constructor.rb +12 -5
- data/lib/remap/contract.rb +27 -0
- data/lib/remap/extensions/enumerable.rb +48 -0
- data/lib/remap/extensions/hash.rb +13 -0
- data/lib/remap/extensions/object.rb +37 -0
- data/lib/remap/failure.rb +25 -15
- data/lib/remap/iteration/array.rb +20 -11
- data/lib/remap/iteration/hash.rb +21 -13
- data/lib/remap/iteration/other.rb +7 -7
- data/lib/remap/iteration.rb +8 -2
- data/lib/remap/mapper/and.rb +29 -7
- data/lib/remap/mapper/binary.rb +3 -6
- data/lib/remap/mapper/or.rb +29 -6
- data/lib/remap/mapper/support/operations.rb +40 -0
- data/lib/remap/mapper/xor.rb +29 -7
- data/lib/remap/mapper.rb +1 -48
- data/lib/remap/notice/traced.rb +19 -0
- data/lib/remap/notice/untraced.rb +11 -0
- data/lib/remap/notice.rb +34 -0
- data/lib/remap/operation.rb +26 -13
- data/lib/remap/path/input.rb +37 -0
- data/lib/remap/path/output.rb +26 -0
- data/lib/remap/path.rb +22 -0
- data/lib/remap/path_error.rb +13 -0
- data/lib/remap/proxy.rb +18 -0
- data/lib/remap/rule/each.rb +25 -24
- data/lib/remap/rule/embed.rb +33 -28
- data/lib/remap/rule/map/optional.rb +42 -0
- data/lib/remap/rule/map/required.rb +35 -0
- data/lib/remap/rule/map.rb +176 -55
- data/lib/remap/rule/set.rb +23 -33
- data/lib/remap/rule/support/collection/empty.rb +7 -7
- data/lib/remap/rule/support/collection/filled.rb +21 -8
- data/lib/remap/rule/support/collection.rb +11 -3
- data/lib/remap/rule/support/enum.rb +44 -21
- data/lib/remap/rule/void.rb +17 -18
- data/lib/remap/rule/wrap.rb +25 -17
- data/lib/remap/rule.rb +8 -1
- data/lib/remap/selector/all.rb +29 -7
- data/lib/remap/selector/index.rb +24 -16
- data/lib/remap/selector/key.rb +31 -16
- data/lib/remap/selector.rb +17 -0
- data/lib/remap/state/extension.rb +182 -208
- data/lib/remap/state/schema.rb +1 -1
- data/lib/remap/state.rb +30 -4
- data/lib/remap/static/fixed.rb +14 -3
- data/lib/remap/static/option.rb +21 -6
- data/lib/remap/static.rb +13 -0
- data/lib/remap/struct.rb +1 -0
- data/lib/remap/types.rb +13 -28
- data/lib/remap.rb +15 -19
- metadata +91 -89
- data/lib/remap/result.rb +0 -11
- data/lib/remap/rule/support/path.rb +0 -45
- data/lib/remap/state/types.rb +0 -11
- data/lib/remap/success.rb +0 -29
- data/lib/remap/version.rb +0 -5
data/lib/remap/mapper.rb
CHANGED
@@ -3,54 +3,7 @@
|
|
3
3
|
module Remap
|
4
4
|
# @abstract
|
5
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
6
|
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
|
7
|
+
include Operations
|
55
8
|
end
|
56
9
|
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
class Notice
|
5
|
+
class Traced < Concrete
|
6
|
+
attribute? :backtrace, Types::Backtrace, default: EMPTY_ARRAY
|
7
|
+
|
8
|
+
def traced(backtrace)
|
9
|
+
Notice.call(**attributes, backtrace: backtrace)
|
10
|
+
end
|
11
|
+
|
12
|
+
def exception
|
13
|
+
return super if backtrace.blank?
|
14
|
+
|
15
|
+
super.tap { _1.set_backtrace(backtrace.map(&:to_s)) }
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/remap/notice.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
using Extensions::Hash
|
5
|
+
|
6
|
+
class Notice < Dry::Interface
|
7
|
+
attribute? :value, Types::Any
|
8
|
+
attribute :reason, String
|
9
|
+
attribute :path, Array
|
10
|
+
|
11
|
+
class Error < Remap::Error
|
12
|
+
extend Dry::Initializer
|
13
|
+
|
14
|
+
param :notice, type: Notice
|
15
|
+
end
|
16
|
+
|
17
|
+
def inspect
|
18
|
+
"#<%s %s>" % [self.class, to_hash.formated]
|
19
|
+
end
|
20
|
+
alias to_s inspect
|
21
|
+
|
22
|
+
# Hash representation of the notice
|
23
|
+
#
|
24
|
+
# @return [Hash]
|
25
|
+
def to_hash
|
26
|
+
super.except(:backtrace).compact_blank
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Error]
|
30
|
+
def exception
|
31
|
+
Error.new(self)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/remap/operation.rb
CHANGED
@@ -1,26 +1,39 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Remap
|
4
|
-
|
5
|
-
using State::Extension
|
6
|
-
include State
|
4
|
+
using State::Extension
|
7
5
|
|
6
|
+
# Class interface for {Remap::Base} and instance interface for {Mapper}
|
7
|
+
module Operation
|
8
|
+
# Public interface for mappers
|
9
|
+
#
|
10
|
+
# @param input [Any] Data to be mapped
|
11
|
+
# @param options [Hash] Mapper arguments
|
12
|
+
#
|
13
|
+
# @yield [Failure] if mapper fails
|
14
|
+
#
|
15
|
+
# @return [Success] if mapper succeeds
|
8
16
|
def call(input, **options, &error)
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
17
|
+
unless error
|
18
|
+
return call(input, **options) do |failure|
|
19
|
+
raise failure.exception
|
20
|
+
end
|
13
21
|
end
|
14
22
|
|
15
|
-
|
16
|
-
|
23
|
+
other = State.call(input, options: options, mapper: self).then do |state|
|
24
|
+
call!(state) do |failure|
|
25
|
+
return error[failure]
|
26
|
+
end
|
17
27
|
end
|
18
28
|
|
19
|
-
|
20
|
-
|
29
|
+
case other
|
30
|
+
in { value: }
|
31
|
+
value
|
32
|
+
in { notices: [] }
|
33
|
+
error[other.failure("No return value")]
|
34
|
+
in { notices: }
|
35
|
+
error[Failure.call(failures: notices)]
|
21
36
|
end
|
22
|
-
|
23
|
-
Success.new(problems: new_state.problems, result: value)
|
24
37
|
end
|
25
38
|
end
|
26
39
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
class Path
|
5
|
+
using State::Extension
|
6
|
+
|
7
|
+
# Returns the value at a given path
|
8
|
+
#
|
9
|
+
# @example Select "A" from { a: { b: { c: ["A"] } } }
|
10
|
+
# state = Remap::State.call({ a: { b: { c: ["A"] } } })
|
11
|
+
# first = Remap::Selector::Index.new(index: 0)
|
12
|
+
# path = Remap::Path::Input.new([:a, :b, :c, first])
|
13
|
+
#
|
14
|
+
# path.call(state) do |state|
|
15
|
+
# state.fetch(:value)
|
16
|
+
# end
|
17
|
+
class Input < Unit
|
18
|
+
# @return [Array<Selector>]
|
19
|
+
attribute :segments, [Selector]
|
20
|
+
|
21
|
+
# Selects the value at the path {#segments}
|
22
|
+
#
|
23
|
+
# @param state [State]
|
24
|
+
#
|
25
|
+
# @return [State]
|
26
|
+
def call(state, &iterator)
|
27
|
+
unless block_given?
|
28
|
+
raise ArgumentError, "Input path requires an iterator block"
|
29
|
+
end
|
30
|
+
|
31
|
+
segments.reverse.reduce(iterator) do |inner_iterator, selector|
|
32
|
+
-> inner_state { selector.call(inner_state, &inner_iterator) }
|
33
|
+
end.call(state)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
class Path
|
5
|
+
using Extensions::Enumerable
|
6
|
+
using State::Extension
|
7
|
+
|
8
|
+
# Sets the value to a given path
|
9
|
+
#
|
10
|
+
# @example Maps "A" to { a: { b: { c: "A" } } }
|
11
|
+
# state = Remap::State.call("A")
|
12
|
+
# result = Remap::Path::Output.new([:a, :b, :c]).call(state)
|
13
|
+
#
|
14
|
+
# result.fetch(:value) # => { a: { b: { c: "A" } } }
|
15
|
+
class Output < Unit
|
16
|
+
attribute :segments, [Types::Key]
|
17
|
+
|
18
|
+
# @return [State]
|
19
|
+
def call(state)
|
20
|
+
state.fmap do |value|
|
21
|
+
segments.hide(value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/remap/path.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
# Represents a sequence of keys and selects or maps a value given a path
|
5
|
+
class Path < Dry::Interface
|
6
|
+
attribute :segments, Types::Array
|
7
|
+
|
8
|
+
delegate :>>, to: :to_proc
|
9
|
+
|
10
|
+
# @return [State]
|
11
|
+
#
|
12
|
+
# @abstract
|
13
|
+
def call(state)
|
14
|
+
raise NotImplementedError, "#{self.class}#call not implemented"
|
15
|
+
end
|
16
|
+
|
17
|
+
# @return [Proc]
|
18
|
+
def to_proc
|
19
|
+
method(:call).to_proc
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
data/lib/remap/proxy.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
class Proxy < ActiveSupport::ProxyObject
|
5
|
+
def self.const_missing(name)
|
6
|
+
::Object.const_get(name)
|
7
|
+
end
|
8
|
+
|
9
|
+
include Dry::Core::Constants
|
10
|
+
extend Dry::Initializer
|
11
|
+
|
12
|
+
# See Object#tap
|
13
|
+
def tap(&block)
|
14
|
+
block[self]
|
15
|
+
self
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
data/lib/remap/rule/each.rb
CHANGED
@@ -2,34 +2,35 @@
|
|
2
2
|
|
3
3
|
module Remap
|
4
4
|
class Rule
|
5
|
-
|
6
|
-
using State::Extension
|
5
|
+
using State::Extension
|
7
6
|
|
8
|
-
|
7
|
+
# Iterates over a rule, even if the rule is not a collection
|
8
|
+
#
|
9
|
+
# @example Upcase each value in an array
|
10
|
+
# state = Remap::State.call(["John", "Jane"])
|
11
|
+
# upcase = Remap::Rule::Map.call({}).then(&:upcase)
|
12
|
+
# each = Remap::Rule::Each.call(rule: upcase)
|
13
|
+
# error = -> failure { raise failure.exception }
|
14
|
+
# each.call(state, &error).fetch(:value) # => ["JOHN", "JANE"]
|
15
|
+
class Each < Unit
|
16
|
+
# @return [Rule]
|
17
|
+
attribute :rule, Types::Rule
|
9
18
|
|
10
|
-
# Iterates over
|
11
|
-
# Restores
|
19
|
+
# Iterates over state and passes each value to rule
|
20
|
+
# Restores element, key & index before returning state
|
12
21
|
#
|
22
|
+
# @param state [State<Enumerable>]
|
13
23
|
#
|
14
|
-
#
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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)
|
24
|
+
# @return [State<Enumerable>]
|
25
|
+
def call(state, &error)
|
26
|
+
unless error
|
27
|
+
raise ArgumentError, "Each#call(state, &error) requires a block"
|
28
|
+
end
|
29
|
+
|
30
|
+
state.map do |inner_state|
|
31
|
+
rule.call(inner_state) do |failure|
|
32
|
+
return error[failure]
|
33
|
+
end
|
33
34
|
end
|
34
35
|
end
|
35
36
|
end
|
data/lib/remap/rule/embed.rb
CHANGED
@@ -2,40 +2,45 @@
|
|
2
2
|
|
3
3
|
module Remap
|
4
4
|
class Rule
|
5
|
-
|
6
|
-
using State::Extension
|
5
|
+
using State::Extension
|
7
6
|
|
7
|
+
# Embed mappers into each other
|
8
|
+
#
|
9
|
+
# @example Embed Mapper A into B
|
10
|
+
# class Car < Remap::Base
|
11
|
+
# define do
|
12
|
+
# map :name, to: :model
|
13
|
+
# end
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# class Person < Remap::Base
|
17
|
+
# define do
|
18
|
+
# to :person do
|
19
|
+
# to :car do
|
20
|
+
# embed Car
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# Person.call({name: "Volvo"}) # => { person: { car: { model: "Volvo" } } }
|
27
|
+
class Embed < Unit
|
28
|
+
# @return [#call!]
|
8
29
|
attribute :mapper, Types::Mapper
|
9
30
|
|
10
|
-
# Evaluates
|
31
|
+
# Evaluates input against mapper and returns the result
|
11
32
|
#
|
12
|
-
# @param state [State]
|
33
|
+
# @param state [State<T>]
|
13
34
|
#
|
14
|
-
# @
|
15
|
-
|
16
|
-
|
17
|
-
|
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)
|
35
|
+
# @return [State<U>]
|
36
|
+
def call(state, &error)
|
37
|
+
unless error
|
38
|
+
raise ArgumentError, "A block is required to evaluate the embed"
|
38
39
|
end
|
40
|
+
|
41
|
+
mapper.call!(state.set(mapper: mapper)) do |failure|
|
42
|
+
return error[failure]
|
43
|
+
end.except(:mapper, :scope)
|
39
44
|
end
|
40
45
|
end
|
41
46
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
class Rule
|
5
|
+
class Map
|
6
|
+
using State::Extension
|
7
|
+
|
8
|
+
class Optional < Concrete
|
9
|
+
# Represents an optional mapping rule
|
10
|
+
# When the mapping fails, the value is ignored
|
11
|
+
#
|
12
|
+
# @param state [State]
|
13
|
+
#
|
14
|
+
# @return [State]
|
15
|
+
def call(state, &error)
|
16
|
+
unless error
|
17
|
+
raise ArgumentError, "map.call(state, &error) requires a block"
|
18
|
+
end
|
19
|
+
|
20
|
+
fatal(state) do
|
21
|
+
return ignore(state) do
|
22
|
+
return notice(state) do
|
23
|
+
return super
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Catches :ignore exceptions and re-package them as a state
|
32
|
+
#
|
33
|
+
# @param state [State]
|
34
|
+
#
|
35
|
+
# @return [State]
|
36
|
+
def ignore(state, &block)
|
37
|
+
state.set(notice: catch(:ignore, &block).traced(backtrace)).except(:value)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Remap
|
4
|
+
class Rule
|
5
|
+
class Map
|
6
|
+
using State::Extension
|
7
|
+
|
8
|
+
class Required < Concrete
|
9
|
+
attribute :backtrace, Types::Backtrace
|
10
|
+
|
11
|
+
# Represents a required mapping rule
|
12
|
+
# When it fails, the entire mapping is marked as failed
|
13
|
+
#
|
14
|
+
# @param state [State]
|
15
|
+
#
|
16
|
+
# @return [State]
|
17
|
+
def call(state, &error)
|
18
|
+
unless block_given?
|
19
|
+
raise ArgumentError, "Required.call(state, &error) requires a block"
|
20
|
+
end
|
21
|
+
|
22
|
+
notice = catch :ignore do
|
23
|
+
return fatal(state) do
|
24
|
+
return notice(state) do
|
25
|
+
return super
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
error[state.failure(notice)]
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|