blood_contracts-core 0.3.5 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +31 -0
- data/.travis.yml +16 -4
- data/CHANGELOG.md +14 -0
- data/README.md +363 -5
- data/Rakefile +1 -1
- data/blood_contracts-core.gemspec +18 -25
- data/examples/json_response.rb +33 -41
- data/examples/tariff_contract.rb +35 -32
- data/examples/tuple.rb +11 -12
- data/lib/blood_contracts/core/anything.rb +23 -0
- data/lib/blood_contracts/core/contract.rb +37 -23
- data/lib/blood_contracts/core/contract_failure.rb +50 -0
- data/lib/blood_contracts/core/pipe.rb +143 -77
- data/lib/blood_contracts/core/refined.rb +148 -125
- data/lib/blood_contracts/core/sum.rb +81 -49
- data/lib/blood_contracts/core/tuple.rb +151 -66
- data/lib/blood_contracts/core/tuple_contract_failure.rb +31 -0
- data/lib/blood_contracts/core.rb +16 -10
- data/spec/blood_contracts/core_spec.rb +314 -0
- data/spec/spec_helper.rb +26 -0
- metadata +36 -10
- data/lib/blood_contracts/core/version.rb +0 -5
@@ -1,145 +1,168 @@
|
|
1
|
-
module BloodContracts
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
def initialize(value, context: Hash.new { |h,k| h[k] = Hash.new }, **)
|
47
|
-
@errors = []
|
48
|
-
@context = context
|
49
|
-
@value = value
|
50
|
-
end
|
51
|
-
|
52
|
-
def match
|
53
|
-
return @match if defined? @match
|
54
|
-
return @match = (yield || self) if block_given?
|
55
|
-
return @match = (_match || self) if respond_to?(:_match)
|
56
|
-
self
|
57
|
-
end
|
58
|
-
alias :call :match
|
59
|
-
|
60
|
-
def valid?
|
61
|
-
match.errors.empty?
|
62
|
-
end
|
63
|
-
def invalid?; !valid?; end
|
64
|
-
|
65
|
-
def unpack
|
66
|
-
return @unpack if defined? @unpack
|
67
|
-
raise "This is not what you're looking for" if match.invalid?
|
68
|
-
return yield(match) if block_given?
|
69
|
-
return @unpack = _unpack(match) if respond_to?(:_unpack)
|
70
|
-
|
71
|
-
unpack_refined @value
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Base class for refinement type validations
|
3
|
+
class Refined
|
4
|
+
class << self
|
5
|
+
# Compose types in a Sum check
|
6
|
+
# Sum passes data from type to type in parallel, only one type
|
7
|
+
# have to match
|
8
|
+
#
|
9
|
+
# @return [BC::Sum]
|
10
|
+
#
|
11
|
+
def or_a(other_type)
|
12
|
+
BC::Sum.new(self, other_type)
|
13
|
+
end
|
14
|
+
alias or_an or_a
|
15
|
+
alias | or_a
|
16
|
+
|
17
|
+
# Compose types in a Pipe check
|
18
|
+
# Pipe passes data from type to type sequentially
|
19
|
+
#
|
20
|
+
# @return [BC::Pipe]
|
21
|
+
#
|
22
|
+
def and_then(other_type)
|
23
|
+
BC::Pipe.new(self, other_type)
|
24
|
+
end
|
25
|
+
alias > and_then
|
26
|
+
|
27
|
+
# Validate data over refinement type conditions
|
28
|
+
# Result is ALWAYS a Refined, but in cases when validation failed,
|
29
|
+
# we return ContractFailure ancestor or ContractFailure itself
|
30
|
+
# (which is Refined anyway)
|
31
|
+
#
|
32
|
+
# @return [Refined]
|
33
|
+
#
|
34
|
+
def match(*args, **kwargs, &block)
|
35
|
+
instance = new(*args, **kwargs)
|
36
|
+
match = instance.match(&block) || instance
|
37
|
+
instance.instance_variable_set(:@match, match)
|
38
|
+
match
|
39
|
+
end
|
40
|
+
alias call match
|
41
|
+
|
42
|
+
# Override of case equality operator, to handle Tuple correctly
|
43
|
+
def ===(object)
|
44
|
+
return object.to_ary.any?(self) if object.is_a?(Tuple)
|
45
|
+
super
|
72
46
|
end
|
73
47
|
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
protected
|
83
|
-
|
84
|
-
def refined?(object)
|
85
|
-
object.class < BloodContracts::Core::Refined
|
48
|
+
# Accessor to define alternative to ContractFailure for #failure
|
49
|
+
# method to use
|
50
|
+
#
|
51
|
+
# @return [ContractFailure]
|
52
|
+
#
|
53
|
+
attr_accessor :failure_klass
|
54
|
+
def inherited(new_klass)
|
55
|
+
new_klass.failure_klass ||= ContractFailure
|
86
56
|
end
|
57
|
+
end
|
87
58
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
59
|
+
# Matching context, contains extra debugging and output data
|
60
|
+
#
|
61
|
+
# @return [Hash<Symbol, Object>]
|
62
|
+
#
|
63
|
+
attr_accessor :context
|
64
|
+
|
65
|
+
# List of errors per type
|
66
|
+
#
|
67
|
+
# @return [Array<Hash<Refined, String>>]
|
68
|
+
#
|
69
|
+
attr_reader :errors
|
70
|
+
|
71
|
+
# Refinement type constructor
|
72
|
+
#
|
73
|
+
# @param [Object] value that Refined holds and should match
|
74
|
+
# @option [Hash<Symbol, Object>] context to share between types
|
75
|
+
#
|
76
|
+
def initialize(value, context: Hash.new { |h, k| h[k] = {} }, **)
|
77
|
+
@errors = []
|
78
|
+
@context = context
|
79
|
+
@value = value
|
80
|
+
end
|
92
81
|
|
93
|
-
|
94
|
-
|
95
|
-
|
82
|
+
# The type which is the result of data matching process
|
83
|
+
#
|
84
|
+
# @return [BC::Refined]
|
85
|
+
#
|
86
|
+
protected def match
|
87
|
+
fail NotImplementedError
|
88
|
+
end
|
89
|
+
alias call match
|
96
90
|
|
97
|
-
|
98
|
-
|
99
|
-
|
91
|
+
# Transform the value before unpacking
|
92
|
+
protected def mapped
|
93
|
+
value
|
94
|
+
end
|
100
95
|
|
101
|
-
|
102
|
-
|
103
|
-
|
96
|
+
# Checks whether the data matches the expectations or not
|
97
|
+
#
|
98
|
+
# @return [Boolean]
|
99
|
+
#
|
100
|
+
def valid?
|
101
|
+
@match.errors.empty?
|
104
102
|
end
|
105
103
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
104
|
+
# Checks whether the data matches the expectations or not
|
105
|
+
# (just negation of #valid?)
|
106
|
+
#
|
107
|
+
# @return [Boolean]
|
108
|
+
#
|
109
|
+
def invalid?
|
110
|
+
!valid?
|
111
|
+
end
|
112
112
|
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
# Unpack the original value from the refinement type
|
114
|
+
#
|
115
|
+
# @return [Object]
|
116
|
+
#
|
117
|
+
def unpack
|
118
|
+
@unpack ||= mapped
|
119
|
+
end
|
116
120
|
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
+
protected
|
122
|
+
|
123
|
+
# Helper to build a ContractFailure with shared context
|
124
|
+
#
|
125
|
+
# @return [ContractFailure]
|
126
|
+
#
|
127
|
+
def failure(error = nil, errors: @errors, **kwargs)
|
128
|
+
error ||= kwargs unless kwargs.empty?
|
129
|
+
errors << error if error
|
130
|
+
self.class.failure_klass.new(
|
131
|
+
{ self.class => errors }, context: @context
|
132
|
+
)
|
133
|
+
end
|
121
134
|
|
122
|
-
|
123
|
-
|
124
|
-
|
135
|
+
# Helper to turn value into raw data
|
136
|
+
#
|
137
|
+
# @return [Object]
|
138
|
+
def value
|
139
|
+
unpack_refined(@value)
|
140
|
+
end
|
125
141
|
|
126
|
-
|
127
|
-
|
128
|
-
end
|
142
|
+
def refined?(object)
|
143
|
+
object.class < BloodContracts::Core::Refined
|
129
144
|
end
|
130
145
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
146
|
+
# FIXME: do we need it?
|
147
|
+
def share_context_with(match)
|
148
|
+
match.context = @context.merge!(match.context)
|
149
|
+
yield(match.context)
|
150
|
+
end
|
135
151
|
|
136
|
-
|
137
|
-
|
138
|
-
|
152
|
+
# Turn data into refinement type if it is not already
|
153
|
+
#
|
154
|
+
# @return [Object]
|
155
|
+
#
|
156
|
+
def refine_value(value)
|
157
|
+
refined?(value) ? value.match : Anything.new(value)
|
158
|
+
end
|
139
159
|
|
140
|
-
|
141
|
-
|
142
|
-
|
160
|
+
# Turn value into raw data if it is refined
|
161
|
+
#
|
162
|
+
# @return [Object]
|
163
|
+
#
|
164
|
+
def unpack_refined(value)
|
165
|
+
refined?(value) ? value.unpack : value
|
143
166
|
end
|
144
167
|
end
|
145
168
|
end
|
@@ -1,67 +1,99 @@
|
|
1
|
-
require
|
1
|
+
require "set"
|
2
2
|
|
3
|
-
module BloodContracts
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
3
|
+
module BloodContracts::Core
|
4
|
+
# Meta refinement type, represents sum of several refinement types
|
5
|
+
class Sum < Refined
|
6
|
+
class << self
|
7
|
+
# Represents list of types in the sum
|
8
|
+
#
|
9
|
+
# @return [Array<Refined>]
|
10
|
+
#
|
11
|
+
attr_reader :sum_of
|
8
12
|
|
9
|
-
|
10
|
-
|
13
|
+
# Metaprogramming around constructor
|
14
|
+
# Turns input into Sum meta-class
|
15
|
+
#
|
16
|
+
# @param (see #initialze)
|
17
|
+
#
|
18
|
+
# rubocop:disable Style/SingleLineMethods
|
19
|
+
def new(*args)
|
20
|
+
return super(*args) if @finalized
|
11
21
|
|
12
|
-
|
13
|
-
|
14
|
-
sum = Class.new(Sum) { def inspect; super; end }
|
15
|
-
sum.instance_variable_set(:@sum_of, ::Set.new(new_sum.compact))
|
16
|
-
sum.instance_variable_set(:@finalized, true)
|
17
|
-
sum
|
22
|
+
new_sum = args.reduce([]) do |acc, type|
|
23
|
+
type.respond_to?(:sum_of) ? acc + type.sum_of.to_a : acc << type
|
18
24
|
end
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
new_sum += other_type.sum_of.to_a
|
25
|
-
else
|
26
|
-
new_sum << other_type
|
27
|
-
end
|
28
|
-
sum.instance_variable_set(:@sum_of, ::Set.new(new_sum.compact))
|
29
|
-
sum.instance_variable_set(:@finalized, true)
|
30
|
-
sum
|
31
|
-
end
|
32
|
-
alias :or_an :or_a
|
33
|
-
alias :| :or_a
|
26
|
+
sum = Class.new(Sum) { def inspect; super; end }
|
27
|
+
finalize!(sum, new_sum)
|
28
|
+
sum
|
29
|
+
end
|
34
30
|
|
35
|
-
|
36
|
-
|
37
|
-
|
31
|
+
# Compose types in a Sum check
|
32
|
+
# Sum passes data from type to type in parallel, only one type
|
33
|
+
# have to match
|
34
|
+
#
|
35
|
+
# @return [BC::Sum]
|
36
|
+
#
|
37
|
+
def or_a(other_type)
|
38
|
+
sum = Class.new(Sum) { def inspect; super; end }
|
39
|
+
new_sum = sum_of.to_a
|
40
|
+
if other_type.respond_to?(:sum_of)
|
41
|
+
new_sum += other_type.sum_of.to_a
|
42
|
+
else
|
43
|
+
new_sum << other_type
|
38
44
|
end
|
45
|
+
finalize!(sum, new_sum)
|
46
|
+
sum
|
39
47
|
end
|
48
|
+
# rubocop:enable Style/SingleLineMethods
|
49
|
+
alias or_an or_a
|
50
|
+
alias | or_a
|
40
51
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
52
|
+
# @private
|
53
|
+
private def finalize!(new_class, new_sum)
|
54
|
+
new_class.instance_variable_set(:@sum_of, ::Set.new(new_sum.compact))
|
55
|
+
new_class.instance_variable_set(:@finalized, true)
|
56
|
+
end
|
46
57
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
end
|
58
|
+
# Returns text representation of Sum meta-class
|
59
|
+
#
|
60
|
+
# @return [String]
|
61
|
+
#
|
62
|
+
def inspect
|
63
|
+
return super if name
|
64
|
+
"Sum(#{sum_of.map(&:inspect).join(',')})"
|
55
65
|
end
|
66
|
+
end
|
56
67
|
|
57
|
-
|
58
|
-
|
68
|
+
# The type which is the result of data matching process
|
69
|
+
# For Tuple it verifies that all the attributes data are valid types
|
70
|
+
#
|
71
|
+
# @return [BC::Refined]
|
72
|
+
#
|
73
|
+
def match
|
74
|
+
or_matches = self.class.sum_of.map do |type|
|
75
|
+
type.match(@value, context: @context)
|
59
76
|
end
|
60
77
|
|
61
|
-
|
62
|
-
|
78
|
+
if (match = or_matches.find(&:valid?))
|
79
|
+
match
|
80
|
+
else
|
81
|
+
failure(:no_matches)
|
63
82
|
end
|
64
83
|
end
|
65
|
-
|
84
|
+
|
85
|
+
# List of errors per type during the matching
|
86
|
+
#
|
87
|
+
# @return [Array<Hash<Refined, String>>]
|
88
|
+
#
|
89
|
+
def errors
|
90
|
+
@context[:errors]
|
91
|
+
end
|
92
|
+
|
93
|
+
# @private
|
94
|
+
private def inspect
|
95
|
+
"#<sum #{self.class.name} is #{self.class.sum_of.to_a.join(' or ')}"\
|
96
|
+
" (value=#{@value})>"
|
97
|
+
end
|
66
98
|
end
|
67
99
|
end
|
@@ -1,82 +1,167 @@
|
|
1
|
-
module BloodContracts
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
if args.last.is_a?(Hash)
|
10
|
-
names = args.pop.delete(:names)
|
11
|
-
end
|
12
|
-
|
13
|
-
raise ArgumentError unless args.all?(Class)
|
14
|
-
tuple = Class.new(Tuple) { def inspect; super; end }
|
15
|
-
tuple.instance_variable_set(:@attributes, args)
|
16
|
-
tuple.instance_variable_set(:@names, names.to_a)
|
17
|
-
tuple.instance_variable_set(:@finalized, true)
|
18
|
-
tuple
|
19
|
-
end
|
1
|
+
module BloodContracts::Core
|
2
|
+
# Meta refinement type, represents product of several refinement types
|
3
|
+
class Tuple < Refined
|
4
|
+
class << self
|
5
|
+
# List of types in the Tuple
|
6
|
+
#
|
7
|
+
# @return [Array<Refined>]
|
8
|
+
attr_reader :attributes
|
20
9
|
|
21
|
-
|
10
|
+
# Names of attributes
|
11
|
+
#
|
12
|
+
# @return [Array<Symbol>]
|
13
|
+
#
|
14
|
+
attr_reader :names
|
22
15
|
|
23
|
-
|
24
|
-
|
16
|
+
# Metaprogramming around constructor
|
17
|
+
# Turns input into Tuple meta-class
|
18
|
+
#
|
19
|
+
# @param (see #initialze)
|
20
|
+
#
|
21
|
+
# rubocop:disable Style/SingleLineMethods
|
22
|
+
def new(*args, **kwargs, &block)
|
23
|
+
return super(*args, **kwargs) if @finalized
|
24
|
+
names = args.pop.delete(:names) if args.last.is_a?(Hash)
|
25
25
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
raise ArgumentError unless args.all? { |type| type < Refined }
|
27
|
+
tuple = Class.new(Tuple) { def inspect; super; end }
|
28
|
+
tuple.instance_variable_set(:@attributes, args)
|
29
|
+
tuple.instance_variable_set(:@names, names.to_a)
|
30
|
+
tuple.instance_variable_set(:@finalized, true)
|
31
|
+
tuple.class_eval(&block) if block_given?
|
32
|
+
tuple
|
32
33
|
end
|
34
|
+
# rubocop:enable Style/SingleLineMethods
|
33
35
|
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
36
|
+
# Helper which registers attribute in the Tuple, also defines a reader
|
37
|
+
def attribute(name, type)
|
38
|
+
raise ArgumentError unless type < Refined
|
39
|
+
@attributes << type
|
40
|
+
@names << name
|
41
|
+
define_method(name) do
|
42
|
+
match.context.dig(:attributes, name)
|
41
43
|
end
|
42
44
|
end
|
43
45
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
def
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
]
|
57
|
-
end
|
58
|
-
alias :to_hash :unpack_h
|
59
|
-
alias :to_h :unpack_h
|
60
|
-
|
61
|
-
private def values_by_names
|
62
|
-
if self.class.names.empty?
|
63
|
-
self.values
|
64
|
-
else
|
65
|
-
self.class.names.zip(values).map { |k, v| [k, v].join('=') }
|
66
|
-
end
|
46
|
+
# Accessor to define alternative to ContractFailure for #failure
|
47
|
+
# method to use
|
48
|
+
#
|
49
|
+
# @return [ContractFailure]
|
50
|
+
#
|
51
|
+
attr_accessor :failure_klass
|
52
|
+
def inherited(new_klass)
|
53
|
+
new_klass.instance_variable_set(:@attributes, [])
|
54
|
+
new_klass.instance_variable_set(:@names, [])
|
55
|
+
new_klass.instance_variable_set(:@finalized, true)
|
56
|
+
new_klass.failure_klass ||= TupleContractFailure
|
57
|
+
super
|
67
58
|
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
# List of values in Tuple
|
63
|
+
#
|
64
|
+
# @return [Array<Object>]
|
65
|
+
#
|
66
|
+
attr_reader :values
|
68
67
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
68
|
+
# Tuple constructor, builds Tuple from list of data values
|
69
|
+
#
|
70
|
+
# @param [Array<Object>] *values that we'll keep inside the Tuple
|
71
|
+
# @option [Hash<Symbol, Object>] context to share between types
|
72
|
+
#
|
73
|
+
def initialize(*values, context: Hash.new { |h, k| h[k] = {} }, **)
|
74
|
+
@context = context
|
75
|
+
@context[:attributes] ||= {}
|
76
|
+
|
77
|
+
additional_context = values.last if values.last.is_a?(Hash)
|
78
|
+
additional_context ||= {}
|
79
|
+
|
80
|
+
@values = parse_values_from_context(context.merge(additional_context))
|
81
|
+
@values ||= values
|
82
|
+
|
83
|
+
@errors = []
|
84
|
+
end
|
85
|
+
|
86
|
+
# The type which is the result of data matching process
|
87
|
+
# For Tuple it verifies that all the attributes data are valid types
|
88
|
+
#
|
89
|
+
# @return [BC::Refined]
|
90
|
+
#
|
91
|
+
def match
|
92
|
+
@matches = attributes_enumerator.map do |(type, value), index|
|
93
|
+
attribute_name = self.class.names[index]
|
94
|
+
attributes.store(attribute_name, type.match(value, context: @context))
|
75
95
|
end
|
96
|
+
return if @matches.find(&:invalid?).nil?
|
97
|
+
failure(:invalid_tuple)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Turns match into array of unpacked values
|
101
|
+
#
|
102
|
+
# @return [Array<Object>]
|
103
|
+
#
|
104
|
+
def mapped
|
105
|
+
@matches.map(&:unpack)
|
106
|
+
end
|
107
|
+
|
108
|
+
# (see #mapped)
|
109
|
+
alias to_ary unpack
|
110
|
+
|
111
|
+
# (see #mapped)
|
112
|
+
alias to_a unpack
|
113
|
+
|
114
|
+
# Unpacked value in form of a hash per attribute
|
115
|
+
#
|
116
|
+
# @return [Hash<String, ContractFailure>]
|
117
|
+
#
|
118
|
+
def unpack_h
|
119
|
+
@unpack_h ||= attributes.transform_values(&:unpack)
|
120
|
+
end
|
121
|
+
alias to_hash unpack_h
|
122
|
+
alias to_h unpack_h
|
123
|
+
alias unpack_attributes unpack_h
|
124
|
+
|
125
|
+
# Hash of attributes (name & type pairs)
|
126
|
+
#
|
127
|
+
# @return [Hash<String, Refined>]
|
128
|
+
#
|
129
|
+
def attributes
|
130
|
+
@context[:attributes]
|
131
|
+
end
|
76
132
|
|
77
|
-
|
78
|
-
|
133
|
+
# Subset of attributes which are invalid
|
134
|
+
#
|
135
|
+
# @return [Hash<String, ContractFailure>]
|
136
|
+
#
|
137
|
+
def attribute_errors
|
138
|
+
{}
|
139
|
+
end
|
140
|
+
|
141
|
+
# @private
|
142
|
+
private def parse_values_from_context(context)
|
143
|
+
return if context.empty?
|
144
|
+
return unless (self.class.names - context.keys).empty?
|
145
|
+
context.values_at(*self.class.names)
|
146
|
+
end
|
147
|
+
|
148
|
+
# @private
|
149
|
+
private def attributes_enumerator
|
150
|
+
self.class.attributes.zip(@values).each.with_index
|
151
|
+
end
|
152
|
+
|
153
|
+
# @private
|
154
|
+
private def values_by_names
|
155
|
+
if self.class.names.empty?
|
156
|
+
values
|
157
|
+
else
|
158
|
+
self.class.names.zip(values).map { |k, v| [k, v].join("=") }
|
79
159
|
end
|
80
160
|
end
|
161
|
+
|
162
|
+
# @private
|
163
|
+
private def inspect
|
164
|
+
"#<tuple #{self.class.name} of (#{values_by_names.join(', ')})>"
|
165
|
+
end
|
81
166
|
end
|
82
167
|
end
|