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,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
|