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