stannum 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|