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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +105 -0
  5. data/LICENSE +22 -0
  6. data/README.md +1327 -0
  7. data/config/locales/en.rb +47 -0
  8. data/lib/stannum/attribute.rb +115 -0
  9. data/lib/stannum/constraint.rb +65 -0
  10. data/lib/stannum/constraints/absence.rb +42 -0
  11. data/lib/stannum/constraints/anything.rb +28 -0
  12. data/lib/stannum/constraints/base.rb +285 -0
  13. data/lib/stannum/constraints/boolean.rb +33 -0
  14. data/lib/stannum/constraints/delegator.rb +71 -0
  15. data/lib/stannum/constraints/enum.rb +64 -0
  16. data/lib/stannum/constraints/equality.rb +47 -0
  17. data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
  18. data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
  19. data/lib/stannum/constraints/hashes.rb +11 -0
  20. data/lib/stannum/constraints/identity.rb +46 -0
  21. data/lib/stannum/constraints/nothing.rb +28 -0
  22. data/lib/stannum/constraints/presence.rb +42 -0
  23. data/lib/stannum/constraints/signature.rb +92 -0
  24. data/lib/stannum/constraints/signatures/map.rb +17 -0
  25. data/lib/stannum/constraints/signatures/tuple.rb +17 -0
  26. data/lib/stannum/constraints/signatures.rb +11 -0
  27. data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
  28. data/lib/stannum/constraints/tuples.rb +10 -0
  29. data/lib/stannum/constraints/type.rb +113 -0
  30. data/lib/stannum/constraints/types/array_type.rb +148 -0
  31. data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
  32. data/lib/stannum/constraints/types/date_time_type.rb +16 -0
  33. data/lib/stannum/constraints/types/date_type.rb +16 -0
  34. data/lib/stannum/constraints/types/float_type.rb +14 -0
  35. data/lib/stannum/constraints/types/hash_type.rb +205 -0
  36. data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
  37. data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
  38. data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
  39. data/lib/stannum/constraints/types/integer_type.rb +14 -0
  40. data/lib/stannum/constraints/types/nil_type.rb +20 -0
  41. data/lib/stannum/constraints/types/proc_type.rb +14 -0
  42. data/lib/stannum/constraints/types/string_type.rb +14 -0
  43. data/lib/stannum/constraints/types/symbol_type.rb +14 -0
  44. data/lib/stannum/constraints/types/time_type.rb +14 -0
  45. data/lib/stannum/constraints/types.rb +25 -0
  46. data/lib/stannum/constraints/union.rb +85 -0
  47. data/lib/stannum/constraints.rb +26 -0
  48. data/lib/stannum/contract.rb +243 -0
  49. data/lib/stannum/contracts/array_contract.rb +108 -0
  50. data/lib/stannum/contracts/base.rb +597 -0
  51. data/lib/stannum/contracts/builder.rb +72 -0
  52. data/lib/stannum/contracts/definition.rb +74 -0
  53. data/lib/stannum/contracts/hash_contract.rb +136 -0
  54. data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
  55. data/lib/stannum/contracts/map_contract.rb +199 -0
  56. data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
  57. data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
  58. data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
  59. data/lib/stannum/contracts/parameters.rb +15 -0
  60. data/lib/stannum/contracts/parameters_contract.rb +530 -0
  61. data/lib/stannum/contracts/tuple_contract.rb +213 -0
  62. data/lib/stannum/contracts.rb +19 -0
  63. data/lib/stannum/errors.rb +730 -0
  64. data/lib/stannum/messages/default_strategy.rb +124 -0
  65. data/lib/stannum/messages.rb +25 -0
  66. data/lib/stannum/parameter_validation.rb +216 -0
  67. data/lib/stannum/rspec/match_errors.rb +17 -0
  68. data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
  69. data/lib/stannum/rspec/validate_parameter.rb +23 -0
  70. data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
  71. data/lib/stannum/rspec.rb +8 -0
  72. data/lib/stannum/schema.rb +131 -0
  73. data/lib/stannum/struct.rb +444 -0
  74. data/lib/stannum/support/coercion.rb +114 -0
  75. data/lib/stannum/support/optional.rb +69 -0
  76. data/lib/stannum/support.rb +8 -0
  77. data/lib/stannum/version.rb +57 -0
  78. data/lib/stannum.rb +27 -0
  79. 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