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,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/contracts'
4
+
5
+ module Stannum::Contracts
6
+ # Struct that encapsulates a constraint definition on a contract.
7
+ class Definition
8
+ # @param attributes [Hash] The attributes for the definition.
9
+ # @option attributes [Stannum::Constraints::Base] :constraint The constraint
10
+ # to define for the contract.
11
+ # @option attributes [Stannum::Contracts::Base] :contract The contract for
12
+ # which the constraint is defined.
13
+ # @option attributes [Hash<Symbol, Object>] :options The options for the
14
+ # constraint.
15
+ def initialize(attributes = {})
16
+ attributes.each do |key, value|
17
+ send(:"#{key}=", value)
18
+ end
19
+ end
20
+
21
+ # @!attribute [rw] constraint
22
+ # @return [Stannum::Constraints::Base] the defined constraint.
23
+ attr_accessor :constraint
24
+
25
+ # @!attribute [rw] contract
26
+ # @return [Stannum::Contracts::Base] the contract containing the
27
+ # constraint.
28
+ attr_accessor :contract
29
+
30
+ # @!attribute [rw] options
31
+ # @return [Hash<Symbol, Object>] the options defined for the constraint.
32
+ attr_accessor :options
33
+
34
+ # Compares the other object with the definition.
35
+ #
36
+ # @param other [Object] The object to compare.
37
+ #
38
+ # @return [Boolean] true if the other object is a Definition with the same
39
+ # attributes; otherwise false.
40
+ def ==(other)
41
+ other.is_a?(self.class) &&
42
+ other.constraint == constraint &&
43
+ other.contract.equal?(contract) &&
44
+ other.options == options
45
+ end
46
+
47
+ # Indicates whether the defined constraint is inherited via concatenation.
48
+ #
49
+ # @return [Boolean] true if options[:concatenatable] is set to a truthy
50
+ # value; otherwise false.
51
+ def concatenatable?
52
+ !!options.fetch(:concatenatable, true)
53
+ end
54
+
55
+ # @return [nil, String, Symbol, Array<String, Symbol>] the property scope of
56
+ # the constraint.
57
+ def property
58
+ options[:property]
59
+ end
60
+
61
+ # @return [nil, String, Symbol, Array<String, Symbol>] the property name of
62
+ # the constraint, used for generating errors. If not given, defaults to
63
+ # the value of #property.
64
+ def property_name
65
+ options.fetch(:property_name, options[:property])
66
+ end
67
+
68
+ # @return [Boolean] true if options[:sanity] is set to a truthy value;
69
+ # otherwise false.
70
+ def sanity?
71
+ !!options[:sanity]
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/contracts'
4
+ require 'stannum/contracts/map_contract'
5
+ require 'stannum/support/coercion'
6
+
7
+ module Stannum::Contracts
8
+ # A HashContract defines constraints on an hash's values.
9
+ #
10
+ # @example Creating A Hash Contract
11
+ # hash_contract = Stannum::Contracts::HashContract.new
12
+ #
13
+ # hash_contract.add_constraint(
14
+ # negated_type: 'example.is_boolean',
15
+ # property: :ok,
16
+ # property_type: :key,
17
+ # type: 'example.is_not_boolean'
18
+ # ) { |actual| actual == true || actual == false }
19
+ # hash_contract.add_constraint(
20
+ # Stannum::Constraints::Type.new(Hash),
21
+ # property: :data,
22
+ # property_type: :key,
23
+ # )
24
+ # hash_contract.add_constraint(
25
+ # Stannum::Constraints::Presence.new,
26
+ # property: :signature,
27
+ # property_type: :key,
28
+ # )
29
+ #
30
+ # @example With A Non-Hash Object
31
+ # hash_contract.matches?(nil) #=> false
32
+ # errors = hash_contract.errors_for(nil)
33
+ # #=> [{ type: 'is_not_type', data: { type: Hash }, path: [], message: nil }]
34
+ #
35
+ # hash_contract.does_not_match?(nil) #=> true
36
+ # hash_contract.negated_errors_for?(nil).to_a #=> []
37
+ #
38
+ # @example With A Hash That Matches None Of The Key Constraints
39
+ # hash_contract.matches?({}) #=> false
40
+ # errors = hash_contract.errors_for({})
41
+ # errors.to_a
42
+ # #=> [
43
+ # { type: 'is_not_boolean', data: {}, path: [:ok], message: nil },
44
+ # { type: 'is_not_type', data: { type: Hash }, path: [:data], message: nil },
45
+ # { type: 'absent', data: {}, path: [:signature], message: nil }
46
+ # ]
47
+ #
48
+ # hash_contract.does_not_match?({}) #=> false
49
+ # errors.to_a
50
+ # #=> [
51
+ # { type: 'is_type', data: { type: Hash }, path: [], message: nil }
52
+ # ]
53
+ #
54
+ # @example With A Hash That Matches Some Of The Key Constraints
55
+ # hash = { ok: true, signature: '' }
56
+ # hash_contract.matches?(hash) #=> false
57
+ # errors = hash_contract.errors_for(hash)
58
+ # errors.to_a
59
+ # #=> [
60
+ # { type: 'is_not_type', data: { type: Hash }, path: [:data], message: nil },
61
+ # { type: 'absent', data: {}, path: [:signature], message: nil }
62
+ # ]
63
+ #
64
+ # hash_contract.does_not_match?(hash) #=> false
65
+ # errors = hash_contract.negated_errors_for?(hash)
66
+ # errors.to_a
67
+ # #=> [
68
+ # { type: 'is_type', data: { type: Hash }, path: [], message: nil },
69
+ # { type: 'is_boolean', data: {}, path: [:ok], message: nil }
70
+ # ]
71
+ #
72
+ # @example With A Hash That Matches All Of The Key Constraints
73
+ # hash = { ok: true, data: {}, signature: 'abc' }
74
+ # hash_contract.matches?(hash) #=> true
75
+ # hash_contract.errors_for(hash).to_a #=> []
76
+ #
77
+ # hash_contract.does_not_match?(hash) #=> false
78
+ # errors = hash_contract.negated_errors_for?(hash)
79
+ # errors.to_a
80
+ # #=> [
81
+ # { type: 'is_type', data: { type: Hash }, path: [], message: nil },
82
+ # { type: 'is_boolean', data: {}, path: [:ok], message: nil },
83
+ # { type: 'present', data: {}, path: [:signature], message: nil },
84
+ # ]
85
+ class HashContract < Stannum::Contracts::MapContract
86
+ # @param allow_extra_keys [true, false] If true, the contract will match
87
+ # hashes with keys that are not constrained by the contract.
88
+ # @param key_type [Stannum::Constraints::Base, Class, nil] If set, then the
89
+ # constraint will check the types of each key in the Hash against the
90
+ # expected type and will fail if any keys do not match.
91
+ # @param value_type [Stannum::Constraints::Base, Class, nil] If set, then
92
+ # the constraint will check the types of each value in the Hash against
93
+ # the expected type and will fail if any values do not match.
94
+ # @param options [Hash<Symbol, Object>] Configuration options for the
95
+ # contract. Defaults to an empty Hash.
96
+ def initialize(
97
+ allow_extra_keys: false,
98
+ key_type: nil,
99
+ value_type: nil,
100
+ **options,
101
+ &block
102
+ )
103
+ super(
104
+ allow_extra_keys: allow_extra_keys,
105
+ key_type: key_type,
106
+ value_type: value_type,
107
+ **options,
108
+ &block
109
+ )
110
+ end
111
+
112
+ # @return [Stannum::Constraints::Base, Class, nil] the expected type for the
113
+ # keys in the Hash, if any.
114
+ def key_type
115
+ options[:key_type]
116
+ end
117
+
118
+ # @return [Stannum::Constraints::Base, Class, nil] the expected type for the
119
+ # values in the Hash, if any.
120
+ def value_type
121
+ options[:value_type]
122
+ end
123
+
124
+ private
125
+
126
+ def add_type_constraint
127
+ add_constraint(
128
+ Stannum::Constraints::Types::HashType.new(
129
+ key_type: key_type,
130
+ value_type: value_type
131
+ ),
132
+ sanity: true
133
+ )
134
+ end
135
+ end
136
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/hashes/indifferent_key'
4
+ require 'stannum/contracts'
5
+
6
+ module Stannum::Contracts
7
+ # An IndifferentHashContract defines constraints on an hash's values.
8
+ #
9
+ # The keys for an IndifferentHashContract must be either strings or symbols.
10
+ # The type of key is ignored when matching - a hash with a string key will
11
+ # match an expected symbol key and vice versa.
12
+ #
13
+ # @example Creating An Indifferent Hash Contract
14
+ # hash_contract = Stannum::Contracts::HashContract.new
15
+ # hash_contract.add_constraint(
16
+ # Stannum::Constraints::Presence.new,
17
+ # property: :data,
18
+ # property_type: :key,
19
+ # )
20
+ #
21
+ # @example With A Non-Hash Object
22
+ # hash_contract.matches?(nil) #=> false
23
+ # errors = hash_contract.errors_for(nil)
24
+ # #=> [{ type: 'is_not_type', data: { type: Hash }, path: [], message: nil }]
25
+ #
26
+ # @example With An Empty Hash
27
+ # hash_contract.matches?({}) #=> false
28
+ # errors = hash_contract.errors_for({})
29
+ # #=> [{ type: 'absent', data: {}, path: [:data], message: nil }]
30
+ #
31
+ # @example With A Hash With String Keys
32
+ # hash = { 'data' => {} }
33
+ # hash_contract.matches?(hash) #=> true
34
+ # hash_contract.errors_for(hash) #=> []
35
+ #
36
+ # @example With A Hash With Symbol Keys
37
+ # hash = { data: {} }
38
+ # hash_contract.matches?(hash) #=> true
39
+ # hash_contract.errors_for(hash) #=> []
40
+ class IndifferentHashContract < HashContract
41
+ # @param allow_extra_keys [true, false] If true, the contract will match
42
+ # hashes with keys that are not constrained by the contract.
43
+ # @param value_type [Stannum::Constraints::Base, Class, nil] If set, then
44
+ # the constraint will check the types of each value in the Hash against
45
+ # the expected type and will fail if any values do not match.
46
+ # @param options [Hash<Symbol, Object>] Configuration options for the
47
+ # contract. Defaults to an empty Hash.
48
+ def initialize(
49
+ allow_extra_keys: false,
50
+ value_type: nil,
51
+ **options,
52
+ &block
53
+ )
54
+ super(
55
+ allow_extra_keys: allow_extra_keys,
56
+ key_type: Stannum::Constraints::Hashes::IndifferentKey.new,
57
+ value_type: value_type,
58
+ **options,
59
+ &block
60
+ )
61
+ end
62
+
63
+ protected
64
+
65
+ def map_value(actual, **options)
66
+ return super unless options[:property_type] == :key
67
+
68
+ property = options[:property]
69
+
70
+ case property
71
+ when String
72
+ actual.fetch(property) { actual[property.intern] }
73
+ when Symbol
74
+ actual.fetch(property) { actual[property.to_s] }
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,199 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/contracts'
4
+
5
+ module Stannum::Contracts
6
+ # A MapContract defines constraints on an hash-like object's values.
7
+ #
8
+ # @example Creating A Map Contract
9
+ # Response = Struct.new(:ok, :data, :signature)
10
+ # map_contract = Stannum::Contracts::MapContract.new
11
+ #
12
+ # map_contract.add_constraint(
13
+ # negated_type: 'example.is_boolean',
14
+ # property: :ok,
15
+ # property_type: :key,
16
+ # type: 'example.is_not_boolean'
17
+ # ) { |actual| actual == true || actual == false }
18
+ # map_contract.add_constraint(
19
+ # Stannum::Constraints::Type.new(Hash),
20
+ # property: :data,
21
+ # property_type: :key,
22
+ # )
23
+ # map_contract.add_constraint(
24
+ # Stannum::Constraints::Presence.new,
25
+ # property: :signature,
26
+ # property_type: :key,
27
+ # )
28
+ #
29
+ # @example With A Non-Map Object
30
+ # map_contract.matches?(nil) #=> false
31
+ # errors = map_contract.errors_for(nil)
32
+ # #=> [
33
+ # {
34
+ # type: 'stannum.constraints.does_not_have_methods',
35
+ # data: { methods: [:[], :each, :keys], missing: [:[], :each, :keys] },
36
+ # message: nil,
37
+ # path: []
38
+ # }
39
+ # ]
40
+ # map_contract.does_not_match?(nil) #=> true
41
+ # map_contract.negated_errors_for?(nil).to_a #=> []
42
+ #
43
+ # @example With An Object That Matches None Of The Key Constraints
44
+ # response = Response.new
45
+ # map_contract.matches?(response) #=> false
46
+ # errors = map_contract.errors_for(response)
47
+ # errors.to_a
48
+ # #=> [
49
+ # { type: 'is_not_boolean', data: {}, path: [:ok], message: nil },
50
+ # { type: 'is_not_type', data: { type: Hash }, path: [:data], message: nil },
51
+ # { type: 'absent', data: {}, path: [:signature], message: nil }
52
+ # ]
53
+ #
54
+ # @example With An Object That Matches Some Of The Key Constraints
55
+ # response = Response.new(true, nil, '')
56
+ # map_contract.matches?(response) #=> false
57
+ # errors = map_contract.errors_for(response)
58
+ # errors.to_a
59
+ # #=> [
60
+ # { type: 'is_not_type', data: { type: Hash }, path: [:data], message: nil },
61
+ # { type: 'absent', data: {}, path: [:signature], message: nil }
62
+ # ]
63
+ #
64
+ # @example With An Object That Matches All Of The Key Constraints
65
+ # response = Response.new(true, {}, 'abc')
66
+ # hash_contract.matches?(response) #=> true
67
+ # hash_contract.errors_for(response).to_a #=> []
68
+ class MapContract < Stannum::Contract
69
+ # Builder class for defining item constraints for a Contract.
70
+ #
71
+ # This class should not be invoked directly. Instead, pass a block to the
72
+ # constructor for HashContract.
73
+ #
74
+ # @api private
75
+ class Builder < Stannum::Contract::Builder
76
+ # Defines a key constraint on the contract.
77
+ #
78
+ # @overload key(key, constraint, **options)
79
+ # Adds the given constraint to the contract for the value at the given
80
+ # key.
81
+ #
82
+ # @param key [String, Symbol, Array<String, Symbol>] The key to
83
+ # constrain.
84
+ # @param constraint [Stannum::Constraint::Base] The constraint to add.
85
+ # @param options [Hash<Symbol, Object>] Options for the constraint.
86
+ #
87
+ # @overload key(**options) { |value| }
88
+ # Creates a new Stannum::Constraint object with the given block, and
89
+ # adds that constraint to the contract for the value at the given key.
90
+ def key(property, constraint = nil, **options, &block)
91
+ self.constraint(
92
+ constraint,
93
+ property: property,
94
+ property_type: :key,
95
+ **options,
96
+ &block
97
+ )
98
+ end
99
+ end
100
+
101
+ # @param allow_extra_keys [true, false] If true, the contract will match
102
+ # hashes with keys that are not constrained by the contract.
103
+ # @param options [Hash<Symbol, Object>] Configuration options for the
104
+ # contract. Defaults to an empty Hash.
105
+ def initialize(
106
+ allow_extra_keys: false,
107
+ **options,
108
+ &block
109
+ )
110
+ super(
111
+ allow_extra_keys: allow_extra_keys,
112
+ **options,
113
+ &block
114
+ )
115
+ end
116
+
117
+ # Adds a key constraint to the contract.
118
+ #
119
+ # When the contract is called, the contract will find the value of the
120
+ # object for the given key.
121
+ #
122
+ # @param key [Integer] The key of the value to match.
123
+ # @param constraint [Stannum::Constraints::Base] The constraint to add.
124
+ # @param sanity [true, false] Marks the constraint as a sanity constraint,
125
+ # which is always matched first and will always short-circuit on a failed
126
+ # match.
127
+ # @param options [Hash<Symbol, Object>] Options for the constraint. These
128
+ # can be used by subclasses to define the value and error mappings for the
129
+ # constraint.
130
+ #
131
+ # @return [self] the contract.
132
+ #
133
+ # @see Stannum::Contract#add_constraint.
134
+ def add_key_constraint(key, constraint, sanity: false, **options)
135
+ add_constraint(
136
+ constraint,
137
+ property: key,
138
+ property_type: :key,
139
+ sanity: sanity,
140
+ **options
141
+ )
142
+ end
143
+
144
+ # @return [true, false] if true, the contract will match hashes with keys
145
+ # that are not constrained by the contract.
146
+ def allow_extra_keys?
147
+ options[:allow_extra_keys]
148
+ end
149
+
150
+ # @return [Array] the list of keys expected by the key constraints.
151
+ def expected_keys
152
+ each_constraint.reduce([]) do |keys, definition|
153
+ next keys unless definition.options[:property_type] == :key
154
+
155
+ keys << definition.options.fetch(:property)
156
+ end
157
+ end
158
+
159
+ # (see Stannum::Contracts::Base#with_options)
160
+ def with_options(**options)
161
+ return super unless options.key?(:allow_extra_keys)
162
+
163
+ raise ArgumentError, "can't change option :allow_extra_keys"
164
+ end
165
+
166
+ protected
167
+
168
+ def map_value(actual, **options)
169
+ return super unless options[:property_type] == :key
170
+
171
+ actual[options[:property]]
172
+ end
173
+
174
+ private
175
+
176
+ def add_extra_keys_constraint
177
+ return if options[:allow_extra_keys]
178
+
179
+ keys = -> { expected_keys }
180
+
181
+ add_constraint(
182
+ Stannum::Constraints::Hashes::ExtraKeys.new(keys),
183
+ concatenatable: false
184
+ )
185
+ end
186
+
187
+ def add_type_constraint
188
+ add_constraint Stannum::Constraints::Signatures::Map.new, sanity: true
189
+ end
190
+
191
+ def define_constraints(&block)
192
+ add_type_constraint
193
+
194
+ add_extra_keys_constraint
195
+
196
+ super
197
+ end
198
+ end
199
+ end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/contracts/parameters'
4
+ require 'stannum/support/coercion'
5
+
6
+ module Stannum::Contracts::Parameters
7
+ # @api private
8
+ #
9
+ # An ArgumentsContract constrains the arguments given for a method.
10
+ class ArgumentsContract < Stannum::Contracts::TupleContract
11
+ # The :type of the error generated for extra arguments.
12
+ EXTRA_ARGUMENTS_TYPE = 'stannum.constraints.parameters.extra_arguments'
13
+
14
+ # Value used when arguments array does not have a value for the given index.
15
+ UNDEFINED = Object.new.freeze
16
+
17
+ # @param options [Hash<Symbol, Object>] Configuration options for the
18
+ # contract. Defaults to an empty Hash.
19
+ def initialize(**options)
20
+ super(allow_extra_items: false, **options)
21
+ end
22
+
23
+ # Adds an argument constraint to the contract.
24
+ #
25
+ # Generates an argument constraint based on the given type. If the type is
26
+ # a constraint, then the given constraint will be copied with the given
27
+ # options and added for the argument at the index. If the type is a Class or
28
+ # a Module, then a Stannum::Constraints::Type constraint will be created
29
+ # with the given type and options and added for the argument.
30
+ #
31
+ # If the index is specified, then the constraint will be added for the
32
+ # argument at the specified index. If the index is not given, then the
33
+ # constraint will be applied to the next unconstrained argument. For
34
+ # example, the first argument constraint will be added for the argument at
35
+ # index 0, the second constraint for the argument at index 1, and so on.
36
+ #
37
+ # @param index [Integer, nil] The index of the argument. If not given, then
38
+ # the next argument will be constrained with the type.
39
+ # @param type [Class, Module, Stannum::Constraints:Base] The expected type
40
+ # of the argument.
41
+ # @param default [Boolean] If true, the argument has a default value, and
42
+ # the constraint will ignore arguments with no value at that index.
43
+ # @param options [Hash<Symbol, Object>] Configuration options for the
44
+ # constraint. Defaults to an empty Hash.
45
+ #
46
+ # @return [Stannum::Contracts::Parameters::ArgumentsContract] the contract.
47
+ def add_argument_constraint(index, type, default: false, **options)
48
+ index ||= next_index
49
+ constraint = Stannum::Support::Coercion.type_constraint(type, **options)
50
+
51
+ add_index_constraint(index, constraint, default: !!default, **options)
52
+
53
+ self
54
+ end
55
+
56
+ # Sets a constraint for the variadic arguments.
57
+ #
58
+ # The given constraint must match the variadic arguments array as a whole.
59
+ # To constraint each individual item, use #set_variadic_item_constraint.
60
+ #
61
+ # @param constraint [Stannum::Constraints::Base] The constraint to add.
62
+ # The variadic arguments (an array) as a whole must match the given
63
+ # constraint.
64
+ # @param as [Symbol] A human-friendly reference for the additional
65
+ # arguments. Used when generating errors. Should be the same name used in
66
+ # the method definition.
67
+ #
68
+ # @return [self] the contract.
69
+ #
70
+ # @raise [RuntimeError] if the variadic arguments constraint is already set.
71
+ #
72
+ # @see #set_variadic_item_constraint
73
+ def set_variadic_constraint(constraint, as: nil)
74
+ raise 'variadic arguments constraint is already set' if allow_extra_items?
75
+
76
+ options[:allow_extra_items] = true
77
+
78
+ variadic_constraint.receiver = constraint
79
+
80
+ variadic_definition.options[:property_name] = as if as
81
+
82
+ self
83
+ end
84
+
85
+ # Sets a constraint for the variadic argument items.
86
+ #
87
+ # The given type or constraint must individually match each item (if any) in
88
+ # the variadic arguments. To constrain the variadic arguments as a whole,
89
+ # use #set_variadic_constraint.
90
+ #
91
+ # @param item_type [Stannum::Constraints::Base, Class, Module] The type or
92
+ # constraint to add. If the type is a Class or Module, then it is
93
+ # converted to a Stannum::Constraints::Type. Each item in the variadic
94
+ # arguments must match the given constraint.
95
+ # @param as [Symbol] A human-friendly reference for the additional
96
+ # arguments. Used when generating errors. Should be the same name used in
97
+ # the method definition.
98
+ #
99
+ # @return [self] the contract.
100
+ #
101
+ # @raise [RuntimeError] if the variadic arguments constraint is already set.
102
+ #
103
+ # @see #set_variadic_constraint
104
+ def set_variadic_item_constraint(item_type, as: nil)
105
+ type = coerce_item_type(item_type)
106
+ constraint = Stannum::Constraints::Types::ArrayType.new(item_type: type)
107
+
108
+ set_variadic_constraint(constraint, as: as)
109
+ end
110
+
111
+ protected
112
+
113
+ def add_errors_for(definition, value, errors)
114
+ return super unless value == UNDEFINED
115
+
116
+ super(definition, nil, errors)
117
+ end
118
+
119
+ def add_negated_errors_for(definition, value, errors)
120
+ return super unless value == UNDEFINED
121
+
122
+ super(definition, nil, errors)
123
+ end
124
+
125
+ def map_value(actual, **options)
126
+ return super unless options[:property_type] == :index
127
+
128
+ return super unless actual.is_a?(Array)
129
+
130
+ return super if options[:property] < actual.size
131
+
132
+ UNDEFINED
133
+ end
134
+
135
+ def match_constraint(definition, value)
136
+ return super unless value == UNDEFINED
137
+
138
+ definition.options[:default] ? true : super(definition, nil)
139
+ end
140
+
141
+ def match_negated_constraint(definition, value)
142
+ return super unless value == UNDEFINED
143
+
144
+ definition.options[:default] ? false : super(definition, nil)
145
+ end
146
+
147
+ private
148
+
149
+ attr_reader :variadic_constraint
150
+
151
+ attr_reader :variadic_definition
152
+
153
+ def add_extra_items_constraint
154
+ count = -> { expected_count }
155
+
156
+ @variadic_constraint = Stannum::Constraints::Delegator.new(
157
+ Stannum::Constraints::Tuples::ExtraItems.new(
158
+ count,
159
+ type: EXTRA_ARGUMENTS_TYPE
160
+ )
161
+ )
162
+
163
+ add_constraint @variadic_constraint
164
+
165
+ @variadic_definition = @constraints.last
166
+ end
167
+
168
+ def coerce_item_type(item_type)
169
+ Stannum::Support::Coercion.type_constraint(item_type, as: 'item type')
170
+ end
171
+
172
+ def next_index
173
+ index = -1
174
+
175
+ each_constraint do |definition|
176
+ next unless definition.options[:property_type] == :index
177
+ next unless definition.property.is_a?(Integer)
178
+
179
+ index = definition.property if definition.property > index
180
+ end
181
+
182
+ 1 + index
183
+ end
184
+ end
185
+ end