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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +49 -0
  3. data/README.md +130 -1200
  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 +13 -13
  15. data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
  16. data/lib/stannum/constraints/hashes.rb +6 -2
  17. data/lib/stannum/constraints/identity.rb +1 -1
  18. data/lib/stannum/constraints/properties/base.rb +124 -0
  19. data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
  20. data/lib/stannum/constraints/properties/match_property.rb +117 -0
  21. data/lib/stannum/constraints/properties/matching.rb +112 -0
  22. data/lib/stannum/constraints/properties.rb +17 -0
  23. data/lib/stannum/constraints/signature.rb +2 -2
  24. data/lib/stannum/constraints/tuples/extra_items.rb +6 -6
  25. data/lib/stannum/constraints/type.rb +4 -4
  26. data/lib/stannum/constraints/types/array_type.rb +2 -2
  27. data/lib/stannum/constraints/types/hash_type.rb +4 -4
  28. data/lib/stannum/constraints/union.rb +1 -1
  29. data/lib/stannum/constraints/uuid.rb +30 -0
  30. data/lib/stannum/constraints.rb +3 -0
  31. data/lib/stannum/contract.rb +7 -7
  32. data/lib/stannum/contracts/array_contract.rb +2 -7
  33. data/lib/stannum/contracts/base.rb +15 -15
  34. data/lib/stannum/contracts/builder.rb +15 -4
  35. data/lib/stannum/contracts/hash_contract.rb +3 -9
  36. data/lib/stannum/contracts/indifferent_hash_contract.rb +15 -2
  37. data/lib/stannum/contracts/map_contract.rb +6 -10
  38. data/lib/stannum/contracts/parameters/arguments_contract.rb +1 -1
  39. data/lib/stannum/contracts/parameters/keywords_contract.rb +1 -1
  40. data/lib/stannum/contracts/parameters/signature_contract.rb +1 -1
  41. data/lib/stannum/contracts/parameters_contract.rb +4 -4
  42. data/lib/stannum/contracts/tuple_contract.rb +6 -6
  43. data/lib/stannum/entities/associations.rb +451 -0
  44. data/lib/stannum/entities/attributes.rb +316 -0
  45. data/lib/stannum/entities/constraints.rb +178 -0
  46. data/lib/stannum/entities/primary_key.rb +148 -0
  47. data/lib/stannum/entities/properties.rb +208 -0
  48. data/lib/stannum/entities.rb +16 -0
  49. data/lib/stannum/entity.rb +87 -0
  50. data/lib/stannum/errors.rb +12 -16
  51. data/lib/stannum/messages/default_strategy.rb +2 -2
  52. data/lib/stannum/parameter_validation.rb +10 -10
  53. data/lib/stannum/rspec/match_errors_matcher.rb +7 -7
  54. data/lib/stannum/rspec/validate_parameter.rb +2 -2
  55. data/lib/stannum/rspec/validate_parameter_matcher.rb +22 -20
  56. data/lib/stannum/schema.rb +117 -76
  57. data/lib/stannum/struct.rb +12 -346
  58. data/lib/stannum/support/optional.rb +1 -1
  59. data/lib/stannum/version.rb +4 -4
  60. data/lib/stannum.rb +6 -0
  61. metadata +26 -85
@@ -4,16 +4,71 @@ require 'stannum'
4
4
  require 'stannum/support/optional'
5
5
 
6
6
  module Stannum
7
- # Data object representing an attribute on a struct.
7
+ # Data object representing an attribute on an entity.
8
8
  class Attribute
9
9
  include Stannum::Support::Optional
10
10
 
11
+ # Builder class for defining attribute methods on an entity.
12
+ class Builder
13
+ # @param schema [Stannum::Schema] the attributes schema on which to define
14
+ # methods.
15
+ def initialize(schema)
16
+ @schema = schema
17
+ end
18
+
19
+ # @return [Stannum::Schema] the attributes schema on which to define
20
+ # methods.
21
+ attr_reader :schema
22
+
23
+ # Defines the reader and writer methods for the attribute.
24
+ #
25
+ # @param attribute [Stannum::Attribute]
26
+ def call(attribute)
27
+ define_reader(attribute)
28
+ define_writer(attribute)
29
+ end
30
+
31
+ private
32
+
33
+ def define_reader(attribute)
34
+ schema.define_method(attribute.reader_name) do
35
+ read_attribute(attribute.name, safe: false)
36
+ end
37
+ end
38
+
39
+ def define_writer(attribute) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
40
+ assoc_name = attribute.association_name
41
+
42
+ schema.define_method(attribute.writer_name) do |value|
43
+ previous_value = read_attribute(attribute.name, safe: false)
44
+
45
+ return if previous_value == value
46
+
47
+ if attribute.foreign_key? && !previous_value.nil?
48
+ self
49
+ .class
50
+ .associations[assoc_name]
51
+ .remove_value(self, previous_value)
52
+ end
53
+
54
+ value = attribute.default_value_for(self) if value.nil?
55
+
56
+ write_attribute(attribute.name, value, safe: false)
57
+ end
58
+ end
59
+ end
60
+
11
61
  # @param name [String, Symbol] The name of the attribute. Converted to a
