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