stannum 0.1.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 +7 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/DEVELOPMENT.md +105 -0
- data/LICENSE +22 -0
- data/README.md +1327 -0
- data/config/locales/en.rb +47 -0
- data/lib/stannum/attribute.rb +115 -0
- data/lib/stannum/constraint.rb +65 -0
- data/lib/stannum/constraints/absence.rb +42 -0
- data/lib/stannum/constraints/anything.rb +28 -0
- data/lib/stannum/constraints/base.rb +285 -0
- data/lib/stannum/constraints/boolean.rb +33 -0
- data/lib/stannum/constraints/delegator.rb +71 -0
- data/lib/stannum/constraints/enum.rb +64 -0
- data/lib/stannum/constraints/equality.rb +47 -0
- data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
- data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
- data/lib/stannum/constraints/hashes.rb +11 -0
- data/lib/stannum/constraints/identity.rb +46 -0
- data/lib/stannum/constraints/nothing.rb +28 -0
- data/lib/stannum/constraints/presence.rb +42 -0
- data/lib/stannum/constraints/signature.rb +92 -0
- data/lib/stannum/constraints/signatures/map.rb +17 -0
- data/lib/stannum/constraints/signatures/tuple.rb +17 -0
- data/lib/stannum/constraints/signatures.rb +11 -0
- data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
- data/lib/stannum/constraints/tuples.rb +10 -0
- data/lib/stannum/constraints/type.rb +113 -0
- data/lib/stannum/constraints/types/array_type.rb +148 -0
- data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
- data/lib/stannum/constraints/types/date_time_type.rb +16 -0
- data/lib/stannum/constraints/types/date_type.rb +16 -0
- data/lib/stannum/constraints/types/float_type.rb +14 -0
- data/lib/stannum/constraints/types/hash_type.rb +205 -0
- data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
- data/lib/stannum/constraints/types/integer_type.rb +14 -0
- data/lib/stannum/constraints/types/nil_type.rb +20 -0
- data/lib/stannum/constraints/types/proc_type.rb +14 -0
- data/lib/stannum/constraints/types/string_type.rb +14 -0
- data/lib/stannum/constraints/types/symbol_type.rb +14 -0
- data/lib/stannum/constraints/types/time_type.rb +14 -0
- data/lib/stannum/constraints/types.rb +25 -0
- data/lib/stannum/constraints/union.rb +85 -0
- data/lib/stannum/constraints.rb +26 -0
- data/lib/stannum/contract.rb +243 -0
- data/lib/stannum/contracts/array_contract.rb +108 -0
- data/lib/stannum/contracts/base.rb +597 -0
- data/lib/stannum/contracts/builder.rb +72 -0
- data/lib/stannum/contracts/definition.rb +74 -0
- data/lib/stannum/contracts/hash_contract.rb +136 -0
- data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
- data/lib/stannum/contracts/map_contract.rb +199 -0
- data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
- data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
- data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
- data/lib/stannum/contracts/parameters.rb +15 -0
- data/lib/stannum/contracts/parameters_contract.rb +530 -0
- data/lib/stannum/contracts/tuple_contract.rb +213 -0
- data/lib/stannum/contracts.rb +19 -0
- data/lib/stannum/errors.rb +730 -0
- data/lib/stannum/messages/default_strategy.rb +124 -0
- data/lib/stannum/messages.rb +25 -0
- data/lib/stannum/parameter_validation.rb +216 -0
- data/lib/stannum/rspec/match_errors.rb +17 -0
- data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
- data/lib/stannum/rspec/validate_parameter.rb +23 -0
- data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
- data/lib/stannum/rspec.rb +8 -0
- data/lib/stannum/schema.rb +131 -0
- data/lib/stannum/struct.rb +444 -0
- data/lib/stannum/support/coercion.rb +114 -0
- data/lib/stannum/support/optional.rb +69 -0
- data/lib/stannum/support.rb +8 -0
- data/lib/stannum/version.rb +57 -0
- data/lib/stannum.rb +27 -0
- metadata +216 -0
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
{
|
4
|
+
en: {
|
5
|
+
stannum: {
|
6
|
+
constraints: {
|
7
|
+
absent: 'is nil or empty',
|
8
|
+
anything: 'is a value',
|
9
|
+
does_not_have_methods: 'does not respond to the methods',
|
10
|
+
has_methods: 'responds to the methods',
|
11
|
+
hashes: {
|
12
|
+
extra_keys: 'has extra keys',
|
13
|
+
is_not_string_or_symbol: 'is not a String or a Symbol',
|
14
|
+
is_string_or_symbol: 'is a String or a Symbol',
|
15
|
+
no_extra_keys: 'does not have extra keys'
|
16
|
+
},
|
17
|
+
invalid: 'is invalid',
|
18
|
+
is_boolean: 'is true or false',
|
19
|
+
is_in_list: 'is in the list',
|
20
|
+
is_in_union: 'matches one of the constraints',
|
21
|
+
is_equal_to: 'is equal to',
|
22
|
+
is_not_boolean: 'is not true or false',
|
23
|
+
is_not_equal_to: 'is not equal to',
|
24
|
+
is_not_in_list: 'is not in the list',
|
25
|
+
is_not_in_union: 'does not match any of the constraints',
|
26
|
+
is_not_type: ->(_type, data) { "is not a #{data[:type]}" },
|
27
|
+
is_not_value: 'is not the expected value',
|
28
|
+
is_type: ->(_type, data) { "is a #{data[:type]}" },
|
29
|
+
is_value: 'is the expected value',
|
30
|
+
parameters: {
|
31
|
+
extra_arguments: 'has extra arguments',
|
32
|
+
extra_keywords: 'has extra keywords'
|
33
|
+
},
|
34
|
+
tuples: {
|
35
|
+
extra_items: 'has extra items',
|
36
|
+
no_extra_items: 'does not have extra items'
|
37
|
+
},
|
38
|
+
types: {
|
39
|
+
is_nil: 'is nil',
|
40
|
+
is_not_nil: 'is not nil'
|
41
|
+
},
|
42
|
+
present: 'is not nil or empty',
|
43
|
+
valid: 'is valid'
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
47
|
+
}
|
@@ -0,0 +1,115 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum'
|
4
|
+
require 'stannum/support/optional'
|
5
|
+
|
6
|
+
module Stannum
|
7
|
+
# Data object representing an attribute on a struct.
|
8
|
+
class Attribute
|
9
|
+
include Stannum::Support::Optional
|
10
|
+
|
11
|
+
# @param name [String, Symbol] The name of the attribute. Converted to a
|
12
|
+
# String.
|
13
|
+
# @param options [Hash, nil] Options for the attribute. Converted to a Hash
|
14
|
+
# with Symbol keys. Defaults to an empty Hash.
|
15
|
+
# @param type [Class, Module, String] The type of the attribute. Can be a
|
16
|
+
# Class, a Module, or the name of a class or module.
|
17
|
+
def initialize(name:, options:, type:)
|
18
|
+
validate_name(name)
|
19
|
+
validate_options(options)
|
20
|
+
validate_type(type)
|
21
|
+
|
22
|
+
@name = name.to_s
|
23
|
+
@options = tools.hash_tools.convert_keys_to_symbols(options || {})
|
24
|
+
@options = resolve_required_option(**@options)
|
25
|
+
|
26
|
+
@type, @resolved_type = resolve_type(type)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [String] the name of the attribute.
|
30
|
+
attr_reader :name
|
31
|
+
|
32
|
+
# @return [Hash] the attribute options.
|
33
|
+
attr_reader :options
|
34
|
+
|
35
|
+
# @return [String] the name of the attribute type Class or Module.
|
36
|
+
attr_reader :type
|
37
|
+
|
38
|
+
# @return [Object] the default value for the attribute, if any.
|
39
|
+
def default
|
40
|
+
@options[:default]
|
41
|
+
end
|
42
|
+
|
43
|
+
# @return [Boolean] true if the attribute has a default value; otherwise
|
44
|
+
# false.
|
45
|
+
def default?
|
46
|
+
!@options[:default].nil?
|
47
|
+
end
|
48
|
+
|
49
|
+
# @return [Symbol] the name of the reader method for the attribute.
|
50
|
+
def reader_name
|
51
|
+
@reader_name ||= name.intern
|
52
|
+
end
|
53
|
+
|
54
|
+
# @return [Module] the type of the attribute.
|
55
|
+
def resolved_type
|
56
|
+
return @resolved_type if @resolved_type
|
57
|
+
|
58
|
+
@resolved_type = Object.const_get(type)
|
59
|
+
|
60
|
+
unless @resolved_type.is_a?(Module)
|
61
|
+
raise NameError, "constant #{type} is not a Class or Module"
|
62
|
+
end
|
63
|
+
|
64
|
+
@resolved_type
|
65
|
+
end
|
66
|
+
|
67
|
+
# @return [Symbol] the name of the writer method for the attribute.
|
68
|
+
def writer_name
|
69
|
+
@writer_name ||= :"#{name}="
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def resolve_type(type)
|
75
|
+
return [type, nil] if type.is_a?(String)
|
76
|
+
|
77
|
+
[type.to_s, type]
|
78
|
+
end
|
79
|
+
|
80
|
+
def tools
|
81
|
+
SleepingKingStudios::Tools::Toolbelt.instance
|
82
|
+
end
|
83
|
+
|
84
|
+
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?
|
92
|
+
end
|
93
|
+
|
94
|
+
def validate_options(options)
|
95
|
+
return if options.nil? || options.is_a?(Hash)
|
96
|
+
|
97
|
+
raise ArgumentError, 'options must be a Hash or nil'
|
98
|
+
end
|
99
|
+
|
100
|
+
def validate_type(type)
|
101
|
+
raise ArgumentError, "type can't be blank" if type.nil?
|
102
|
+
|
103
|
+
return if type.is_a?(Module)
|
104
|
+
|
105
|
+
if type.is_a?(String)
|
106
|
+
return unless type.empty?
|
107
|
+
|
108
|
+
raise ArgumentError, "type can't be blank"
|
109
|
+
end
|
110
|
+
|
111
|
+
raise ArgumentError,
|
112
|
+
'type must be a Class, a Module, or the name of a class or module'
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/base'
|
4
|
+
|
5
|
+
module Stannum
|
6
|
+
# Constraint class for defining a custom or one-off constraint instance.
|
7
|
+
#
|
8
|
+
# The Stannum::Constraint class allows you to define a constraint instance
|
9
|
+
# with a block, and optionally a type and negated type when generating errors
|
10
|
+
# for non-matching objects.
|
11
|
+
#
|
12
|
+
# If your use case is more complicated, such as a constraint with multiple
|
13
|
+
# expectations and thus different errors depending on the given object, use
|
14
|
+
# a subclass of the Stannum::Constraints::Base class instead. For example, an
|
15
|
+
# is_odd constraint that checks if an object is an odd integer might have
|
16
|
+
# different errors when passed a non-integer object and when passed an even
|
17
|
+
# integer, even though both are failing matches.
|
18
|
+
#
|
19
|
+
# Likewise, if you want to define a custom constraint class, it is recommended
|
20
|
+
# that you use Stannum::Constraints::Base as the base class for all but the
|
21
|
+
# simplest constraints.
|
22
|
+
#
|
23
|
+
# @example Defining a Custom Constraint
|
24
|
+
# is_integer = Stannum::Constraint.new { |actual| actual.is_a?(Integer) }
|
25
|
+
# is_integer.matches?(nil) #=> false
|
26
|
+
# is_integer.matches?(3) #=> true
|
27
|
+
# is_integer.matches?(3.5) #=> false
|
28
|
+
#
|
29
|
+
# @example Defining a Custom Constraint With Errors
|
30
|
+
# is_even_integer = Stannum::Constraint.new(
|
31
|
+
# negated_type: 'examples.an_even_integer',
|
32
|
+
# type: 'examples.not_an_even_integer'
|
33
|
+
# ) { |actual| actual.is_a?(Integer) && actual.even? }
|
34
|
+
#
|
35
|
+
# is_even_integer.matches?(nil) #=> false
|
36
|
+
# is_even_integer.matches?(2) #=> true
|
37
|
+
# is_even_integer.matches?(3) #=> false
|
38
|
+
#
|
39
|
+
# @see Stannum::Constraints::Base
|
40
|
+
class Constraint < Stannum::Constraints::Base
|
41
|
+
# @overload initialize(**options)
|
42
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
43
|
+
# constraint. Defaults to an empty Hash.
|
44
|
+
#
|
45
|
+
# @yield The definition for the constraint. Each time #matches? is called
|
46
|
+
# for this constraint, the given object will be passed to this block and
|
47
|
+
# the result of the block will be returned.
|
48
|
+
# @yieldparam actual [Object] The object to check against the constraint.
|
49
|
+
# @yieldreturn [true, false] true if the given object matches the
|
50
|
+
# constraint, otherwise false.
|
51
|
+
#
|
52
|
+
# @see #matches?
|
53
|
+
def initialize(**options, &block)
|
54
|
+
@definition = block
|
55
|
+
|
56
|
+
super(**options)
|
57
|
+
end
|
58
|
+
|
59
|
+
# (see Stannum::Constraints::Base#matches?)
|
60
|
+
def matches?(actual)
|
61
|
+
@definition ? @definition.call(actual) : super
|
62
|
+
end
|
63
|
+
alias match? matches?
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/base'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# An absence constraint asserts that the object is nil or empty.
|
7
|
+
#
|
8
|
+
# @example Using an Absence constraint
|
9
|
+
# constraint = Stannum::Constraints::Absence.new
|
10
|
+
#
|
11
|
+
# constraint.matches?(nil) #=> true
|
12
|
+
# constraint.matches?(Object.new) #=> false
|
13
|
+
#
|
14
|
+
# @example Using a Absence constraint with an Array
|
15
|
+
# constraint.matches?([]) #=> true
|
16
|
+
# constraint.matches?([1, 2, 3]) #=> false
|
17
|
+
#
|
18
|
+
# @example Using a Absence constraint with an Hash
|
19
|
+
# constraint.matches?({}) #=> true
|
20
|
+
# constraint.matches?({ key: 'value' }) #=> false
|
21
|
+
class Absence < Stannum::Constraints::Base
|
22
|
+
# The :type of the error generated for a matching object.
|
23
|
+
NEGATED_TYPE = Stannum::Constraints::Presence::TYPE
|
24
|
+
|
25
|
+
# The :type of the error generated for a non-matching object.
|
26
|
+
TYPE = Stannum::Constraints::Presence::NEGATED_TYPE
|
27
|
+
|
28
|
+
# Checks that the object is nil or empty.
|
29
|
+
#
|
30
|
+
# @return [true, false] true if the object is nil or empty, otherwise false.
|
31
|
+
#
|
32
|
+
# @see Stannum::Constraint#matches?
|
33
|
+
def matches?(actual)
|
34
|
+
return true if actual.nil?
|
35
|
+
|
36
|
+
return true if actual.respond_to?(:empty?) && actual.empty?
|
37
|
+
|
38
|
+
false
|
39
|
+
end
|
40
|
+
alias match? matches?
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/base'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# An example constraint that matches any object, even nil.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# constraint = Stannum::Constraints::Anything.new
|
10
|
+
# constraint.matches?(Object.new)
|
11
|
+
# #=> true
|
12
|
+
# constraint.does_not_match?(Object.new)
|
13
|
+
# #=> false
|
14
|
+
class Anything < Stannum::Constraints::Base
|
15
|
+
# The :type of the error generated for a matching object.
|
16
|
+
NEGATED_TYPE = 'stannum.constraints.anything'
|
17
|
+
|
18
|
+
# Returns true for all objects.
|
19
|
+
#
|
20
|
+
# @return [true] in all cases.
|
21
|
+
#
|
22
|
+
# @see Stannum::Constraint#matches?
|
23
|
+
def matches?(_actual)
|
24
|
+
true
|
25
|
+
end
|
26
|
+
alias match? matches?
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,285 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# A constraint codifies a particular expectation about an object.
|
7
|
+
class Base
|
8
|
+
# Builder class for defining constraints for a Contract.
|
9
|
+
#
|
10
|
+
# This class should not be invoked directly. Instead, pass a block to the
|
11
|
+
# constructor for Contract.
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
class Builder < Stannum::Contracts::Builder
|
15
|
+
end
|
16
|
+
|
17
|
+
# The :type of the error generated for a matching object.
|
18
|
+
NEGATED_TYPE = 'stannum.constraints.valid'
|
19
|
+
|
20
|
+
# The :type of the error generated for a non-matching object.
|
21
|
+
TYPE = 'stannum.constraints.invalid'
|
22
|
+
|
23
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
24
|
+
# constraint. Defaults to an empty Hash.
|
25
|
+
# @option options [String] :message The default error message generated for
|
26
|
+
# a non-matching object.
|
27
|
+
# @option options [String] :negated_message The default error message
|
28
|
+
# generated for a matching object.
|
29
|
+
# @option options [String] :negated_type The type of the error generated for
|
30
|
+
# a matching object.
|
31
|
+
# @option options [String] :type The type of the error generated for a
|
32
|
+
# non-matching object.
|
33
|
+
def initialize(**options)
|
34
|
+
self.options = options
|
35
|
+
end
|
36
|
+
|
37
|
+
# @return [Hash<Symbol, Object>] Configuration options for the constraint.
|
38
|
+
attr_reader :options
|
39
|
+
|
40
|
+
# Performs an equality comparison.
|
41
|
+
#
|
42
|
+
# @param other [Object] The object to compare.
|
43
|
+
#
|
44
|
+
# @return [true, false] true if the other object has the same class and
|
45
|
+
# options; otherwise false.
|
46
|
+
def ==(other)
|
47
|
+
other.class == self.class && options == other.options
|
48
|
+
end
|
49
|
+
|
50
|
+
# Produces a shallow copy of the constraint.
|
51
|
+
#
|
52
|
+
# @param freeze [true, false, nil] If true or false, sets the frozen status
|
53
|
+
# of the cloned constraint; otherwise, copies the frozen status of the
|
54
|
+
# original. Defaults to nil.
|
55
|
+
#
|
56
|
+
# @return [Stannum::Constraints::Base] the cloned constraint.
|
57
|
+
def clone(freeze: nil)
|
58
|
+
freeze = true if freeze.nil? && RUBY_VERSION <= '3.0.0'
|
59
|
+
|
60
|
+
super(freeze: freeze).copy_properties(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Checks that the given object does not match the constraint.
|
64
|
+
#
|
65
|
+
# @example Checking a matching object.
|
66
|
+
# constraint = CustomConstraint.new
|
67
|
+
# object = MatchingObject.new
|
68
|
+
#
|
69
|
+
# constraint.does_not_match?(object) #=> false
|
70
|
+
#
|
71
|
+
# @example Checking a non-matching object.
|
72
|
+
# constraint = CustomConstraint.new
|
73
|
+
# object = NonMatchingObject.new
|
74
|
+
#
|
75
|
+
# constraint.does_not_match?(object) #=> true
|
76
|
+
#
|
77
|
+
# @return [true, false] false if the object matches the expected properties
|
78
|
+
# or behavior, otherwise true.
|
79
|
+
#
|
80
|
+
# @see #matches?
|
81
|
+
def does_not_match?(actual)
|
82
|
+
!matches?(actual)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Produces a shallow copy of the constraint.
|
86
|
+
#
|
87
|
+
# @return [Stannum::Constraints::Base] the duplicated constraint.
|
88
|
+
def dup
|
89
|
+
super.copy_properties(self)
|
90
|
+
end
|
91
|
+
|
92
|
+
# Generates an errors object for the given object.
|
93
|
+
#
|
94
|
+
# The errors object represents the difference between the given object and
|
95
|
+
# the expected properties or behavior. It may be the same for all objects,
|
96
|
+
# or different based on the details of the object or the constraint.
|
97
|
+
#
|
98
|
+
# @param actual [Object] The object to generate errors for.
|
99
|
+
# @param errors [Stannum::Errors] The errors object to append errors to. If
|
100
|
+
# an errors object is not given, a new errors object will be created.
|
101
|
+
#
|
102
|
+
# @example Generating errors for a non-matching object.
|
103
|
+
# constraint = CustomConstraint.new
|
104
|
+
# object = NonMatchingObject.new
|
105
|
+
# errors = constraint.errors_for(object)
|
106
|
+
#
|
107
|
+
# errors.class #=> Stannum::Errors
|
108
|
+
# errors.to_a #=> [{ type: 'some_error', message: 'some error message' }]
|
109
|
+
#
|
110
|
+
# @note This method should only be called for an object that does not match
|
111
|
+
# the constraint. Generating errors for a matching object can result in
|
112
|
+
# undefined behavior.
|
113
|
+
#
|
114
|
+
# @return [Stannum::Errors] the given or generated errors object.
|
115
|
+
#
|
116
|
+
# @see #matches?
|
117
|
+
# @see #negated_errors_for
|
118
|
+
def errors_for(actual, errors: nil) # rubocop:disable Lint/UnusedMethodArgument
|
119
|
+
(errors || Stannum::Errors.new).add(type, message: message)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Checks the given object against the constraint and returns errors, if any.
|
123
|
+
#
|
124
|
+
# This method checks the given object against the expected properties or
|
125
|
+
# behavior. If the object matches the constraint, #match will return true.
|
126
|
+
# If the object does not match the constraint, #match will return false and
|
127
|
+
# the generated errors for that object.
|
128
|
+
#
|
129
|
+
# @example Checking a matching object.
|
130
|
+
# constraint = CustomConstraint.new
|
131
|
+
# object = MatchingObject.new
|
132
|
+
#
|
133
|
+
# success, errors = constraint.match(object)
|
134
|
+
# success #=> true
|
135
|
+
# errors #=> nil
|
136
|
+
#
|
137
|
+
# @example Checking a non-matching object.
|
138
|
+
# constraint = CustomConstraint.new
|
139
|
+
# object = NonMatchingObject.new
|
140
|
+
#
|
141
|
+
# success, errors = constraint.match(object)
|
142
|
+
# success #=> false
|
143
|
+
# errors.class #=> Stannum::Errors
|
144
|
+
# errors.to_a #=> [{ type: 'some_error', message: 'some error message' }]
|
145
|
+
#
|
146
|
+
# @see #errors_for
|
147
|
+
# @see #matches?
|
148
|
+
def match(actual)
|
149
|
+
return [true, Stannum::Errors.new] if matches?(actual)
|
150
|
+
|
151
|
+
[false, errors_for(actual)]
|
152
|
+
end
|
153
|
+
|
154
|
+
# @overload matches?(actual)
|
155
|
+
#
|
156
|
+
# Checks that the given object matches the constraint.
|
157
|
+
#
|
158
|
+
# @example Checking a matching object.
|
159
|
+
# constraint = CustomConstraint.new
|
160
|
+
# object = MatchingObject.new
|
161
|
+
#
|
162
|
+
# constraint.matches?(object) #=> true
|
163
|
+
#
|
164
|
+
# @example Checking a non-matching object.
|
165
|
+
# constraint = CustomConstraint.new
|
166
|
+
# object = NonMatchingObject.new
|
167
|
+
#
|
168
|
+
# constraint.matches?(object) #=> false
|
169
|
+
#
|
170
|
+
# @return [true, false] true if the object matches the expected properties
|
171
|
+
# or behavior, otherwise false.
|
172
|
+
#
|
173
|
+
# @see #does_not_match?
|
174
|
+
def matches?(_actual)
|
175
|
+
false
|
176
|
+
end
|
177
|
+
alias match? matches?
|
178
|
+
|
179
|
+
# @return [String, nil] the default error message generated for a
|
180
|
+
# non-matching object.
|
181
|
+
def message
|
182
|
+
options[:message]
|
183
|
+
end
|
184
|
+
|
185
|
+
# Generates an errors object for the given object when negated.
|
186
|
+
#
|
187
|
+
# The errors object represents the difference between the given object and
|
188
|
+
# the expected properties or behavior when the constraint is negated. It may
|
189
|
+
# be the same for all objects, or different based on the details of the
|
190
|
+
# object or the constraint.
|
191
|
+
#
|
192
|
+
# @param actual [Object] The object to generate errors for.
|
193
|
+
# @param errors [Stannum::Errors] The errors object to append errors to. If
|
194
|
+
# an errors object is not given, a new errors object will be created.
|
195
|
+
#
|
196
|
+
# @example Generating errors for a matching object.
|
197
|
+
# constraint = CustomConstraint.new
|
198
|
+
# object = MatchingObject.new
|
199
|
+
# errors = constraint.negated_errors_for(object)
|
200
|
+
#
|
201
|
+
# errors.class #=> Stannum::Errors
|
202
|
+
# errors.to_a #=> [{ type: 'some_error', message: 'some error message' }]
|
203
|
+
#
|
204
|
+
# @note This method should only be called for an object that matches the
|
205
|
+
# constraint. Generating errors for a matching object can result in
|
206
|
+
# undefined behavior.
|
207
|
+
#
|
208
|
+
# @return [Stannum::Errors] the given or generated errors object.
|
209
|
+
#
|
210
|
+
# @see #does_not_match?
|
211
|
+
# @see #errors_for
|
212
|
+
def negated_errors_for(actual, errors: nil) # rubocop:disable Lint/UnusedMethodArgument
|
213
|
+
(errors || Stannum::Errors.new)
|
214
|
+
.add(negated_type, message: negated_message)
|
215
|
+
end
|
216
|
+
|
217
|
+
# Checks the given object against the constraint and returns errors, if any.
|
218
|
+
#
|
219
|
+
# This method checks the given object against the expected properties or
|
220
|
+
# behavior. If the object matches the constraint, #negated_match will return
|
221
|
+
# false and the generated errors for that object. If the object does not
|
222
|
+
# match the constraint, #negated_match will return true.
|
223
|
+
#
|
224
|
+
# @example Checking a matching object.
|
225
|
+
# constraint = CustomConstraint.new
|
226
|
+
# object = MatchingObject.new
|
227
|
+
#
|
228
|
+
# success, errors = constraint.negated_match(object)
|
229
|
+
# success #=> false
|
230
|
+
# errors.class #=> Stannum::Errors
|
231
|
+
# errors.to_a #=> [{ type: 'some_error', message: 'some error message' }]
|
232
|
+
#
|
233
|
+
# @example Checking a non-matching object.
|
234
|
+
# constraint = CustomConstraint.new
|
235
|
+
# object = NonMatchingObject.new
|
236
|
+
#
|
237
|
+
# success, errors = constraint.negated_match(object)
|
238
|
+
# success #=> true
|
239
|
+
# errors #=> nil
|
240
|
+
#
|
241
|
+
# @see #does_not_match?
|
242
|
+
# @see #match
|
243
|
+
# @see #negated_errors_for
|
244
|
+
def negated_match(actual)
|
245
|
+
return [true, Stannum::Errors.new] if does_not_match?(actual)
|
246
|
+
|
247
|
+
[false, negated_errors_for(actual)]
|
248
|
+
end
|
249
|
+
|
250
|
+
# @return [String, nil] The default error message generated for a matching
|
251
|
+
# object.
|
252
|
+
def negated_message
|
253
|
+
options[:negated_message]
|
254
|
+
end
|
255
|
+
|
256
|
+
# @return [String] the error type generated for a matching object.
|
257
|
+
def negated_type
|
258
|
+
options.fetch(:negated_type, self.class::NEGATED_TYPE)
|
259
|
+
end
|
260
|
+
|
261
|
+
# @return [String] the error type generated for a non-matching object.
|
262
|
+
def type
|
263
|
+
options.fetch(:type, self.class::TYPE)
|
264
|
+
end
|
265
|
+
|
266
|
+
# Creates a copy of the constraint and updates the copy's options.
|
267
|
+
#
|
268
|
+
# @param options [Hash] The options to update.
|
269
|
+
#
|
270
|
+
# @return [Stannum::Constraints::Base] the copied constraint.
|
271
|
+
def with_options(**options)
|
272
|
+
dup.copy_properties(self, options: self.options.merge(options))
|
273
|
+
end
|
274
|
+
|
275
|
+
protected
|
276
|
+
|
277
|
+
attr_writer :options
|
278
|
+
|
279
|
+
def copy_properties(source, options: nil, **_)
|
280
|
+
self.options = options || source.options.dup
|
281
|
+
|
282
|
+
self
|
283
|
+
end
|
284
|
+
end
|
285
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# A Boolean constraint matches only true or false.
|
7
|
+
#
|
8
|
+
# @example Using a Boolean constraint
|
9
|
+
# constraint = Stannum::Constraints::Boolean.new
|
10
|
+
#
|
11
|
+
# constraint.matches?(nil) #=> false
|
12
|
+
# constraint.matches?('a string') #=> false
|
13
|
+
# constraint.matches?(false) #=> true
|
14
|
+
# constraint.matches?(true) #=> true
|
15
|
+
class Boolean < Stannum::Constraints::Base
|
16
|
+
# The :type of the error generated for a matching object.
|
17
|
+
NEGATED_TYPE = 'stannum.constraints.is_boolean'
|
18
|
+
|
19
|
+
# The :type of the error generated for a non-matching object.
|
20
|
+
TYPE = 'stannum.constraints.is_not_boolean'
|
21
|
+
|
22
|
+
# Checks that the object is either true or false.
|
23
|
+
#
|
24
|
+
# @return [true, false] true if the object is true or false, otherwise
|
25
|
+
# false.
|
26
|
+
#
|
27
|
+
# @see Stannum::Constraint#matches?
|
28
|
+
def matches?(actual)
|
29
|
+
true.equal?(actual) || false.equal?(actual)
|
30
|
+
end
|
31
|
+
alias match? matches?
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'forwardable'
|
4
|
+
|
5
|
+
require 'stannum/constraints'
|
6
|
+
|
7
|
+
module Stannum::Constraints
|
8
|
+
# A Delegator constraint delegates the constraint methods to a receiver.
|
9
|
+
#
|
10
|
+
# Use the Delegator constraint when the behavior of a constraint needs to
|
11
|
+
# change based on the context. For example, a contract may use a Delegator
|
12
|
+
# constraint to wrap changes made after the contract is first initialized.
|
13
|
+
#
|
14
|
+
# @example Using a Delegator constraint
|
15
|
+
# receiver = Stannum::Constraints::Type.new(String)
|
16
|
+
# constraint = Stannum::Constraints::Delegator.new(receiver)
|
17
|
+
#
|
18
|
+
# constraint.matches?('a string') #=> true
|
19
|
+
# constraint.matches?(:a_symbol) #=> false
|
20
|
+
#
|
21
|
+
# constraint.receiver = Stannum::Constraints::Type.new(Symbol)
|
22
|
+
#
|
23
|
+
# constraint.matches?('a string') #=> false
|
24
|
+
# constraint.matches?(:a_symbol) #=> true
|
25
|
+
class Delegator < Stannum::Constraints::Base
|
26
|
+
extend Forwardable
|
27
|
+
|
28
|
+
# @param receiver [Stannum::Constraints::Base] The constraint that methods
|
29
|
+
# will be delegated to.
|
30
|
+
def initialize(receiver)
|
31
|
+
super()
|
32
|
+
|
33
|
+
self.receiver = receiver
|
34
|
+
end
|
35
|
+
|
36
|
+
def_delegators :@receiver,
|
37
|
+
:does_not_match?,
|
38
|
+
:errors_for,
|
39
|
+
:match,
|
40
|
+
:matches?,
|
41
|
+
:negated_errors_for,
|
42
|
+
:negated_match,
|
43
|
+
:negated_type,
|
44
|
+
:options,
|
45
|
+
:type
|
46
|
+
|
47
|
+
alias match? matches?
|
48
|
+
|
49
|
+
# @return [Stannum::Constraints::Base] the constraint that methods will be
|
50
|
+
# delegated to.
|
51
|
+
attr_reader :receiver
|
52
|
+
|
53
|
+
# @param value [Stannum::Constraints::Base] The constraint that methods
|
54
|
+
# will be delegated to.
|
55
|
+
def receiver=(value)
|
56
|
+
validate_receiver(value)
|
57
|
+
|
58
|
+
@receiver = value
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def validate_receiver(receiver)
|
64
|
+
return if receiver.is_a?(Stannum::Constraints::Base)
|
65
|
+
|
66
|
+
raise ArgumentError,
|
67
|
+
'receiver must be a Stannum::Constraints::Base',
|
68
|
+
caller(1..-1)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|