12
62
  # String.
13
63
  # @param options [Hash, nil] Options for the attribute. Converted to a Hash
14
64
  # with Symbol keys. Defaults to an empty Hash.
15
65
  # @param type [Class, Module, String] The type of the attribute. Can be a
16
66
  # Class, a Module, or the name of a class or module.
67
+ #
68
+ # @option options [Object] :default The default value for the attribute.
69
+ # Defaults to nil.
70
+ # @option options [Boolean] :primary_key true if the attribute represents
71
+ # the primary key for the entity; otherwise false. Defaults to false.
17
72
  def initialize(name:, options:, type:)
18
73
  validate_name(name)
19
74
  validate_options(options)
@@ -35,6 +90,12 @@ module Stannum
35
90
  # @return [String] the name of the attribute type Class or Module.
36
91
  attr_reader :type
37
92
 
93
+ # @return [String] the name of the association if the attribute is a foreign
94
+ # key; otherwise false.
95
+ def association_name
96
+ @options[:association_name]
97
+ end
98
+
38
99
  # @return [Object] the default value for the attribute, if any.
39
100
  def default
40
101
  @options[:default]
@@ -46,6 +107,29 @@ module Stannum
46
107
  !@options[:default].nil?
47
108
  end
48
109
 
110
+ # @param context [Object] the context object used to determinet the default
111
+ # value.
112
+ #
113
+ # @return [Object] the value of the default attribute for the given context
114
+ # object, if any.
115
+ def default_value_for(context)
116
+ return default unless default.is_a?(Proc)
117
+
118
+ default.arity.zero? ? default.call : default.call(context)
119
+ end
120
+
121
+ # @return [Boolean] true if the attribute represents the foreign key for an
122
+ # association; otherwise false.
123
+ def foreign_key?
124
+ !!@options[:foreign_key]
125
+ end
126
+
127
+ # @return [Boolean] true if the attribute represents the primary key for the
128
+ # entity; otherwise false.
129
+ def primary_key?
130
+ !!@options[:primary_key]
131
+ end
132
+
49
133
  # @return [Symbol] the name of the reader method for the attribute.
50
134
  def reader_name
51
135
  @reader_name ||= name.intern
@@ -82,13 +166,7 @@ module Stannum
82
166
  end
83
167
 
84
168
  def validate_name(name)
85
- raise ArgumentError, "name can't be blank" if name.nil?
86
-
87
- unless name.is_a?(String) || name.is_a?(Symbol)
88
- raise ArgumentError, 'name must be a String or Symbol'
89
- end
90
-
91
- raise ArgumentError, "name can't be blank" if name.empty?
169
+ tools.assertions.validate_name(name, as: 'name')
92
170
  end
93
171
 
94
172
  def validate_options(options)
@@ -55,9 +55,7 @@ module Stannum::Constraints
55
55
  #
56
56
  # @return [Stannum::Constraints::Base] the cloned constraint.
57
57
  def clone(freeze: nil)
58
- freeze = true if freeze.nil? && RUBY_VERSION <= '3.0.0'
59
-
60
- super(freeze: freeze).copy_properties(self)
58
+ super.copy_properties(self)
61
59
  end
62
60
 
63
61
  # Checks that the given object does not match the constraint.
@@ -80,7 +78,7 @@ module Stannum::Constraints
80
78
  # or behavior, otherwise true.
81
79
  #
82
80
  # @see #matches?
83
- def does_not_match?(actual)
81
+ def does_not_match?(actual) # rubocop:disable Naming/PredicatePrefix
84
82
  !matches?(actual)
85
83
  end
86
84
 
@@ -118,7 +116,7 @@ module Stannum::Constraints
118
116
  # @see #matches?
119
117
  # @see #negated_errors_for
