blood_contracts-core 0.3.5 → 0.4.0
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.
- 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
|