contracted_value 0.1.0.alpha.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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"