120
118
  def errors_for(actual, errors: nil) # rubocop:disable Lint/UnusedMethodArgument
121
- (errors || Stannum::Errors.new).add(type, message: message)
119
+ (errors || Stannum::Errors.new).add(type, message:)
122
120
  end
123
121
 
124
122
  # Checks the given object against the constraint and returns errors, if any.
@@ -26,7 +26,7 @@ module Stannum::Constraints
26
26
  def initialize(first, *rest, **options)
27
27
  expected_values = rest.unshift(first)
28
28
 
29
- super(expected_values: expected_values, **options)
29
+ super(expected_values:, **options)
30
30
 
31
31
  @matching_values = Set.new(expected_values)
32
32
  end
@@ -27,7 +27,7 @@ module Stannum::Constraints
27
27
  def initialize(expected_value, **options)
28
28
  @expected_value = expected_value
29
29
 
30
- super(expected_value: expected_value, **options)
30
+ super(expected_value:, **options)
31
31
  end
32
32
 
33
33
  # @return [Object] the expected object.
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints'
4
+
5
+ module Stannum::Constraints
6
+ # A Format constraint asserts the value is a string matching the given format.
7
+ #
8
+ # @example Using a Format constraint with a String format.
9
+ # format = 'Greetings'
10
+ # constraint = Stannum::Constraints::Format.new(format)
11
+ #
12
+ # constraint.matches?(nil) #=> false
13
+ # constraint.matches?('Hello, world') #=> false
14
+ # constraint.matches?('Greetings, programs!') #=> true
15
+ #
16
+ # @example Using a Format constraint with a Regex format.
17
+ # format = /\AGreetings/
18
+ # constraint = Stannum::Constraints::Format.new(format)
19
+ #
20
+ # constraint.matches?(nil) #=> false
21
+ # constraint.matches?('Hello, world') #=> false
22
+ # constraint.matches?('Greetings, programs!') #=> true
23
+ class Format < Stannum::Constraints::Base
24
+ # The :type of the error generated for a matching object.
25
+ NEGATED_TYPE = 'stannum.constraints.matches_format'
26
+
27
+ # The :type of the error generated for a non-matching object.
28
+ TYPE = 'stannum.constraints.does_not_match_format'
29
+
30
+ # @param expected_format [Regex, String] The expected object.
31
+ # @param options [Hash<Symbol, Object>] Configuration options for the
32
+ # constraint. Defaults to an empty Hash.
33
+ def initialize(expected_format, **options)
34
+ @expected_format = expected_format
35
+
36
+ super(expected_format:, **options)
37
+ end
38
+
39
+ # @return [Regex, String] the expected format.
40
+ attr_reader :expected_format
41
+
42
+ # (see Stannum::Constraints::Base#errors_for)
43
+ def errors_for(actual, errors: nil)
44
+ return super if type_constraint.matches?(actual)
45
+
46
+ type_constraint.errors_for(actual, errors:)
47
+ end
48
+
49
+ # Checks that the object is a string with the expected format.
50
+ #
51
+ # @return [true, false] true if the object is a string with the expected
52
+ # format, otherwise false.
53
+ #
54
+ # @see Stannum::Constraint#matches?
55
+ def matches?(actual)
56
+ return false unless type_constraint.matches?(actual)
57
+
58
+ if expected_format.is_a?(String)
59
+ actual.include?(expected_format)
60
+ else
61
+ actual.match?(expected_format)
62
+ end
63
+ end
64
+ alias match? matches?
65
+
66
+ private
67
+
68
+ def type_constraint
69
+ @type_constraint ||= Stannum::Constraints::Type.new(String)
70
+ end
71
+ end
72
+ end
@@ -6,12 +6,17 @@ require 'stannum/support/coercion'
6
6
  module Stannum::Constraints::Hashes
7
7
  # Constraint for validating the keys of a hash-like object.
8
8
  #
9
+ # When using this constraint, the keys must be strings or symbols, and the
10
+ # hash keys must be of the same type. A constraint configured with string keys
11
+ # will not match a hash with symbol keys, and vice versa.
12
+ #
9
13
  # @example
10
- # keys = %[fuel mass size]
14
+ # keys = %i[fuel mass size]
11
15
  # constraint = Stannum::Constraints::Hashes::ExpectedKeys.new(keys)
12
16
  #
13
17
  # constraint.matches?({}) #=> true
14
18
  # constraint.matches?({ fuel: 'Monopropellant' }) #=> true
