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,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/types'
4
+
5
+ module Stannum::Constraints::Types
6
+ # A TimeType constraint asserts that the object is a Time.
7
+ class TimeType < Stannum::Constraints::Type
8
+ # @param options [Hash<Symbol, Object>] Configuration options for the
9
+ # constraint. Defaults to an empty Hash.
10
+ def initialize(**options)
11
+ super(::Time, **options)
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints'
4
+
5
+ module Stannum::Constraints
6
+ # Namespace for type constraints.
7
+ module Types
8
+ autoload :ArrayType, 'stannum/constraints/types/array_type'
9
+ autoload :DateType, 'stannum/constraints/types/date_type'
10
+ autoload :DateTimeType, 'stannum/constraints/types/date_time_type'
11
+ autoload :BigDecimalType, 'stannum/constraints/types/big_decimal_type'
12
+ autoload :FloatType, 'stannum/constraints/types/float_type'
13
+ autoload :HashType, 'stannum/constraints/types/hash_type'
14
+ autoload :HashWithIndifferentKeys,
15
+ 'stannum/constraints/types/hash_with_indifferent_keys'
16
+ autoload :HashWithStringKeys,
17
+ 'stannum/constraints/types/hash_with_string_keys'
18
+ autoload :IntegerType, 'stannum/constraints/types/integer_type'
19
+ autoload :NilType, 'stannum/constraints/types/nil_type'
20
+ autoload :ProcType, 'stannum/constraints/types/proc_type'
21
+ autoload :StringType, 'stannum/constraints/types/string_type'
22
+ autoload :SymbolType, 'stannum/constraints/types/symbol_type'
23
+ autoload :TimeType, 'stannum/constraints/types/time_type'
24
+ end
25
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/base'
4
+
5
+ module Stannum::Constraints
6
+ # Asserts that the object matches one of the given constraints.
7
+ #
8
+ # @example Using a Union Constraint.
9
+ # false_constraint = Stannum::Constraint.new { |actual| actual == false }
10
+ # true_constraint = Stannum::Constraint.new { |actual| actual == true }
11
+ # union_constraint = Stannum::Constraints::Union.new(
12
+ # false_constraint,
13
+ # true_constraint
14
+ # )
15
+ #
16
+ # constraint.matches?(nil) #=> false
17
+ # constraint.matches?(false) #=> true
18
+ # constraint.matches?(true) #=> true
19
+ class Union < Stannum::Constraints::Base
20
+ # The :type of the error generated for a matching object.
21
+ NEGATED_TYPE = 'stannum.constraints.is_in_union'
22
+
23
+ # The :type of the error generated for a non-matching object.
24
+ TYPE = 'stannum.constraints.is_not_in_union'
25
+
26
+ # @overload initialize(*expected_constraints, **options)
27
+ # @param expected_constraints [Array<Stannum::Constraints::Base>] The
28
+ # possible values for the object.
29
+ # @param options [Hash<Symbol, Object>] Configuration options for the
30
+ # constraint. Defaults to an empty Hash.
31
+ def initialize(first, *rest, **options)
32
+ expected_constraints = rest.unshift(first)
33
+
34
+ super(expected_constraints: expected_constraints, **options)
35
+
36
+ @expected_constraints = expected_constraints
37
+ end
38
+
39
+ # @return [Array<Stannum::Constraints::Base>] the possible values for the
40
+ # object.
41
+ attr_reader :expected_constraints
42
+
43
+ # (see Stannum::Constraints::Base#errors_for)
44
+ def errors_for(actual, errors: nil) # rubocop:disable Lint/UnusedMethodArgument
45
+ (errors || Stannum::Errors.new).add(type, constraints: expected_values)
46
+ end
47
+
48
+ # Checks that the object matches at least one of the given constraints.
49
+ #
50
+ # @return [true, false] false if the object matches a constraint, otherwise
51
+ # false.
52
+ #
53
+ # @see Stannum::Constraint#matches?
54
+ def matches?(actual)
55
+ expected_constraints.any? { |constraint| constraint.matches?(actual) }
56
+ end
57
+ alias match? matches?
58
+
59
+ # (see Stannum::Constraints::Base#negated_errors_for)
60
+ def negated_errors_for(actual, errors: nil) # rubocop:disable Lint/UnusedMethodArgument
61
+ (errors || Stannum::Errors.new)
62
+ .add(negated_type, constraints: negated_values)
63
+ end
64
+
65
+ private
66
+
67
+ def expected_values
68
+ @expected_constraints.map do |constraint|
69
+ {
70
+ options: constraint.options,
71
+ type: constraint.type
72
+ }
73
+ end
74
+ end
75
+
76
+ def negated_values
77
+ @expected_constraints.map do |constraint|
78
+ {
79
+ negated_type: constraint.negated_type,
80
+ options: constraint.options
81
+ }
82
+ end
83
+ end
84
+ end
85
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum'
4
+
5
+ module Stannum
6
+ # Namespace for pre-defined constraints.
7
+ module Constraints
8
+ autoload :Absence, 'stannum/constraints/absence'
9
+ autoload :Anything, 'stannum/constraints/anything'
10
+ autoload :Base, 'stannum/constraints/base'
11
+ autoload :Boolean, 'stannum/constraints/boolean'
12
+ autoload :Delegator, 'stannum/constraints/delegator'
13
+ autoload :Enum, 'stannum/constraints/enum'
14
+ autoload :Equality, 'stannum/constraints/equality'
15
+ autoload :Hashes, 'stannum/constraints/hashes'
16
+ autoload :Identity, 'stannum/constraints/identity'
17
+ autoload :Nothing, 'stannum/constraints/nothing'
18
+ autoload :Presence, 'stannum/constraints/presence'
19
+ autoload :Signature, 'stannum/constraints/signature'
20
+ autoload :Signatures, 'stannum/constraints/signatures'
21
+ autoload :Tuples, 'stannum/constraints/tuples'
22
+ autoload :Type, 'stannum/constraints/type'
23
+ autoload :Types, 'stannum/constraints/types'
24
+ autoload :Union, 'stannum/constraints/union'
25
+ end
26
+ end
@@ -0,0 +1,243 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum'
4
+ require 'stannum/contracts/base'
5
+
6
+ module Stannum
7
+ # A Contract defines constraints on an object and its properties.
8
+ #
9
+ # @example Creating A Contract With Property Constraints
10
+ # Widget = Struct.new(:name, :manufacturer)
11
+ # Manufacturer = Struct.new(:factory)
12
+ # Factory = Struct.new(:address)
13
+ #
14
+ # type_constraint = Stannum::Constraints::Type.new(Widget)
15
+ # name_constraint =
16
+ # Stannum::Constraint.new(type: 'wrong_name', negated_type: 'right_name') do |value|
17
+ # value == 'Self-sealing Stem Bolt'
18
+ # end
19
+ # address_constraint =
20
+ # Stannum::Constraint.new(type: 'wrong_address', negated_type: 'right_address') do |value|
21
+ # value == '123 Example Street'
22
+ # end
23
+ # contract =
24
+ # Stannum::Contract.new
25
+ # .add_constraint(type_constraint)
26
+ # .add_constraint(name_constraint, property: :name)
27
+ # .add_constraint(address_constraint, property: %i[manufacturer factory address])
28
+ #
29
+ # @example With An Object That Matches None Of The Property Constraints
30
+ # # With a non-Widget object.
31
+ # contract.matches?(nil) #=> false
32
+ # errors = contract.errors_for(nil)
33
+ # errors.to_a
34
+ # #=> [
35
+ # { type: 'is_not_type', data: { type: Widget }, path: [], message: nil },
36
+ # { type: 'wrong_name', data: {}, path: [:name], message: nil },
37
+ # { type: 'wrong_address', data: {}, path: [:manufacturer, :factory, :address], message: nil }
38
+ # ]
39
+ # errors[:name].to_a
40
+ # #=> [
41
+ # { type: 'wrong_name', data: {}, path: [], message: nil }
42
+ # ]
43
+ # errors[:manufacturer].to_a
44
+ # #=> [
45
+ # { type: 'wrong_address', data: {}, path: [:factory, :address], message: nil }
46
+ # ]
47
+ #
48
+ # contract.does_not_match?(nil) #=> true
49
+ # contract.negated_errors_for?(nil).to_a #=> []
50
+ #
51
+ # @example With An Object That Matches Some Of The Property Constraints
52
+ # contract.matches?(Widget.new) #=> false
53
+ # errors = contract.errors_for(Widget.new)
54
+ # errors.to_a
55
+ # #=> [
56
+ # { type: 'wrong_name', data: {}, path: [:name], message: nil },
57
+ # { type: 'wrong_address', data: {}, path: [:manufacturer, :factory, :address], message: nil }
58
+ # ]
59
+ #
60
+ # contract.does_not_match?(Widget.new) #=> false
61
+ # errors = contract.negated_errors_for(Widget.new)
62
+ # errors.to_a
63
+ # #=> [
64
+ # { type: 'is_type', data: { type: Widget }, path: [], message: nil }
65
+ # ]
66
+ #
67
+ # @example With An Object That Matches All Of The Property Constraints
68
+ # factory = Factory.new('123 Example Street')
69
+ # manufacturer = Manufacturer.new(factory)
70
+ # widget = Widget.new('Self-sealing Stem Bolt', manufacturer)
71
+ # contract.matches?(widget) #=> true
72
+ # contract.errors_for(widget).to_a #=> []
73
+ #
74
+ # contract.does_not_match?(widget) #=> true
75
+ # errors = contract.negated_errors_for(widget)
76
+ # errors.to_a
77
+ # #=> [
78
+ # { type: 'is_type', data: { type: Widget }, path: [], message: nil },
79
+ # { type: 'right_name', data: {}, path: [:name], message: nil },
80
+ # { type: 'right_address', data: {}, path: [:manufacturer, :factory, :address], message: nil }
81
+ # ]
82
+ #
83
+ # @example Defining A Custom Contract
84
+ # user_contract = Stannum::Contract.new do
85
+ # # Sanity constraints are evaluated first, and if a sanity constraint
86
+ # # fails, the contract will immediately halt.
87
+ # constraint Stannum::Constraints::Type.new(User), sanity: true
88
+ #
89
+ # # You can also define a constraint using a block.
90
+ # constraint(type: 'example.is_not_user') do |user|
91
+ # user.role == 'user'
92
+ # end
93
+ #
94
+ # # You can define a constraint on a property of the object.
95
+ # property :name, Stannum::Constraints::Presence.new
96
+ # end
97
+ #
98
+ # @see Stannum::Contracts::Base.
99
+ class Contract < Stannum::Contracts::Base
100
+ # Builder class for defining item constraints for a Contract.
101
+ #
102
+ # This class should not be invoked directly. Instead, pass a block to the
103
+ # constructor for Contract.
104
+ #
105
+ # @api private
106
+ class Builder < Stannum::Contracts::Base::Builder
107
+ # Defines a property constraint on the contract.
108
+ #
109
+ # @overload property(property, constraint, **options)
110
+ # Adds the given constraint to the contract for the property.
111
+ #
112
+ # @param property [String, Symbol, Array<String, Symbol>] The property
113
+ # to constrain.
114
+ # @param constraint [Stannum::Constraint::Base] The constraint to add.
115
+ # @param options [Hash<Symbol, Object>] Options for the constraint.
116
+ #
117
+ # @overload property(**options) { |value| }
118
+ # Creates a new Stannum::Constraint object with the given block, and
119
+ # adds that constraint to the contract for the property.
120
+ def property(property, constraint = nil, **options, &block)
121
+ self.constraint(
122
+ constraint,
123
+ property: property,
124
+ **options,
125
+ &block
126
+ )
127
+ end
128
+ end
129
+
130
+ # (see Stannum::Contracts::Base#add_constraint)
131
+ #
132
+ # If the :property option is set, this defines a property constraint. See
133
+ # #add_property_constraint for more information.
134
+ #
135
+ # @param property [String, Symbol, Array<String, Symbol>, nil] The
136
+ # property to match.
137
+ #
138
+ # @see #add_property_constraint.
139
+ def add_constraint(constraint, property: nil, sanity: false, **options)
140
+ validate_constraint(constraint)
141
+ validate_property(property: property, **options)
142
+
143
+ @constraints << Stannum::Contracts::Definition.new(
144
+ constraint: constraint,
145
+ contract: self,
146
+ options: options.merge(property: property, sanity: sanity)
147
+ )
148
+
149
+ self
150
+ end
151
+
152
+ # Adds a property constraint to the contract.
153
+ #
154
+ # When the contract is called, the contract will find the value of that
155
+ # property for the given object. If the property is an array, the contract
156
+ # will recursively retrieve each property.
157
+ #
158
+ # A property of nil will match against the given object itself, rather
159
+ # than one of its properties.
160
+ #
161
+ # If the value does not match the constraint, then the error from the
162
+ # constraint will be added in an error namespace matching the constraint.
163
+ # For example, a property of :name will add the error message to
164
+ # errors.dig(:name), while a property of [:manufacturer, :address, :street]
165
+ # will add the error message to
166
+ # errors.dig(:manufacturer, :address, :street).
167
+ #
168
+ # @param property [String, Symbol, Array<String, Symbol>, nil] The
169
+ # property to match.
170
+ # @param constraint [Stannum::Constraints::Base] The constraint to add.
171
+ # @param sanity [true, false] Marks the constraint as a sanity constraint,
172
+ # which is always matched first and will always short-circuit on a failed
173
+ # match.
174
+ # @param options [Hash<Symbol, Object>] Options for the constraint. These
175
+ # can be used by subclasses to define the value and error mappings for the
176
+ # constraint.
177
+ #
178
+ # @return [self] the contract.
179
+ #
180
+ # @see #add_constraint.
181
+ def add_property_constraint(property, constraint, sanity: false, **options)
182
+ add_constraint(constraint, property: property, sanity: sanity, **options)
183
+ end
184
+
185
+ protected
186
+
187
+ def map_errors(errors, **options)
188
+ property_name = options.fetch(:property_name, options[:property])
189
+
190
+ return errors if property_name.nil?
191
+
192
+ errors.dig(*Array(property_name))
193
+ end
194
+
195
+ def map_value(actual, **options)
196
+ property = options[:property]
197
+
198
+ return actual if property.nil?
199
+
200
+ access_nested_property(actual, property)
201
+ end
202
+
203
+ private
204
+
205
+ def access_nested_property(object, property)
206
+ Array(property).reduce(object) { |obj, prop| access_property(obj, prop) }
207
+ end
208
+
209
+ def access_property(object, property)
210
+ object.send(property) if object.respond_to?(property, true)
211
+ end
212
+
213
+ def valid_property?(property: nil, **_options)
214
+ if property.is_a?(Array)
215
+ return false if property.empty?
216
+
217
+ return property.all? { |item| valid_property_name?(item) }
218
+ end
219
+
220
+ valid_property_name?(property)
221
+ end
222
+
223
+ def valid_property_name?(name)
224
+ return false unless name.is_a?(String) || name.is_a?(Symbol)
225
+
226
+ !name.empty?
227
+ end
228
+
229
+ def validate_property(**options)
230
+ return unless validate_property?(**options)
231
+
232
+ return if valid_property?(**options)
233
+
234
+ raise ArgumentError,
235
+ "invalid property name #{options[:property].inspect}",
236
+ caller(1..-1)
237
+ end
238
+
239
+ def validate_property?(property: nil, **_options)
240
+ !property.nil?
241
+ end
242
+ end
243
+ end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/types/array_type'
4
+ require 'stannum/contracts'
5
+ require 'stannum/contracts/tuple_contract'
6
+
7
+ module Stannum::Contracts
8
+ # An ArrayContract defines constraints for an Array and its items.
9
+ #
10
+ # In order to match an ArrayContract, the object must be an instance of Array,
11
+ # and the items in the array at each index must match the item constraint
12
+ # defined for that index. If the :item_type option is set, each item must
13
+ # match that type or constraint. Finally, unless the :allow_extra_items option
14
+ # is set to true, the object must not have any extra items.
15
+ #
16
+ # @example Creating A Contract With Item Constraints
17
+ # third_base_constraint = Stannum::Constraint.new do |actual|
18
+ # actual == "I Don't Know"
19
+ # end
20
+ # array_contract = Stannum::Contracts::ArrayContract.new do
21
+ # item { |actual| actual == 'Who' }
22
+ # item { |actual| actual == 'What' }
23
+ # item third_base_constraint
24
+ # end
25
+ #
26
+ # @example With A Non-Array Object
27
+ # array_contract.matches?(nil) #=> false
28
+ # errors = array_contract.errors_for(nil)
29
+ # errors.to_a
30
+ # #=> [
31
+ # {
32
+ # type: 'stannum.constraints.type',
33
+ # data: { required: true, type: Array },
34
+ # message: nil,
35
+ # path: []
36
+ # }
37
+ # ]
38
+ #
39
+ # @example With An Object With Missing Items
40
+ # array_contract.matches?(['Who']) #=> false
41
+ # errors = array_contract.errors_for(['Who'])
42
+ # errors.to_a
43
+ # #=> [
44
+ # { type: 'stannum.constraints.invalid', data: {}, path: [1], message: nil },
45
+ # { type: 'stannum.constraints.invalid', data: {}, path: [2], message: nil }
46
+ # ]
47
+ #
48
+ # @example With An Object With Incorrect Items
49
+ # array_contract.matches?(['What', 'What', "I Don't Know"]) #=> false
50
+ # errors = array_contract.errors_for(['What', 'What', "I Don't Know"])
51
+ # errors.to_a
52
+ # #=> [
53
+ # { type: 'stannum.constraints.invalid', data: {}, path: [0], message: nil }
54
+ # ]
55
+ #
56
+ # @example With An Object With Valid Items
57
+ # array_contract.matches?(['Who', 'What', "I Don't Know"]) #=> true
58
+ # errors = array_contract.errors_for(['What', 'What', "I Don't Know"])
59
+ # errors.to_a #=> []
60
+ #
61
+ # @example With An Object With Extra Items
62
+ # array_contract.matches?(['Who', 'What', "I Don't Know", 'Tomorrow', 'Today']) #=> false
63
+ # errors = array_contract.errors_for(['Who', 'What', "I Don't Know", 'Tomorrow', 'Today'])
64
+ # errors.to_a
65
+ # #=> [
66
+ # { type: 'stannum.constraints.tuples.extra_items', data: {}, path: [3], message: nil },
67
+ # { type: 'stannum.constraints.tuples.extra_items', data: {}, path: [4], message: nil }
68
+ # ]
69
+ class ArrayContract < Stannum::Contracts::TupleContract
70
+ # @param allow_extra_items [true, false] If false, then a tuple with extra
71
+ # items after the last expected item will not match the contract.
72
+ # @param item_type [Stannum::Constraints::Base, Class, nil] If set, then
73
+ # the constraint will check the types of each item in the Array against
74
+ # the expected type and will fail if any items do not match.
75
+ # @param options [Hash<Symbol, Object>] Configuration options for the
76
+ # contract. Defaults to an empty Hash.
77
+ def initialize(allow_extra_items: false, item_type: nil, **options, &block)
78
+ super(
79
+ allow_extra_items: allow_extra_items,
80
+ item_type: item_type,
81
+ **options,
82
+ &block
83
+ )
84
+ end
85
+
86
+ # @return [Stannum::Constraints::Base, nil] the expected type for the items
87
+ # in the array.
88
+ def item_type
89
+ options[:item_type]
90
+ end
91
+
92
+ # (see Stannum::Contracts::Base#with_options)
93
+ def with_options(**options)
94
+ return super unless options.key?(:item_type)
95
+
96
+ raise ArgumentError, "can't change option :item_type"
97
+ end
98
+
99
+ private
100
+
101
+ def add_type_constraint
102
+ add_constraint(
103
+ Stannum::Constraints::Types::ArrayType.new(item_type: item_type),
104
+ sanity: true
105
+ )
106
+ end
107
+ end
108
+ end