stannum 0.1.0

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