19
+ # constraint.matches?({ 'fuel' => 'Monopropellant' }) #=> false
15
20
  # constraint.matches?({ electric: true, fuel: 'Xenon' }) #=> false
16
21
  # constraint.matches?({ fuel: 'LF/O', mass: '1 ton', size: 'Medium' })
17
22
  # #=> true
@@ -33,19 +38,14 @@ module Stannum::Constraints::Hashes
33
38
  def initialize(expected_keys, **options)
34
39
  validate_expected_keys(expected_keys)
35
40
 
36
- expected_keys =
37
- if expected_keys.is_a?(Array)
38
- Set.new(expected_keys)
39
- else
40
- expected_keys
41
- end
41
+ expected_keys = Set.new(expected_keys) if expected_keys.is_a?(Array)
42
42
 
43
- super(expected_keys: expected_keys, **options)
43
+ super(expected_keys:, **options)
44
44
  end
45
45
 
46
46
  # @return [true, false] true if the object responds to #[] and #keys and the
47
47
  # object has at least one key that is not in expected_keys.
48
- def does_not_match?(actual)
48
+ def does_not_match?(actual) # rubocop:disable Naming/PredicatePrefix
49
49
  return false unless hash?(actual)
50
50
 
51
51
  !(Set.new(actual.keys) <= expected_keys) # rubocop:disable Style/InverseMethods
@@ -56,19 +56,19 @@ module Stannum::Constraints::Hashes
56
56
  errors ||= Stannum::Errors.new
57
57
 
58
58
  unless actual.respond_to?(:keys)
59
- return add_invalid_hash_error(actual: actual, errors: errors)
59
+ return add_invalid_hash_error(actual:, errors:)
60
60
  end
61
61
 
62
62
  each_extra_key(actual) do |key, value|
63
63
  key = Stannum::Support::Coercion.error_key(key)
64
64
 
65
- errors[key].add(type, value: value)
65
+ errors[key].add(type, value:)
66
66
  end
67
67
 
68
68
  errors
69
69
  end
70
70
 
71
- # @return [Array] the expected keys.
71
+ # @return [Set] the expected keys.
72
72
  def expected_keys
73
73
  keys = options[:expected_keys]
74
74
 
@@ -91,7 +91,7 @@ module Stannum::Constraints::Hashes
91
91
  def add_invalid_hash_error(actual:, errors:)
92
92
  Stannum::Constraints::Signature
93
93
  .new(:keys)
94
- .errors_for(actual, errors: errors)
94
+ .errors_for(actual, errors:)
95
95
  end
96
96
 
97
97
  def each_extra_key(actual)
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/hashes'
4
+ require 'stannum/constraints/hashes/extra_keys'
5
+
6
+ module Stannum::Constraints::Hashes
7
+ # Constraint for validating the keys of an indifferent hash-like object.
8
+ #
9
+ # When using this constraint, the keys must be strings or symbols, but it does
10
+ # not matter which - a constraint configured with string keys will match a
11
+ # hash with symbol keys, and vice versa.
12
+ #
13
+ # @example
14
+ # keys = %i[fuel mass size]
15
+ # constraint = Stannum::Constraints::Hashes::ExpectedKeys.new(keys)
16
+ #
17
+ # constraint.matches?({}) #=> true
18
+ # constraint.matches?({ fuel: 'Monopropellant' }) #=> true
19
+ # constraint.matches?({ 'fuel' => 'Monopropellant' }) #=> true
20
+ # constraint.matches?({ electric: true, fuel: 'Xenon' }) #=> false
21
+ # constraint.matches?({ fuel: 'LF/O', mass: '1 ton', size: 'Medium' })
22
+ # #=> true
23
+ # constraint.matches?(
24
+ # { fuel: 'LF', mass: '2 tons', nuclear: true, size: 'Medium' }
25
+ # )
26
+ # #=> false
27
+ class IndifferentExtraKeys < Stannum::Constraints::Hashes::ExtraKeys
28
+ # @return [Set] the expected keys.
29
+ def expected_keys
30
+ keys = options[:expected_keys]
31
+
32
+ return indifferent_keys_for(keys) unless keys.is_a?(Proc)
33
+
34
+ indifferent_keys_for(keys.call)
35
+ end
36
+
37
+ private
38
+
39
+ def indifferent_keys_for(keys)
40
+ Set.new(
41
+ keys.reduce([]) do |ary, key|
42
+ ary << key.to_s << key.intern
43
+ end
44
+ )
45
+ end
46
+ end
47
+ end
@@ -5,7 +5,11 @@ require 'stannum/constraints'
5
5
  module Stannum::Constraints
