stannum 0.3.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.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -1
  3. data/README.md +129 -1263
  4. data/config/locales/en.rb +4 -0
  5. data/lib/stannum/association.rb +293 -0
  6. data/lib/stannum/associations/many.rb +250 -0
  7. data/lib/stannum/associations/one.rb +106 -0
  8. data/lib/stannum/associations.rb +11 -0
  9. data/lib/stannum/attribute.rb +86 -8
  10. data/lib/stannum/constraints/base.rb +3 -5
  11. data/lib/stannum/constraints/enum.rb +1 -1
  12. data/lib/stannum/constraints/equality.rb +1 -1
  13. data/lib/stannum/constraints/format.rb +72 -0
  14. data/lib/stannum/constraints/hashes/extra_keys.rb +7 -12
  15. data/lib/stannum/constraints/identity.rb +1 -1
  16. data/lib/stannum/constraints/properties/base.rb +1 -1
  17. data/lib/stannum/constraints/properties/do_not_match_property.rb +11 -11
  18. data/lib/stannum/constraints/properties/match_property.rb +11 -11
  19. data/lib/stannum/constraints/properties/matching.rb +7 -7
  20. data/lib/stannum/constraints/signature.rb +2 -2
  21. data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
  22. data/lib/stannum/constraints/type.rb +3 -3
  23. data/lib/stannum/constraints/types/array_type.rb +2 -2
  24. data/lib/stannum/constraints/types/hash_type.rb +4 -4
  25. data/lib/stannum/constraints/union.rb +1 -1
  26. data/lib/stannum/constraints/uuid.rb +30 -0
  27. data/lib/stannum/constraints.rb +2 -0
  28. data/lib/stannum/contract.rb +7 -7
  29. data/lib/stannum/contracts/array_contract.rb +2 -7
  30. data/lib/stannum/contracts/base.rb +15 -15
  31. data/lib/stannum/contracts/builder.rb +2 -2
  32. data/lib/stannum/contracts/hash_contract.rb +3 -9
  33. data/lib/stannum/contracts/indifferent_hash_contract.rb +2 -2
  34. data/lib/stannum/contracts/map_contract.rb +6 -10
  35. data/lib/stannum/contracts/parameters/arguments_contract.rb +1 -1
  36. data/lib/stannum/contracts/parameters/keywords_contract.rb +1 -1
  37. data/lib/stannum/contracts/parameters/signature_contract.rb +1 -1
  38. data/lib/stannum/contracts/parameters_contract.rb +4 -4
  39. data/lib/stannum/contracts/tuple_contract.rb +5 -5
  40. data/lib/stannum/entities/associations.rb +451 -0
  41. data/lib/stannum/entities/attributes.rb +116 -18
  42. data/lib/stannum/entities/constraints.rb +3 -2
  43. data/lib/stannum/entities/primary_key.rb +148 -0
  44. data/lib/stannum/entities/properties.rb +30 -8
  45. data/lib/stannum/entities.rb +5 -2
  46. data/lib/stannum/entity.rb +4 -0
  47. data/lib/stannum/errors.rb +9 -13
  48. data/lib/stannum/messages/default_strategy.rb +2 -2
  49. data/lib/stannum/parameter_validation.rb +10 -10
  50. data/lib/stannum/rspec/match_errors_matcher.rb +1 -1
  51. data/lib/stannum/rspec/validate_parameter.rb +2 -2
  52. data/lib/stannum/rspec/validate_parameter_matcher.rb +15 -13
  53. data/lib/stannum/schema.rb +62 -62
  54. data/lib/stannum/support/optional.rb +1 -1
  55. data/lib/stannum/version.rb +4 -4
  56. data/lib/stannum.rb +3 -0
  57. metadata +14 -79
@@ -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
@@ -8,6 +8,9 @@ module Stannum::Entities
8
8
  # This module provides a base for accessing and mutating entity properties
9
9
  # such as attributes and associations.
10
10
  module Properties
11
+ MEMORY_ADDRESS_PATTERN = /0x([0-9a-f]+)/
12
+ private_constant :MEMORY_ADDRESS_PATTERN
13
+
11
14
  # @param properties [Hash] the properties used to initialize the entity.
12
15
  def initialize(**properties)
