contracted_value 0.1.0.alpha.1
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 +7 -0
- data/.codeclimate.yml +17 -0
- data/.editorconfig +24 -0
- data/.gitignore +35 -0
- data/.rubocop.yml +41 -0
- data/.travis.yml +25 -0
- data/Appraisals +9 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +346 -0
- data/Rakefile +16 -0
- data/contracted_value.gemspec +55 -0
- data/gemfiles/contracts_15_0.gemfile +7 -0
- data/gemfiles/contracts_16_0.gemfile +7 -0
- data/lib/contracted_value/core.rb +322 -0
- data/lib/contracted_value/version.rb +5 -0
- data/lib/contracted_value.rb +4 -0
- data/spec/contracted_value/value_spec.rb +680 -0
- data/spec/contracted_value_spec.rb +11 -0
- data/spec/spec_helper.rb +15 -0
- metadata +254 -0
@@ -0,0 +1,322 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "contracts"
|
4
|
+
require "ice_nine"
|
5
|
+
|
6
|
+
module ContractedValue
|
7
|
+
module RefrigerationMode
|
8
|
+
module Enum
|
9
|
+
DEEP = :deep
|
10
|
+
SHALLOW = :shallow
|
11
|
+
NONE = :none
|
12
|
+
|
13
|
+
def self.all
|
14
|
+
[
|
15
|
+
DEEP,
|
16
|
+
SHALLOW,
|
17
|
+
NONE,
|
18
|
+
].freeze
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
module Errors
|
24
|
+
class DuplicateAttributeDeclaration < ArgumentError
|
25
|
+
def initialize(key)
|
26
|
+
super("Attribute :#{key} has already been declared")
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class InvalidRefrigerationMode < ArgumentError
|
31
|
+
def initialize(val)
|
32
|
+
valid_values = RefrigerationMode::Enum.all
|
33
|
+
|
34
|
+
super(<<~MSG)
|
35
|
+
option `refrigeration_mode` received <#{val.inspect}> but expected:
|
36
|
+
#{valid_values.to_a.map(&:inspect).join(", ")}
|
37
|
+
MSG
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
class InvalidInputType < ArgumentError
|
42
|
+
def initialize(input_val)
|
43
|
+
super(
|
44
|
+
<<~MSG
|
45
|
+
Input must be a Hash, but got: <#{input_val.inspect}>
|
46
|
+
MSG
|
47
|
+
)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class MissingAttributeInput < ArgumentError
|
52
|
+
def initialize(key)
|
53
|
+
super(
|
54
|
+
<<~MSG
|
55
|
+
Attribute :#{key} missing from input
|
56
|
+
MSG
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class InvalidAttributeValue < ArgumentError
|
62
|
+
def initialize(key, val)
|
63
|
+
super(
|
64
|
+
<<~MSG
|
65
|
+
Attribute :#{key} received invalid value:
|
66
|
+
#{val.inspect}
|
67
|
+
MSG
|
68
|
+
)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class InvalidAttributeDefaultValue < ArgumentError
|
73
|
+
def initialize(key, val)
|
74
|
+
super(
|
75
|
+
<<~MSG
|
76
|
+
Attribute :#{key} is declared with invalid default value:
|
77
|
+
#{val.inspect}
|
78
|
+
MSG
|
79
|
+
)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module Private
|
85
|
+
# No 2 procs are ever the same
|
86
|
+
ATTR_DEFAULT_VALUE_ABSENT_VAL = -> {}
|
87
|
+
end
|
88
|
+
private_constant :Private
|
89
|
+
|
90
|
+
class AttributeSet
|
91
|
+
def self.new(*)
|
92
|
+
::IceNine.deep_freeze(super)
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialize(attributes_hash = {})
|
96
|
+
@attributes_hash = attributes_hash
|
97
|
+
end
|
98
|
+
|
99
|
+
def merge(other_attr_set)
|
100
|
+
self.class.new(attributes_hash.merge(other_attr_set.attributes_hash))
|
101
|
+
end
|
102
|
+
|
103
|
+
def add(attr)
|
104
|
+
merge!(self.class.new(attr.name => attr))
|
105
|
+
end
|
106
|
+
|
107
|
+
def each_attribute
|
108
|
+
return to_enum(:each_attribute) unless block_given?
|
109
|
+
|
110
|
+
attributes_hash.each_value do |v|
|
111
|
+
yield(v)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
protected
|
116
|
+
|
117
|
+
def merge!(other_attr_set)
|
118
|
+
shared_keys = attr_names & other_attr_set.attr_names
|
119
|
+
if shared_keys.any?
|
120
|
+
raise(Errors::DuplicateAttributeDeclaration, shared_keys.first)
|
121
|
+
end
|
122
|
+
|
123
|
+
self.class.new(attributes_hash.merge(other_attr_set.attributes_hash))
|
124
|
+
end
|
125
|
+
|
126
|
+
def attr_names
|
127
|
+
@attributes_hash.keys
|
128
|
+
end
|
129
|
+
|
130
|
+
attr_reader :attributes_hash
|
131
|
+
end
|
132
|
+
|
133
|
+
class Attribute
|
134
|
+
def self.new(*)
|
135
|
+
::IceNine.deep_freeze(super)
|
136
|
+
end
|
137
|
+
|
138
|
+
def initialize(
|
139
|
+
name:, contract:, refrigeration_mode:, default_value:
|
140
|
+
)
|
141
|
+
|
142
|
+
@name = name
|
143
|
+
@contract = contract
|
144
|
+
@refrigeration_mode = refrigeration_mode
|
145
|
+
@default_value = default_value
|
146
|
+
|
147
|
+
raise_error_if_inputs_invalid
|
148
|
+
end
|
149
|
+
|
150
|
+
attr_reader :name
|
151
|
+
attr_reader :contract
|
152
|
+
attr_reader :refrigeration_mode
|
153
|
+
|
154
|
+
def extract_value(hash)
|
155
|
+
if hash.key?(name)
|
156
|
+
attr_value = hash.fetch(name)
|
157
|
+
|
158
|
+
unless Contract.valid?(attr_value, contract)
|
159
|
+
raise(
|
160
|
+
Errors::InvalidAttributeValue.new(name, attr_value),
|
161
|
+
)
|
162
|
+
end
|
163
|
+
|
164
|
+
return attr_value
|
165
|
+
end
|
166
|
+
|
167
|
+
# Data missing from input
|
168
|
+
# Use default value if present
|
169
|
+
# Raise error otherwise
|
170
|
+
|
171
|
+
return default_value if default_value_present?
|
172
|
+
|
173
|
+
raise(
|
174
|
+
Errors::MissingAttributeInput.new(
|
175
|
+
name,
|
176
|
+
),
|
177
|
+
)
|
178
|
+
end
|
179
|
+
|
180
|
+
private
|
181
|
+
|
182
|
+
attr_reader :default_value
|
183
|
+
|
184
|
+
def raise_error_if_inputs_invalid
|
185
|
+
raise_error_if_refrigeration_mode_invalid
|
186
|
+
raise_error_if_default_value_invalid
|
187
|
+
end
|
188
|
+
|
189
|
+
def raise_error_if_refrigeration_mode_invalid
|
190
|
+
return if RefrigerationMode::Enum.all.include?(refrigeration_mode)
|
191
|
+
|
192
|
+
raise Errors::InvalidRefrigerationMode.new(
|
193
|
+
refrigeration_mode,
|
194
|
+
)
|
195
|
+
end
|
196
|
+
|
197
|
+
def raise_error_if_default_value_invalid
|
198
|
+
return unless default_value_present?
|
199
|
+
return if Contract.valid?(default_value, contract)
|
200
|
+
|
201
|
+
raise(
|
202
|
+
Errors::InvalidAttributeDefaultValue.new(
|
203
|
+
name,
|
204
|
+
default_value,
|
205
|
+
),
|
206
|
+
)
|
207
|
+
end
|
208
|
+
|
209
|
+
def default_value_present?
|
210
|
+
# The default value of default value (ATTR_DEFAULT_VALUE_ABSENT_VAL)
|
211
|
+
# only represents the absence of default value
|
212
|
+
default_value != Private::ATTR_DEFAULT_VALUE_ABSENT_VAL
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
class Value
|
217
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
218
|
+
# rubocop:disable Metrics/AbcSize
|
219
|
+
def initialize(input_attr_values = {})
|
220
|
+
input_attr_values_hash =
|
221
|
+
case input_attr_values
|
222
|
+
when ::Hash
|
223
|
+
input_attr_values
|
224
|
+
when Value
|
225
|
+
input_attr_values.to_h
|
226
|
+
else
|
227
|
+
raise(
|
228
|
+
Errors::InvalidInputType.new(
|
229
|
+
input_attr_values,
|
230
|
+
),
|
231
|
+
)
|
232
|
+
end
|
233
|
+
|
234
|
+
self.class.send(:attribute_set).each_attribute do |attribute|
|
235
|
+
attr_value = attribute.extract_value(input_attr_values_hash)
|
236
|
+
|
237
|
+
sometimes_frozen_attr_value =
|
238
|
+
case attribute.refrigeration_mode
|
239
|
+
when RefrigerationMode::Enum::DEEP
|
240
|
+
# Use ice_nine for deep freezing
|
241
|
+
::IceNine.deep_freeze(attr_value)
|
242
|
+
when RefrigerationMode::Enum::SHALLOW
|
243
|
+
# No need to re-freeze
|
244
|
+
attr_value.frozen? ? attr_value : attr_value.freeze
|
245
|
+
when RefrigerationMode::Enum::NONE
|
246
|
+
# No freezing
|
247
|
+
attr_value
|
248
|
+
else
|
249
|
+
raise Errors::InvalidRefrigerationMode.new(
|
250
|
+
refrigeration_mode,
|
251
|
+
)
|
252
|
+
end
|
253
|
+
|
254
|
+
# Using symbol since attribute names are limited in number
|
255
|
+
# An alternative would be using frozen string
|
256
|
+
instance_variable_set(
|
257
|
+
:"@#{attribute.name}",
|
258
|
+
sometimes_frozen_attr_value,
|
259
|
+
)
|
260
|
+
end
|
261
|
+
|
262
|
+
freeze
|
263
|
+
end
|
264
|
+
# rubocop:enable Metrics/AbcSize
|
265
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
266
|
+
|
267
|
+
def to_h
|
268
|
+
self.class.send(:attribute_set).
|
269
|
+
each_attribute.each_with_object({}) do |attribute, hash|
|
270
|
+
hash[attribute.name] = instance_variable_get(:"@#{attribute.name}")
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
# == Class interface == #
|
275
|
+
class << self
|
276
|
+
def inherited(klass)
|
277
|
+
super
|
278
|
+
|
279
|
+
klass.instance_variable_set(:@attribute_set, AttributeSet.new)
|
280
|
+
end
|
281
|
+
|
282
|
+
private
|
283
|
+
|
284
|
+
# @api
|
285
|
+
def attribute(
|
286
|
+
name,
|
287
|
+
contract: ::Contracts::Builtin::Any,
|
288
|
+
refrigeration_mode: RefrigerationMode::Enum::DEEP,
|
289
|
+
default_value: Private::ATTR_DEFAULT_VALUE_ABSENT_VAL
|
290
|
+
)
|
291
|
+
|
292
|
+
attr = Attribute.new(
|
293
|
+
name: name,
|
294
|
+
contract: contract,
|
295
|
+
refrigeration_mode: refrigeration_mode,
|
296
|
+
default_value: default_value,
|
297
|
+
)
|
298
|
+
@attribute_set = @attribute_set.add(attr)
|
299
|
+
|
300
|
+
attr_reader(name)
|
301
|
+
end
|
302
|
+
|
303
|
+
# @api private
|
304
|
+
def super_attribute_set
|
305
|
+
unless superclass.respond_to?(:attribute_set, true)
|
306
|
+
return AttributeSet.new
|
307
|
+
end
|
308
|
+
|
309
|
+
superclass.send(:attribute_set)
|
310
|
+
end
|
311
|
+
|
312
|
+
# @api private
|
313
|
+
def attribute_set
|
314
|
+
# When the chain comes back to original class
|
315
|
+
# (ContractedValue::Value)
|
316
|
+
# @attribute_set would be nil
|
317
|
+
super_attribute_set.merge(@attribute_set || AttributeSet.new)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
# == Class interface == #
|
321
|
+
end
|
322
|
+
end
|