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
data/lib/stannum/attribute.rb
CHANGED
@@ -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
|
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
|
-
|
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
|
-
|
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:
|
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
|
29
|
+
super(expected_values:, **options)
|
30
30
|
|
31
31
|
@matching_values = Set.new(expected_values)
|
32
32
|
end
|
@@ -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
|
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
|
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:
|
65
|
+
errors[key].add(type, value:)
|
66
66
|
end
|
67
67
|
|
68
68
|
errors
|
69
69
|
end
|
70
70
|
|
71
|
-
# @return [
|
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:
|
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,
|
9
|
-
|
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
|
@@ -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
|