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