6
6
  # Namespace for Hash-specific constraints.
7
7
  module Hashes
8
- autoload :ExtraKeys, 'stannum/constraints/hashes/extra_keys'
9
- autoload :IndifferentKey, 'stannum/constraints/hashes/indifferent_key'
8
+ autoload :ExtraKeys,
9
+ 'stannum/constraints/hashes/extra_keys'
10
+ autoload :IndifferentExtraKeys,
11
+ 'stannum/constraints/hashes/indifferent_extra_keys'
12
+ autoload :IndifferentKey,
13
+ 'stannum/constraints/hashes/indifferent_key'
10
14
  end
11
15
  end
@@ -26,7 +26,7 @@ module Stannum::Constraints
26
26
  def initialize(expected_value, **options)
27
27
  @expected_value = expected_value
28
28
 
29
- super(expected_value: expected_value, **options)
29
+ super(expected_value:, **options)
30
30
  end
31
31
 
32
32
  # @return [Object] the expected object.
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbelt'
4
+
5
+ require 'stannum/constraints/properties'
6
+
7
+ module Stannum::Constraints::Properties
8
+ # Abstract base class for property constraints.
9
+ class Base < Stannum::Constraints::Base
10
+ # Default parameter names to filter out of errors.
11
+ FILTERED_PARAMETERS = %i[
12
+ passw
13
+ secret
14
+ token
15
+ _key
16
+ crypt
17
+ salt
18
+ certificate
19
+ otp
20
+ ssn
21
+ ].freeze
22
+
23
+ # @param property_names [Array<String, Symbol>] the name or names of the
24
+ # properties to match.
25
+ # @param options [Hash<Symbol, Object>] configuration options for the
26
+ # constraint. Defaults to an empty Hash.
27
+ #
28
+ # @option options allow_empty [true, false] if true, will match against an
29
+ # object with empty property values, such as an empty string.
30
+ # @option options allow_nil [true, false] if true, will match against an
31
+ # object with nil property values.
32
+ def initialize(*property_names, **options)
33
+ @property_names = property_names
34
+
35
+ validate_property_names
36
+
37
+ super(
38
+ allow_empty: !!options[:allow_empty],
39
+ allow_nil: !!options[:allow_nil],
40
+ property_names:,
41
+ **options
42
+ )
43
+ end
44
+
45
+ # @return [Array<String, Symbol>] the name or names of the properties to
46
+ # match.
47
+ attr_reader :property_names
48
+
49
+ # @return [true, false] if true, will match against an object with empty
50
+ # property values, such as an empty string.
51
+ def allow_empty?
52
+ options[:allow_empty]
53
+ end
54
+
55
+ # @return [true, false] if true, will match against an object with nil
56
+ # property values.
57
+ def allow_nil?
58
+ options[:allow_nil]
59
+ end
60
+
61
+ private
62
+
63
+ def can_match_properties?(actual)
64
+ actual.respond_to?(:[])
65
+ end
66
+
67
+ def each_property(actual)
68
+ return to_enum(__method__, actual) unless block_given?
69
+
70
+ property_names.each do |property_name|
71
+ yield property_name, actual[property_name]
72
+ end
73
+ end
74
+
75
+ def empty?(value)
76
+ value.respond_to?(:empty?) && value.empty?
77
+ end
78
+
79
+ def filter_parameters?
80
+ return @filter_parameters unless @filter_parameters.nil?
81
+
82
+ filters = filtered_parameters.map { |param| Regexp.new(param.to_s) }
83
+
84
+ @filter_parameters =
85
+ property_names.any? do |property_name|
86
+ filters.any? { |filter| filter.match?(property_name.to_s) }
87
+ end
88
+ end
89
+
90
+ def filtered_parameters
91
+ return Rails.configuration.filter_parameters if defined?(Rails)
92
+
93
+ FILTERED_PARAMETERS
94
+ end
95
+
96
+ def invalid_object_errors(errors)
97
+ errors.add(
98
+ Stannum::Constraints::Signature::TYPE,
99
+ methods: %i[[]],
100
+ missing: %i[[]]
101
+ )
102
+ end
103
+
104
+ def skip_property?(value)
105
+ (allow_empty? && empty?(value)) || (allow_nil? && value.nil?)
106
+ end
107
+
108
+ def tools
109
+ SleepingKingStudios::Tools::Toolbelt.instance
110
+ end
111
+
112
+ def validate_property_names
113
+ if property_names.empty?
114
+ raise ArgumentError, "property names can't be empty"
115
+ end
116
+
117
+ property_names.each.with_index do |property_name, index|
118
+ tools
119
+ .assertions
120
+ .validate_name(property_name, as: "property name at #{index}")
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/properties'
4
+ require 'stannum/constraints/properties/matching'
5
+
6
+ module Stannum::Constraints::Properties
7
+ # Compares the properties of the given object with the specified property.
8
+ #
9
+ # If none of the property values equal the expected value, the constraint will
10
+ # match the object; otherwise, if there are any matching values, the
11
+ # constraint will not match.
12
+ #
13
+ # @example Using an Properties::Match constraint
14
+ # UpdatePassword = Struct.new(:old_password, :new_password)
15
+ # constraint = Stannum::Constraints::Properties::DoNotMatchProperty.new(
16
+ # :old_password,
17
+ # :new_password
18
+ # )
19
+ #
20
+ # params = UpdatePassword.new('tronlives', 'ifightfortheusers')
21
+ # constraint.matches?(params)
22
+ # #=> true
23
+ #
24
+ # params = UpdatePassword.new('tronlives', 'tronlives')
25
+ # constraint.matches?(params)
26
+ # #=> false
27
+ # constraint.errors_for(params)
28
+ # #=> [
29
+ # {
30
+ # path: [:confirmation],
31
+ # type: 'stannum.constraints.is_equal_to',
32
+ # data: { expected: '[FILTERED]', actual: '[FILTERED]' }
33
+ # }
34
+ # ]
35
+ class DoNotMatchProperty < Stannum::Constraints::Properties::Matching
36
+ # The :type of the error generated for a matching object.
37
+ NEGATED_TYPE = Stannum::Constraints::Equality::TYPE
38
+
39
+ # The :type of the error generated for a non-matching object.
40
+ TYPE = Stannum::Constraints::Equality::NEGATED_TYPE
41
+
42
+ # @return [true, false] true if the property values match the reference
43
+ # property value; otherwise false.
44
+ def does_not_match?(actual) # rubocop:disable Naming/PredicatePrefix
45
+ return false unless can_match_properties?(actual)
46
+
47
+ expected = expected_value(actual)
48
+
49
+ return false if skip_property?(expected)
50
+
51
+ each_non_matching_property(
52
+ actual:,
53
+ expected:,
54
+ include_all: true
55
+ )
56
+ .none?
57
+ end
58
+
59
+ # (see Stannum::Constraints::Base#errors_for)
60
+ def errors_for(actual, errors: nil)
61
+ errors ||= Stannum::Errors.new
62
+
63
+ return invalid_object_errors(errors) unless can_match_properties?(actual)
64
+
65
+ expected = expected_value(actual)
66
+ matching = each_matching_property(actual:, expected:)
67
+
68
+ return generic_errors(errors) if matching.none?
69
+
70
+ matching.each do |property_name, _| # rubocop:disable Style/HashEachMethods
71
+ errors[property_name].add(type, message:)
72
+ end
73
+
74
+ errors
75
+ end
76
+
77
+ # @return [true, false] false if any of the property values match the
78
+ # reference property value; otherwise true.
79
+ def matches?(actual)
80
+ return false unless can_match_properties?(actual)
81
+
82
+ expected = expected_value(actual)
83
+
84
+ return true if skip_property?(expected)
85
+
86
+ each_matching_property(actual:, expected:).none?
87
+ end
88
+ alias match? matches?
89
+
90
+ # (see Stannum::Constraints::Base#negated_errors_for)
91
+ def negated_errors_for(actual, errors: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
92
+ errors ||= Stannum::Errors.new
93
+
94
+ return invalid_object_errors(errors) unless can_match_properties?(actual)
95
+
96
+ expected = expected_value(actual)
97
+ matching = each_non_matching_property(
98
+ actual:,
99
+ expected:,
100
+ include_all: true
101
+ )
102
+
103
+ return generic_errors(errors) if matching.none?
104
+
105
+ matching.each do |property_name, value|
106
+ errors[property_name].add(
107
+ negated_type,
108
+ message: negated_message,
109
+ expected: filter_parameters? ? '[FILTERED]' : expected_value(actual),
110
+ actual: filter_parameters? ? '[FILTERED]' : value
111
+ )
112
+ end
113
+
114
+ errors
115
+ end
116
+ end
117
+ end