13
16
  set_properties(properties, force: true)
@@ -78,11 +81,22 @@ module Stannum::Entities
78
81
 
79
82
  # @return [String] a string representation of the entity and its properties.
80
83
  def inspect
81
- mapped = inspectable_properties.reduce('') do |memo, (key, value)|
82
- memo + " #{key}: #{value.inspect}"
83
- end
84
+ inspect_with_options
85
+ end
86
+
87
+ # @param options [Hash] options for inspecting the entity.
88
+ #
89
+ # @option options memory_address [Boolean] if true, displays the memory
90
+ # address of the object (as per Object#inspect). Defaults to false.
91
+ # @option options properties [Boolean] if true, displays the entity
92
+ # properties. Defaults to true.
93
+ #
94
+ # @return [String] a string representation of the entity and its properties.
95
+ def inspect_with_options(**options)
96
+ address = options[:memory_address] ? ":#{memory_address}" : ''
97
+ mapped = inspect_properties(**options)
84
98
 
85
- "#<#{self.class.name}#{mapped}>"
99
+ "#<#{self.class.name}#{address}#{mapped}>"
86
100
  end
87
101
 
88
102
  # Collects the entity properties.
@@ -148,14 +162,14 @@ module Stannum::Entities
148
162
 
149
163
  def handle_invalid_properties(properties, as: 'property')
150
164
  properties.each_key do |key|
151
- tools.assertions.assert_name(key, as: as, error_class: ArgumentError)
165
+ tools.assertions.assert_name(key, as:, error_class: ArgumentError)
152
166
  end
153
167
 
154
- raise ArgumentError, invalid_properties_message(properties, as: as)
168
+ raise ArgumentError, invalid_properties_message(properties, as:)
155
169
  end
156
170
 
157
- def inspectable_properties
158
- {}
171
+ def inspect_properties(**)
172
+ ''
159
173
  end
160
174
 
161
175
  def invalid_properties_message(properties, as: 'property')
@@ -163,6 +177,14 @@ module Stannum::Entities
163
177
  properties.keys.map(&:inspect).join(', ')
164
178
  end
165
179
 
180
+ def memory_address
181
+ Object
182
+ .instance_method(:inspect)
183
+ .bind(self)
184
+ .call
185
+ .match(MEMORY_ADDRESS_PATTERN)[1]
186
+ end
187
+
166
188
  def set_property(key, _)
167
189
  raise ArgumentError, "unknown property #{key.inspect}"
168
190
  end
@@ -7,7 +7,10 @@ require 'stannum/entities'
7
7
  module Stannum
8
8
  # Namespace for modules implementing Entity functionality.
9
9
  module Entities
10
- autoload :Attributes, 'stannum/entities/attributes'
11
- autoload :Properties, 'stannum/entities/properties'
10
+ autoload :Attributes, 'stannum/entities/attributes'
11
+ autoload :Associations, 'stannum/entities/associations'
12
+ autoload :Constraints, 'stannum/entities/constraints'
13
+ autoload :PrimaryKey, 'stannum/entities/primary_key'
14
+ autoload :Properties, 'stannum/entities/properties'
12
15
  end
13
16
  end
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'stannum'
4
+ require 'stannum/entities/associations'
4
5
  require 'stannum/entities/attributes'
5
6
  require 'stannum/entities/constraints'
7
+ require 'stannum/entities/primary_key'
6
8
  require 'stannum/entities/properties'
7
9
 
8
10
  module Stannum
@@ -78,6 +80,8 @@ module Stannum
78
80
  module Entity
79
81
  include Stannum::Entities::Properties
80
82
  include Stannum::Entities::Attributes
83
+ include Stannum::Entities::Associations
84
+ include Stannum::Entities::PrimaryKey
81
85
  include Stannum::Entities::Constraints
82
86
  end
83
87
  end
@@ -326,7 +326,7 @@ module Stannum
326
326
  # .add(:not_integer, message: 'is outside the range')
327
327
  # .add(:not_in_range)
328
328
  def add(type, message: nil, **data)
329
- error = build_error(data: data, message: message, type: type)
329
+ error = build_error(data:, message:, type:)
330
330
  hashed = error.hash
331
331
 
332
332
  return self if @cache.include?(hashed)
