stannum 0.2.0 → 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/CHANGELOG.md +49 -0
- data/README.md +130 -1200
- data/config/locales/en.rb +4 -0
- data/lib/stannum/association.rb +293 -0
- data/lib/stannum/associations/many.rb +250 -0
- data/lib/stannum/associations/one.rb +106 -0
- data/lib/stannum/associations.rb +11 -0
- data/lib/stannum/attribute.rb +86 -8
- data/lib/stannum/constraints/base.rb +3 -5
- data/lib/stannum/constraints/enum.rb +1 -1
- data/lib/stannum/constraints/equality.rb +1 -1
- data/lib/stannum/constraints/format.rb +72 -0
- data/lib/stannum/constraints/hashes/extra_keys.rb +13 -13
- data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
- data/lib/stannum/constraints/hashes.rb +6 -2
- data/lib/stannum/constraints/identity.rb +1 -1
- data/lib/stannum/constraints/properties/base.rb +124 -0
- data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
- data/lib/stannum/constraints/properties/match_property.rb +117 -0
- data/lib/stannum/constraints/properties/matching.rb +112 -0
- data/lib/stannum/constraints/properties.rb +17 -0
- data/lib/stannum/constraints/signature.rb +2 -2
- data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
- data/lib/stannum/constraints/type.rb +4 -4
- data/lib/stannum/constraints/types/array_type.rb +2 -2
- data/lib/stannum/constraints/types/hash_type.rb +4 -4
- data/lib/stannum/constraints/union.rb +1 -1
- data/lib/stannum/constraints/uuid.rb +30 -0
- data/lib/stannum/constraints.rb +3 -0
- data/lib/stannum/contract.rb +7 -7
- data/lib/stannum/contracts/array_contract.rb +2 -7
- data/lib/stannum/contracts/base.rb +15 -15
- data/lib/stannum/contracts/builder.rb +15 -4
- data/lib/stannum/contracts/hash_contract.rb +3 -9
- data/lib/stannum/contracts/indifferent_hash_contract.rb +15 -2
- data/lib/stannum/contracts/map_contract.rb +6 -10
- data/lib/stannum/contracts/parameters/arguments_contract.rb +1 -1
- data/lib/stannum/contracts/parameters/keywords_contract.rb +1 -1
- data/lib/stannum/contracts/parameters/signature_contract.rb +1 -1
- data/lib/stannum/contracts/parameters_contract.rb +4 -4
- data/lib/stannum/contracts/tuple_contract.rb +6 -6
- data/lib/stannum/entities/associations.rb +451 -0
- data/lib/stannum/entities/attributes.rb +316 -0
- data/lib/stannum/entities/constraints.rb +178 -0
- data/lib/stannum/entities/primary_key.rb +148 -0
- data/lib/stannum/entities/properties.rb +208 -0
- data/lib/stannum/entities.rb +16 -0
- data/lib/stannum/entity.rb +87 -0
- data/lib/stannum/errors.rb +12 -16
- data/lib/stannum/messages/default_strategy.rb +2 -2
- data/lib/stannum/parameter_validation.rb +10 -10
- data/lib/stannum/rspec/match_errors_matcher.rb +7 -7
- data/lib/stannum/rspec/validate_parameter.rb +2 -2
- data/lib/stannum/rspec/validate_parameter_matcher.rb +22 -20
- data/lib/stannum/schema.rb +117 -76
- data/lib/stannum/struct.rb +12 -346
- data/lib/stannum/support/optional.rb +1 -1
- data/lib/stannum/version.rb +4 -4
- data/lib/stannum.rb +6 -0
- metadata +26 -85
@@ -0,0 +1,316 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/entities'
|
4
|
+
|
5
|
+
module Stannum::Entities
|
6
|
+
# Methods for defining and accessing entity attributes.
|
7
|
+
module Attributes # rubocop:disable Metrics/ModuleLength
|
8
|
+
# Class methods to extend the class when including Attributes.
|
9
|
+
module ClassMethods
|
10
|
+
# Defines an attribute on the entity.
|
11
|
+
#
|
12
|
+
# When an attribute is defined, each of the following steps is executed:
|
13
|
+
#
|
14
|
+
# - Adds the attribute to ::Attributes and the .attributes class method.
|
15
|
+
# - Adds the attribute to #attributes and the associated methods, such as
|
16
|
+
# #assign_attributes, #[] and #[]=.
|
17
|
+
# - Defines reader and writer methods.
|
18
|
+
#
|
19
|
+
# @param attr_name [String, Symbol] The name of the attribute. Must be a
|
20
|
+
# non-empty String or Symbol.
|
21
|
+
# @param attr_type [Class, String] The type of the attribute. Must be a
|
22
|
+
# Class or Module, or the name of a class or module.
|
23
|
+
# @param options [Hash] Additional options for the attribute.
|
24
|
+
#
|
25
|
+
# @option options [Object] :default The default value for the attribute.
|
26
|
+
# Defaults to nil.
|
27
|
+
# @option options [Boolean] :primary_key true if the attribute represents
|
28
|
+
# the primary key for the entity; otherwise false. Defaults to false.
|
29
|
+
#
|
30
|
+
# @return [Symbol] the attribute name as a symbol.
|
31
|
+
def attribute(attr_name, attr_type, **options)
|
32
|
+
attributes.define(
|
33
|
+
name: attr_name,
|
34
|
+
type: attr_type,
|
35
|
+
options:
|
36
|
+
)
|
37
|
+
|
38
|
+
attr_name.intern
|
39
|
+
end
|
40
|
+
alias define_attribute attribute
|
41
|
+
|
42
|
+
# @return [Stannum::Schema] The attributes Schema object for the Entity.
|
43
|
+
def attributes
|
44
|
+
self::Attributes
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def included(other)
|
50
|
+
super
|
51
|
+
|
52
|
+
other.include(Stannum::Entities::Attributes)
|
53
|
+
|
54
|
+
Stannum::Entities::Attributes.apply(other) if other.is_a?(Class)
|
55
|
+
end
|
56
|
+
|
57
|
+
def inherited(other)
|
58
|
+
super
|
59
|
+
|
60
|
+
Stannum::Entities::Attributes.apply(other)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
class << self
|
65
|
+
# Generates Attributes schema for the class.
|
66
|
+
#
|
67
|
+
# Creates a new Stannum::Schema and sets it as the class's :Attributes
|
68
|
+
# constant. If the superclass is an entity class (and already defines its
|
69
|
+
# own Attributes, includes the superclass Attributes in the class
|
70
|
+
# Attributes). Finally, includes the class Attributes in the class.
|
71
|
+
#
|
72
|
+
# @param other [Class] the class to which attributes are added.
|
73
|
+
def apply(other)
|
74
|
+
return unless other.is_a?(Class)
|
75
|
+
|
76
|
+
return if entity_class?(other)
|
77
|
+
|
78
|
+
other.const_set(:Attributes, build_schema)
|
79
|
+
|
80
|
+
if entity_class?(other.superclass)
|
81
|
+
other::Attributes.include(other.superclass::Attributes)
|
82
|
+
end
|
83
|
+
|
84
|
+
other.include(other::Attributes)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def build_schema
|
90
|
+
Stannum::Schema.new(
|
91
|
+
property_class: Stannum::Attribute,
|
92
|
+
property_name: 'attributes'
|
93
|
+
)
|
94
|
+
end
|
95
|
+
|
96
|
+
def entity_class?(other)
|
97
|
+
other.const_defined?(:Attributes, false)
|
98
|
+
end
|
99
|
+
|
100
|
+
def included(other)
|
101
|
+
super
|
102
|
+
|
103
|
+
other.extend(self::ClassMethods)
|
104
|
+
|
105
|
+
apply(other) if other.is_a?(Class)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param properties [Hash] the properties used to initialize the entity.
|
110
|
+
def initialize(**properties)
|
111
|
+
@attributes = {}
|
112
|
+
|
113
|
+
self.class.attributes.each_key { |key| @attributes[key] = nil }
|
114
|
+
|
115
|
+
super
|
116
|
+
end
|
117
|
+
|
118
|
+
# Updates the struct's attributes with the given values.
|
119
|
+
#
|
120
|
+
# This method is used to update some (but not all) of the attributes of the
|
121
|
+
# struct. For each key in the hash, it calls the corresponding writer method
|
122
|
+
# with the value for that attribute. If the value is nil, this will set the
|
123
|
+
# attribute value to the default for that attribute.
|
124
|
+
#
|
125
|
+
# Any attributes that are not in the given hash are unchanged, as are any
|
126
|
+
# properties that are not attributes.
|
127
|
+
#
|
128
|
+
# If the attributes hash includes any keys that do not correspond to an
|
129
|
+
# attribute, the struct will raise an error.
|
130
|
+
#
|
131
|
+
# @param attributes [Hash] The attributes for the struct.
|
132
|
+
#
|
133
|
+
# @raise ArgumentError if any key is not a valid attribute.
|
134
|
+
#
|
135
|
+
# @see #attributes=
|
136
|
+
def assign_attributes(attributes)
|
137
|
+
unless attributes.is_a?(Hash)
|
138
|
+
raise ArgumentError, 'attributes must be a Hash'
|
139
|
+
end
|
140
|
+
|
141
|
+
set_attributes(attributes, force: false)
|
142
|
+
end
|
143
|
+
|
144
|
+
# Collects the entity attributes.
|
145
|
+
#
|
146
|
+
# @return [Hash<String, Object>] the entity attributes.
|
147
|
+
def attributes
|
148
|
+
@attributes.dup
|
149
|
+
end
|
150
|
+
|
151
|
+
# Replaces the entity's attributes with the given values.
|
152
|
+
#
|
153
|
+
# This method is used to update all of the attributes of the entity. For
|
154
|
+
# each attribute, the writer method is called with the value from the hash,
|
155
|
+
# or nil if the corresponding key is not present in the hash. Any nil or
|
156
|
+
# missing values set the attribute value to that attribute's default value,
|
157
|
+
# if any. Non-attribute properties are unchanged.
|
158
|
+
#
|
159
|
+
# If the attributes hash includes any keys that do not correspond to a valid
|
160
|
+
# attribute, the entity will raise an error.
|
161
|
+
#
|
162
|
+
# @param attributes [Hash] the attributes to assign to the entity.
|
163
|
+
#
|
164
|
+
# @raise ArgumentError if any key is not a valid attribute.
|
165
|
+
#
|
166
|
+
# @see #assign_attributes
|
167
|
+
def attributes=(attributes)
|
168
|
+
unless attributes.is_a?(Hash)
|
169
|
+
raise ArgumentError, 'attributes must be a Hash'
|
170
|
+
end
|
171
|
+
|
172
|
+
set_attributes(attributes, force: true)
|
173
|
+
end
|
174
|
+
|
175
|
+
# (see Stannum::Entities::Properties#properties)
|
176
|
+
def properties
|
177
|
+
super.merge(attributes)
|
178
|
+
end
|
179
|
+
|
180
|
+
# Retrieves the attribute value for the requested key.
|
181
|
+
#
|
182
|
+
# If the :safe flag is set, will verify that the attribute name is valid (a
|
183
|
+
# non-empty String or Symbol) and that there is a defined attribute by that
|
184
|
+
# name. By default, :safe is set to true.
|
185
|
+
#
|
186
|
+
# @param key [String, Symbol] the key of the attribute to retrieve.
|
187
|
+
# @param safe [Boolean] if true, validates the attribute key.
|
188
|
+
#
|
189
|
+
# @return [Object] the value of the requested attribute.
|
190
|
+
#
|
191
|
+
# @api private
|
192
|
+
def read_attribute(key, safe: true)
|
193
|
+
if safe
|
194
|
+
tools.assertions.validate_name(key, as: 'attribute')
|
195
|
+
|
196
|
+
unless self.class.attributes.key?(key.to_s)
|
197
|
+
raise ArgumentError, "unknown attribute #{key.inspect}"
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
@attributes[key.to_s]
|
202
|
+
end
|
203
|
+
|
204
|
+
# Assigns the attribute value for the requested key.
|
205
|
+
#
|
206
|
+
# If the :safe flag is set, will verify that the attribute name is valid (a
|
207
|
+
# non-empty String or Symbol) and that there is a defined attribute by that
|
208
|
+
# name. By default, :safe is set to true.
|
209
|
+
#
|
210
|
+
# @param key [String, Symbol] the key of the attribute to assign.
|
211
|
+
# @param value [Object] the value to assign.
|
212
|
+
# @param safe [Boolean] if true, validates the attribute key.
|
213
|
+
#
|
214
|
+
# @return [Object] the assigned value.
|
215
|
+
#
|
216
|
+
# @api private
|
217
|
+
def write_attribute(key, value, safe: true)
|
218
|
+
if safe
|
219
|
+
tools.assertions.validate_name(key, as: 'attribute')
|
220
|
+
|
221
|
+
unless self.class.attributes.key?(key.to_s)
|
222
|
+
raise ArgumentError, "unknown attribute #{key.inspect}"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
@attributes[key.to_s] = value
|
227
|
+
end
|
228
|
+
|
229
|
+
private
|
230
|
+
|
231
|
+
def apply_defaults_for(attributes) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
232
|
+
with_value, with_proc = bisect_attributes_by_default_type
|
233
|
+
|
234
|
+
with_value.each do |attribute|
|
235
|
+
next unless @attributes[attribute.name].nil?
|
236
|
+
next unless attributes.key?(attribute.name)
|
237
|
+
|
238
|
+
send(attribute.writer_name, attribute.default)
|
239
|
+
end
|
240
|
+
|
241
|
+
with_proc.each do |attribute|
|
242
|
+
next unless @attributes[attribute.name].nil?
|
243
|
+
next unless attributes.key?(attribute.name)
|
244
|
+
|
245
|
+
send(attribute.writer_name, attribute.default_value_for(self))
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def bisect_attributes_by_default_type
|
250
|
+
with_value = []
|
251
|
+
with_proc = []
|
252
|
+
|
253
|
+
self.class.attributes.each_value do |attribute|
|
254
|
+
next unless attribute.default?
|
255
|
+
|
256
|
+
(attribute.default.is_a?(Proc) ? with_proc : with_value) << attribute
|
257
|
+
end
|
258
|
+
|
259
|
+
[with_value, with_proc]
|
260
|
+
end
|
261
|
+
|
262
|
+
def get_property(key)
|
263
|
+
return @attributes[key.to_s] if attributes.key?(key.to_s)
|
264
|
+
|
265
|
+
super
|
266
|
+
end
|
267
|
+
|
268
|
+
def inspect_properties(**options)
|
269
|
+
return super unless options.fetch(:attributes, true)
|
270
|
+
|
271
|
+
@attributes.reduce(super) do |memo, (key, value)|
|
272
|
+
"#{memo} #{key}: #{value.inspect}"
|
273
|
+
end
|
274
|
+
end
|
275
|
+
|
276
|
+
def set_attributes(attributes, force:)
|
277
|
+
attributes, non_matching =
|
278
|
+
bisect_properties(attributes, self.class.attributes)
|
279
|
+
|
280
|
+
unless non_matching.empty?
|
281
|
+
handle_invalid_properties(non_matching, as: 'attribute')
|
282
|
+
end
|
283
|
+
|
284
|
+
write_attributes(attributes, force:)
|
285
|
+
end
|
286
|
+
|
287
|
+
def set_properties(properties, force:)
|
288
|
+
attributes, non_matching =
|
289
|
+
bisect_properties(properties, self.class.attributes)
|
290
|
+
|
291
|
+
super(non_matching, force:)
|
292
|
+
|
293
|
+
write_attributes(attributes, force:)
|
294
|
+
end
|
295
|
+
|
296
|
+
def set_property(key, value)
|
297
|
+
return super unless attributes.key?(key.to_s)
|
298
|
+
|
299
|
+
send(self.class.attributes[key.to_s].writer_name, value)
|
300
|
+
end
|
301
|
+
|
302
|
+
def write_attributes(attributes, force:)
|
303
|
+
self.class.attributes.each do |attr_name, attribute|
|
304
|
+
next unless attributes.key?(attr_name) || force
|
305
|
+
|
306
|
+
if attributes[attr_name].nil? && attribute.default?
|
307
|
+
@attributes[attr_name] = nil
|
308
|
+
else
|
309
|
+
send(attribute.writer_name, attributes[attr_name])
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
apply_defaults_for(force ? @attributes : attributes)
|
314
|
+
end
|
315
|
+
end
|
316
|
+
end
|
@@ -0,0 +1,178 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/entities'
|
4
|
+
|
5
|
+
module Stannum::Entities
|
6
|
+
# Methods for defining and accessing entity constraints.
|
7
|
+
module Constraints
|
8
|
+
# Class methods to extend the class when including Attributes.
|
9
|
+
module AttributesMethods
|
10
|
+
# Defines an attribute on the entity.
|
11
|
+
#
|
12
|
+
# Delegates to the superclass method, and then adds a type constraint to
|
13
|
+
# ::Contract.
|
14
|
+
#
|
15
|
+
# @see Stannum::Entities::Attributes::ClassMethods#attribute.
|
16
|
+
def attribute(attr_name, attr_type, **options) # rubocop:disable Metrics/MethodLength
|
17
|
+
returned = super
|
18
|
+
|
19
|
+
attribute = attributes[attr_name.to_s]
|
20
|
+
constraint = Stannum::Constraints::Type.new(
|
21
|
+
attribute.type,
|
22
|
+
required: attribute.required?
|
23
|
+
)
|
24
|
+
|
25
|
+
self::Contract.add_constraint(
|
26
|
+
constraint,
|
27
|
+
property: attribute.reader_name
|
28
|
+
)
|
29
|
+
|
30
|
+
returned
|
31
|
+
end
|
32
|
+
alias define_attribute attribute
|
33
|
+
end
|
34
|
+
|
35
|
+
# Class methods to extend the class when including Constraints.
|
36
|
+
module ClassMethods
|
37
|
+
# Defines a constraint on the entity or one of its properties.
|
38
|
+
#
|
39
|
+
# @overload constraint()
|
40
|
+
# Defines a constraint on the entity.
|
41
|
+
#
|
42
|
+
# A new Stannum::Constraint instance will be generated, passing the
|
43
|
+
# block from .constraint to the new constraint. This constraint will be
|
44
|
+
# added to the contract.
|
45
|
+
#
|
46
|
+
# @yieldparam entity [Stannum::Entities::Constraints] The entity at the
|
47
|
+
# time the constraint is evaluated.
|
48
|
+
#
|
49
|
+
# @overload constraint(constraint)
|
50
|
+
# Defines a constraint on the entity.
|
51
|
+
#
|
52
|
+
# The given constraint is added to the contract. When the contract is
|
53
|
+
# evaluated, this constraint will be matched against the entity.
|
54
|
+
#
|
55
|
+
# @param constraint [Stannum::Constraints::Base] The constraint to add.
|
56
|
+
#
|
57
|
+
# @overload constraint(attr_name)
|
58
|
+
# Defines a constraint on the given attribute or property.
|
59
|
+
#
|
60
|
+
# A new Stannum::Constraint instance will be generated, passing the
|
61
|
+
# block from .constraint to the new constraint. This constraint will be
|
62
|
+
# added to the contract.
|
63
|
+
#
|
64
|
+
# @param attr_name [String, Symbol] The name of the attribute or
|
65
|
+
# property to constrain.
|
66
|
+
#
|
67
|
+
# @yieldparam value [Object] The value of the attribute or property of
|
68
|
+
# the entity at the time the constraint is evaluated.
|
69
|
+
#
|
70
|
+
# @overload constraint(attr_name, constraint)
|
71
|
+
# Defines a constraint on the given attribute or property.
|
72
|
+
#
|
73
|
+
# The given constraint is added to the contract. When the contract is
|
74
|
+
# evaluated, this constraint will be matched against the value of the
|
75
|
+
# attribute or property.
|
76
|
+
#
|
77
|
+
# @param attr_name [String, Symbol] The name of the attribute or
|
78
|
+
# property to constrain.
|
79
|
+
# @param constraint [Stannum::Constraints::Base] The constraint to add.
|
80
|
+
def constraint(attr_name = nil, constraint = nil, &)
|
81
|
+
attr_name, constraint = resolve_constraint(attr_name, constraint)
|
82
|
+
|
83
|
+
if block_given?
|
84
|
+
constraint = Stannum::Constraint.new(&)
|
85
|
+
else
|
86
|
+
validate_constraint(constraint)
|
87
|
+
end
|
88
|
+
|
89
|
+
contract.add_constraint(constraint, property: attr_name)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [Stannum::Contract] The Contract object for the entity.
|
93
|
+
def contract
|
94
|
+
self::Contract
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
def included(other)
|
100
|
+
super
|
101
|
+
|
102
|
+
other.include(Stannum::Entities::Constraints)
|
103
|
+
|
104
|
+
Stannum::Entities::Constraints.apply(other) if other.is_a?(Class)
|
105
|
+
end
|
106
|
+
|
107
|
+
def inherited(other)
|
108
|
+
super
|
109
|
+
|
110
|
+
Stannum::Entities::Constraints.apply(other)
|
111
|
+
end
|
112
|
+
|
113
|
+
def resolve_constraint(attr_name, constraint)
|
114
|
+
return [nil, attr_name] if attr_name.is_a?(Stannum::Constraints::Base)
|
115
|
+
|
116
|
+
unless attr_name.nil?
|
117
|
+
tools.assertions.validate_name(attr_name, as: 'attribute')
|
118
|
+
end
|
119
|
+
|
120
|
+
[attr_name.nil? ? attr_name : attr_name.intern, constraint]
|
121
|
+
end
|
122
|
+
|
123
|
+
def tools
|
124
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
125
|
+
end
|
126
|
+
|
127
|
+
def validate_constraint(constraint)
|
128
|
+
raise ArgumentError, "constraint can't be blank" if constraint.nil?
|
129
|
+
|
130
|
+
return if constraint.is_a?(Stannum::Constraints::Base)
|
131
|
+
|
132
|
+
raise ArgumentError, 'constraint must be a Stannum::Constraints::Base'
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class << self
|
137
|
+
# Generates a Contract for the class.
|
138
|
+
#
|
139
|
+
# Creates a new Stannum::Contract and sets it as the class's :Contract
|
140
|
+
# constant. If the superclass is an entity class (and already defines its
|
141
|
+
# own Contract, concatenates the superclass Contract into the class
|
142
|
+
# Contract).
|
143
|
+
#
|
144
|
+
# @param other [Class] the class to which attributes are added.
|
145
|
+
def apply(other)
|
146
|
+
return unless other.is_a?(Class)
|
147
|
+
|
148
|
+
return if entity_class?(other)
|
149
|
+
|
150
|
+
contract = Stannum::Contract.new
|
151
|
+
|
152
|
+
other.const_set(:Contract, contract)
|
153
|
+
|
154
|
+
return unless entity_class?(other.superclass)
|
155
|
+
|
156
|
+
contract.concat(other.superclass::Contract)
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
def entity_class?(other)
|
162
|
+
other.const_defined?(:Contract, false)
|
163
|
+
end
|
164
|
+
|
165
|
+
def included(other)
|
166
|
+
super
|
167
|
+
|
168
|
+
other.extend(self::ClassMethods)
|
169
|
+
|
170
|
+
if other < Stannum::Entities::Attributes
|
171
|
+
other.extend(self::AttributesMethods)
|
172
|
+
end
|
173
|
+
|
174
|
+
apply(other) if other.is_a?(Class)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/entities'
|
4
|
+
|
5
|
+
module Stannum::Entities
|
6
|
+
# Methods for defining and accessing an entity's primary key attribute.
|
7
|
+
module PrimaryKey
|
8
|
+
# Raised when adding a primary key to an entity that already has one.
|
9
|
+
class PrimaryKeyAlreadyExists < StandardError; end
|
10
|
+
|
11
|
+
# Raised when accessing a primary key for an entity that does not have one.
|
12
|
+
class PrimaryKeyMissing < StandardError; end
|
13
|
+
|
14
|
+
# Class methods to extend the class when including PrimaryKey.
|
15
|
+
module ClassMethods
|
16
|
+
# Defines a primary key attribute on the entity.
|
17
|
+
#
|
18
|
+
# @param attr_name [String, Symbol] The name of the attribute. Must be a
|
19
|
+
# non-empty String or Symbol.
|
20
|
+
# @param attr_type [Class, String] The type of the attribute. Must be a
|
21
|
+
# Class or Module, or the name of a class or module.
|
22
|
+
# @param options [Hash] Additional options for the attribute.
|
23
|
+
#
|
24
|
+
# @option options [Object] :default The default value for the attribute.
|
25
|
+
# Defaults to nil.
|
26
|
+
# @option options [Boolean] :primary_key true if the attribute represents
|
27
|
+
# the primary key for the entity; otherwise false. Defaults to false.
|
28
|
+
#
|
29
|
+
# @return [Symbol] The attribute name as a symbol.
|
30
|
+
#
|
31
|
+
# @see Stannum::Entities::Attributes::ClassMethods#define_attribute.
|
32
|
+
def define_primary_key(attr_name, attr_type, **options)
|
33
|
+
if primary_key?
|
34
|
+
raise PrimaryKeyAlreadyExists,
|
35
|
+
"#{name} already defines primary key #{primary_key_name.inspect}"
|
36
|
+
end
|
37
|
+
|
38
|
+
attribute(attr_name, attr_type, **options, primary_key: true)
|
39
|
+
end
|
40
|
+
|
41
|
+
# @return [Stannum::Attribute] the primary key attribute.
|
42
|
+
#
|
43
|
+
# @raise [Stannum::Entities::PrimaryKey::PrimaryKeyMissing] if the entity
|
44
|
+
# does not define a primary key.
|
45
|
+
def primary_key
|
46
|
+
primary_key =
|
47
|
+
attributes
|
48
|
+
.find { |_, attribute| attribute.primary_key? }
|
49
|
+
&.last
|
50
|
+
|
51
|
+
return primary_key if primary_key
|
52
|
+
|
53
|
+
raise PrimaryKeyMissing, "#{name} does not define a primary key"
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Boolean] true if the entity class defines a primary key;
|
57
|
+
# otherwise false.
|
58
|
+
def primary_key?
|
59
|
+
attributes.any? { |_, attribute| attribute.primary_key? }
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [String, nil] the name of the primary key attribute, or nil if
|
63
|
+
# the entity does not define a primary key.
|
64
|
+
def primary_key_name
|
65
|
+
attributes
|
66
|
+
.find { |_, attribute| attribute.primary_key? }
|
67
|
+
&.last
|
68
|
+
&.name
|
69
|
+
end
|
70
|
+
|
71
|
+
# @return [Class, nil] the type of the primary key attribute, or nil if
|
72
|
+
# the entity does not define a primary key.
|
73
|
+
def primary_key_type
|
74
|
+
attributes
|
75
|
+
.find { |_, attribute| attribute.primary_key? }
|
76
|
+
&.last
|
77
|
+
&.resolved_type
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def included(other)
|
83
|
+
super
|
84
|
+
|
85
|
+
other.include(Stannum::Entities::PrimaryKey)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class << self
|
90
|
+
private
|
91
|
+
|
92
|
+
def included(other)
|
93
|
+
super
|
94
|
+
|
95
|
+
other.extend(self::ClassMethods)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# @return [Boolean] true if the entity class defines a primary key and if
|
100
|
+
# the entity has a non-empty value for that attribute; otherwise false.
|
101
|
+
def primary_key?
|
102
|
+
return false unless self.class.primary_key?
|
103
|
+
|
104
|
+
value = attributes[self.class.primary_key_name]
|
105
|
+
|
106
|
+
return false if value.nil? || (value.respond_to?(:empty?) && value.empty?)
|
107
|
+
|
108
|
+
true
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return [String] the name of the primary key attribute.
|
112
|
+
#
|
113
|
+
# @raise [Stannum::Entities::PrimaryKey::PrimaryKeyMissing] if the entity
|
114
|
+
# does not define a primary key.
|
115
|
+
def primary_key_name
|
116
|
+
unless self.class.primary_key?
|
117
|
+
raise PrimaryKeyMissing, "#{self.class} does not define a primary key"
|
118
|
+
end
|
119
|
+
|
120
|
+
self.class.primary_key_name
|
121
|
+
end
|
122
|
+
|
123
|
+
# @return [Class] the type of the primary key attribute.
|
124
|
+
#
|
125
|
+
# @raise [Stannum::Entities::PrimaryKey::PrimaryKeyMissing] if the entity
|
126
|
+
# does not define a primary key.
|
127
|
+
def primary_key_type
|
128
|
+
unless self.class.primary_key?
|
129
|
+
raise PrimaryKeyMissing, "#{self.class} does not define a primary key"
|
130
|
+
end
|
131
|
+
|
132
|
+
self.class.primary_key_type
|
133
|
+
end
|
134
|
+
|
135
|
+
# @return [Object] the current value of the primary key attribute.
|
136
|
+
#
|
137
|
+
# @raise [Stannum::Entities::PrimaryKey::PrimaryKeyMissing] if the entity
|
138
|
+
# does not define a primary key.
|
139
|
+
def primary_key_value
|
140
|
+
unless self.class.primary_key?
|
141
|
+
raise PrimaryKeyMissing, "#{self.class} does not define a primary key"
|
142
|
+
end
|
143
|
+
|
144
|
+
attributes[self.class.primary_key_name]
|
145
|
+
end
|
146
|
+
alias primary_key primary_key_value
|
147
|
+
end
|
148
|
+
end
|