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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ContractedValue
4
+ VERSION = "0.1.0.alpha.1"
5
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "contracted_value/version"
4
+ require_relative "contracted_value/core"