@@ -436,7 +436,7 @@ module Stannum
436
436
  # @overload each
437
437
  # Iterates through the errors, yielding each error to the provided block.
438
438
  #
439
- # @yieldparam error [Hash<Symbol=>Object>] The error object. Each error
439
+ # @yieldparam error [Hash{Symbol=>Object}] The error object. Each error
440
440
  # is a hash containing the keys :data, :message, :path and :type.
441
441
  def each
442
442
  return to_enum(:each) { size } unless block_given?
@@ -592,14 +592,12 @@ module Stannum
592
592
 
593
593
  protected
594
594
 
595
- def each_error(&block)
595
+ def each_error(&)
596
596
  return enum_for(:each_error) unless block_given?
597
597
 
598
- @errors.each(&block)
598
+ @errors.each(&)
599
599
 
600
- @children.each_value do |child|
601
- child.each_error(&block)
602
- end
600
+ @children.each_value { |child| child.each_error(&) }
603
601
  end
604
602
 
605
603
  def update_errors(other_errors)
@@ -621,10 +619,10 @@ module Stannum
621
619
  type = normalize_type(type)
622
620
  msg = normalize_message(message)
623
621
 
624
- { data: data, message: msg, type: type }
622
+ { data:, message: msg, type: }
625
623
  end
626
624
 
627
- def compare_hashed_errors(other_errors)
625
+ def compare_hashed_errors(other_errors) # rubocop:disable Naming/PredicateMethod
628
626
  hashes = Set.new(map(&:hash))
629
627
  other_hashes = Set.new(other_errors.map(&:hash))
630
628
 
@@ -669,7 +667,7 @@ module Stannum
669
667
  child = self.class.new
670
668
 
671
669
  ary.each do |item|
672
- err = normalize_array_item(item, allow_nil: allow_nil)
670
+ err = normalize_array_item(item, allow_nil:)
673
671
  data = err.fetch(:data, {})
674
672
  path = err.fetch(:path, [])
675
673
 
@@ -708,9 +706,7 @@ module Stannum
708
706
 
709
707
  return value.dup if value.is_a?(self.class)
710
708
 
711
- if value.is_a?(Array)
712
- return normalize_array_value(value, allow_nil: allow_nil)
713
- end
709
+ return normalize_array_value(value, allow_nil:) if value.is_a?(Array)
714
710
 
715
711
  raise ArgumentError, invalid_value_error(allow_nil)
716
712
  end
@@ -18,7 +18,7 @@ module Stannum::Messages
18
18
  @load_paths ||= DEFAULT_LOAD_PATHS.dup
19
19
  end
20
20
 
21
- # @param configuration [Hash{Symbol, Object}] The configured messages.
21
+ # @param configuration [Hash{Symbol=>Object}] The configured messages.
22
22
  # @param load_paths [Array<String>] The directories from which to load
23
23
  # configured error messages.
24
24
  # @param locale [String] The locale used to load and scope configured
@@ -97,7 +97,7 @@ module Stannum::Messages
97
97
  Stannum::Messages::DefaultLoader
98
98
  .new(
99
99
  file_paths: load_paths,
100
- locale: locale
100
+ locale:
101
101
  )
102
102
  .call
103
103
  end
@@ -126,11 +126,11 @@ module Stannum
126
126
  self::MethodValidations.define_method(method_name) \
127
127
  do |*arguments, **keywords, &block|
128
128
  result = match_parameters_to_contract(
129
- arguments: arguments,
130
- block: block,
131
- contract: contract,
132
- keywords: keywords,
133
- method_name: method_name
129
+ arguments:,
130
+ block:,
131
+ contract:,
132
+ keywords:,
133
+ method_name:
134
134
  )
135
135
 
136
136
  return result unless result == VALIDATION_SUCCESS
@@ -199,17 +199,17 @@ module Stannum
199
199
  )
200
200
  match, errors = contract.match(
201
201
  {
202
- arguments: arguments,
203
- keywords: keywords,
204
- block: block
202
+ arguments:,
203
+ keywords:,
204
+ block:
205
205
  }
206
206
  )
207
207
 
208
208
  return VALIDATION_SUCCESS if match
209
209
 
210
210
  handle_invalid_parameters(
211
- errors: errors,
212
- method_name: method_name
211
+ errors:,
212
+ method_name:
213
213
  )
214
214
  end
215
215
  end
@@ -27,7 +27,7 @@ module Stannum::RSpec
27
27
  end
28
28
 
29
29
  # Checks that the given errors do not match the expected errors.
30
- def does_not_match?(actual)
30
+ def does_not_match?(actual) # rubocop:disable Naming/PredicatePrefix
31
31
  @actual = actual.is_a?(Stannum::Errors) ? actual.to_a : actual
32
32
 
33
33
  errors? && equality_matcher.does_not_match?(@actual)
@@ -15,8 +15,8 @@ module Stannum::RSpec
15
15
  # @return [Stannum::RSpec::ValidateParameterMatcher] the matcher.
16
16
  def validate_parameter(method_name, parameter_name)
17
17
  Stannum::RSpec::ValidateParameterMatcher.new(
18
- method_name: method_name,
19
- parameter_name: parameter_name
18
+ method_name:,
19
+ parameter_name:
20
20
  )
21
21
  end
22
22
  end
@@ -18,13 +18,21 @@ module Stannum::RSpec
18
18
  class InvalidParameterHandledError < StandardError; end
19
19
  private_constant :InvalidParameterHandledError
20
20
 
21
+ EXTRA_PARAMETER_ERROR_TYPES = Set.new(
22
+ [
23
+ Stannum::Constraints::Parameters::ExtraArguments::TYPE,
24
+ Stannum::Constraints::Parameters::ExtraKeywords::TYPE
25
+ ]
26
+ ).freeze
27
+ private_constant :EXTRA_PARAMETER_ERROR_TYPES
28
+
21
29
  class << self
22
30
  # @private
23
31
  def add_parameter_mapping(map:, match:)
24
32
  raise ArgumentError, 'map must be a Proc' unless map.is_a?(Proc)
25
33
  raise ArgumentError, 'match must be a Proc' unless match.is_a?(Proc)
26
34
 
27
- parameter_mappings << { match: match, map: map }
35
+ parameter_mappings << { match:, map: }
28
36
  end
29
37
 
30
38
  # @private
@@ -33,12 +41,12 @@ module Stannum::RSpec
33
41
  match = keywords.fetch(:match)
34
42
  map = keywords.fetch(:map)
35
43
 
36
- next unless match.call(actual: actual, method_name: method_name)
44
+ next unless match.call(actual:, method_name:)
37
45
 
38
- return map.call(actual: actual, method_name: method_name)
46
+ return map.call(actual:, method_name:)
39
47
  end
40
48
 
41
- unwrapped_method(actual: actual, method_name: method_name).parameters
49
+ unwrapped_method(actual:, method_name:).parameters
42
50
  end
43
51
 
44
52
  private
@@ -109,7 +117,7 @@ module Stannum::RSpec
109
117
  #
110
118
  # @return [true, false] false if the object validates the parameter,
111
119
  # otherwise true.
112
- def does_not_match?(actual)
120
+ def does_not_match?(actual) # rubocop:disable Naming/PredicatePrefix
113
121
  disallow_fluent_options!
114
122
 
115
123
  @actual = actual
@@ -313,14 +321,8 @@ module Stannum::RSpec
313
321
  end
314
322
 
315
323
  def extra_parameter?
316
- extra_arguments_type =
317
- Stannum::Constraints::Parameters::ExtraArguments::TYPE
318
- extra_keywords_type =
319
- Stannum::Constraints::Parameters::ExtraKeywords::TYPE
320
-
321
324
  return false unless scoped_errors(indexed: true).any? do |error|
322
- error[:type] == extra_arguments_type ||
323
- error[:type] == extra_keywords_type
325
+ EXTRA_PARAMETER_ERROR_TYPES.include?(error[:type])
324
326
  end
325
327
 
326
328
  @failure_reason = :parameter_not_validated
@@ -372,7 +374,7 @@ module Stannum::RSpec
372
374
 
373
375
  def method_parameters
374
376
  @method_parameters ||=
375
- self.class.map_parameters(actual: actual, method_name: method_name)
377
+ self.class.map_parameters(actual:, method_name:)
376
378
  end
377
379
 
378
380
  def mock_validation_handler