stannum 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/DEVELOPMENT.md +105 -0
- data/LICENSE +22 -0
- data/README.md +1327 -0
- data/config/locales/en.rb +47 -0
- data/lib/stannum/attribute.rb +115 -0
- data/lib/stannum/constraint.rb +65 -0
- data/lib/stannum/constraints/absence.rb +42 -0
- data/lib/stannum/constraints/anything.rb +28 -0
- data/lib/stannum/constraints/base.rb +285 -0
- data/lib/stannum/constraints/boolean.rb +33 -0
- data/lib/stannum/constraints/delegator.rb +71 -0
- data/lib/stannum/constraints/enum.rb +64 -0
- data/lib/stannum/constraints/equality.rb +47 -0
- data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
- data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
- data/lib/stannum/constraints/hashes.rb +11 -0
- data/lib/stannum/constraints/identity.rb +46 -0
- data/lib/stannum/constraints/nothing.rb +28 -0
- data/lib/stannum/constraints/presence.rb +42 -0
- data/lib/stannum/constraints/signature.rb +92 -0
- data/lib/stannum/constraints/signatures/map.rb +17 -0
- data/lib/stannum/constraints/signatures/tuple.rb +17 -0
- data/lib/stannum/constraints/signatures.rb +11 -0
- data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
- data/lib/stannum/constraints/tuples.rb +10 -0
- data/lib/stannum/constraints/type.rb +113 -0
- data/lib/stannum/constraints/types/array_type.rb +148 -0
- data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
- data/lib/stannum/constraints/types/date_time_type.rb +16 -0
- data/lib/stannum/constraints/types/date_type.rb +16 -0
- data/lib/stannum/constraints/types/float_type.rb +14 -0
- data/lib/stannum/constraints/types/hash_type.rb +205 -0
- data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
- data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
- data/lib/stannum/constraints/types/integer_type.rb +14 -0
- data/lib/stannum/constraints/types/nil_type.rb +20 -0
- data/lib/stannum/constraints/types/proc_type.rb +14 -0
- data/lib/stannum/constraints/types/string_type.rb +14 -0
- data/lib/stannum/constraints/types/symbol_type.rb +14 -0
- data/lib/stannum/constraints/types/time_type.rb +14 -0
- data/lib/stannum/constraints/types.rb +25 -0
- data/lib/stannum/constraints/union.rb +85 -0
- data/lib/stannum/constraints.rb +26 -0
- data/lib/stannum/contract.rb +243 -0
- data/lib/stannum/contracts/array_contract.rb +108 -0
- data/lib/stannum/contracts/base.rb +597 -0
- data/lib/stannum/contracts/builder.rb +72 -0
- data/lib/stannum/contracts/definition.rb +74 -0
- data/lib/stannum/contracts/hash_contract.rb +136 -0
- data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
- data/lib/stannum/contracts/map_contract.rb +199 -0
- data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
- data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
- data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
- data/lib/stannum/contracts/parameters.rb +15 -0
- data/lib/stannum/contracts/parameters_contract.rb +530 -0
- data/lib/stannum/contracts/tuple_contract.rb +213 -0
- data/lib/stannum/contracts.rb +19 -0
- data/lib/stannum/errors.rb +730 -0
- data/lib/stannum/messages/default_strategy.rb +124 -0
- data/lib/stannum/messages.rb +25 -0
- data/lib/stannum/parameter_validation.rb +216 -0
- data/lib/stannum/rspec/match_errors.rb +17 -0
- data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
- data/lib/stannum/rspec/validate_parameter.rb +23 -0
- data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
- data/lib/stannum/rspec.rb +8 -0
- data/lib/stannum/schema.rb +131 -0
- data/lib/stannum/struct.rb +444 -0
- data/lib/stannum/support/coercion.rb +114 -0
- data/lib/stannum/support/optional.rb +69 -0
- data/lib/stannum/support.rb +8 -0
- data/lib/stannum/version.rb +57 -0
- data/lib/stannum.rb +27 -0
- metadata +216 -0
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
require 'stannum/constraints/base'
|
6
|
+
|
7
|
+
module Stannum::Constraints
|
8
|
+
# An enum constraint asserts that the object is one of the given values.
|
9
|
+
#
|
10
|
+
# @example Using an Enum Constraint
|
11
|
+
# constraint = Stannum::Constraints::Enum.new('red', 'green', 'blue')
|
12
|
+
#
|
13
|
+
# constraint.matches?('red') #=> true
|
14
|
+
# constraint.matches?('yellow') #=> false
|
15
|
+
class Enum < Stannum::Constraints::Base
|
16
|
+
# The :type of the error generated for a matching object.
|
17
|
+
NEGATED_TYPE = 'stannum.constraints.is_in_list'
|
18
|
+
|
19
|
+
# The :type of the error generated for a non-matching object.
|
20
|
+
TYPE = 'stannum.constraints.is_not_in_list'
|
21
|
+
|
22
|
+
# @overload initialize(*expected_values, **options)
|
23
|
+
# @param expected_values [Array] the possible values for the object.
|
24
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
25
|
+
# constraint. Defaults to an empty Hash.
|
26
|
+
def initialize(first, *rest, **options)
|
27
|
+
expected_values = rest.unshift(first)
|
28
|
+
|
29
|
+
super(expected_values: expected_values, **options)
|
30
|
+
|
31
|
+
@matching_values = Set.new(expected_values)
|
32
|
+
end
|
33
|
+
|
34
|
+
# (see Stannum::Constraints::Base#errors_for)
|
35
|
+
def errors_for(actual, errors: nil) # rubocop:disable Lint/UnusedMethodArgument
|
36
|
+
(errors || Stannum::Errors.new).add(type, values: expected_values)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Array] the possible values for the object.
|
40
|
+
def expected_values
|
41
|
+
options[:expected_values]
|
42
|
+
end
|
43
|
+
|
44
|
+
# (see Stannum::Constraints::Base#negated_errors_for)
|
45
|
+
def negated_errors_for(actual, errors: nil) # rubocop:disable Lint/UnusedMethodArgument
|
46
|
+
(errors || Stannum::Errors.new).add(negated_type, values: expected_values)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Checks that the object is in the list of expected values.
|
50
|
+
#
|
51
|
+
# @return [true, false] false if the object is in the list of expected
|
52
|
+
# values, otherwise true.
|
53
|
+
#
|
54
|
+
# @see Stannum::Constraint#matches?
|
55
|
+
def matches?(actual)
|
56
|
+
@matching_values.include?(actual)
|
57
|
+
end
|
58
|
+
alias match? matches?
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
attr_reader :matching_values
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# An Equality constraint uses #== to compare the actual and expected objects.
|
7
|
+
#
|
8
|
+
# @example Using an Equality constraint
|
9
|
+
# string = 'Greetings, programs!'
|
10
|
+
# constraint = Stannum::Constraints::Equality.new(string)
|
11
|
+
#
|
12
|
+
# constraint.matches?(nil) #=> false
|
13
|
+
# constraint.matches?('something') #=> false
|
14
|
+
# constraint.matches?('a string') #=> true
|
15
|
+
# constraint.matches?(string.dup) #=> true
|
16
|
+
# constraint.matches?(string) #=> true
|
17
|
+
class Equality < Stannum::Constraints::Base
|
18
|
+
# The :type of the error generated for a matching object.
|
19
|
+
NEGATED_TYPE = 'stannum.constraints.is_equal_to'
|
20
|
+
|
21
|
+
# The :type of the error generated for a non-matching object.
|
22
|
+
TYPE = 'stannum.constraints.is_not_equal_to'
|
23
|
+
|
24
|
+
# @param expected_value [Object] The expected object.
|
25
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
26
|
+
# constraint. Defaults to an empty Hash.
|
27
|
+
def initialize(expected_value, **options)
|
28
|
+
@expected_value = expected_value
|
29
|
+
|
30
|
+
super(expected_value: expected_value, **options)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Object] the expected object.
|
34
|
+
attr_reader :expected_value
|
35
|
+
|
36
|
+
# Checks that the object is the expected value.
|
37
|
+
#
|
38
|
+
# @return [true, false] true if the object is the expected value, otherwise
|
39
|
+
# false.
|
40
|
+
#
|
41
|
+
# @see Stannum::Constraint#matches?
|
42
|
+
def matches?(actual)
|
43
|
+
expected_value == actual
|
44
|
+
end
|
45
|
+
alias match? matches?
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/hashes'
|
4
|
+
|
5
|
+
module Stannum::Constraints::Hashes
|
6
|
+
# Constraint for validating the keys of a hash-like object.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# keys = %[fuel mass size]
|
10
|
+
# constraint = Stannum::Constraints::Hashes::ExpectedKeys.new(keys)
|
11
|
+
#
|
12
|
+
# constraint.matches?({}) #=> true
|
13
|
+
# constraint.matches?({ fuel: 'Monopropellant' }) #=> true
|
14
|
+
# constraint.matches?({ electric: true, fuel: 'Xenon' }) #=> false
|
15
|
+
# constraint.matches?({ fuel: 'LF/O', mass: '1 ton', size: 'Medium' })
|
16
|
+
# #=> true
|
17
|
+
# constraint.matches?(
|
18
|
+
# { fuel: 'LF', mass: '2 tons', nuclear: true, size: 'Medium' }
|
19
|
+
# )
|
20
|
+
# #=> false
|
21
|
+
class ExtraKeys < Stannum::Constraints::Base
|
22
|
+
# The :type of the error generated for a matching object.
|
23
|
+
NEGATED_TYPE = 'stannum.constraints.hashes.no_extra_keys'
|
24
|
+
|
25
|
+
# The :type of the error generated for a non-matching object.
|
26
|
+
TYPE = 'stannum.constraints.hashes.extra_keys'
|
27
|
+
|
28
|
+
# @param expected_keys [Array, Proc] The expected keys. If a Proc, will be
|
29
|
+
# evaluated each time the constraint is matched.
|
30
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
31
|
+
# constraint. Defaults to an empty Hash.
|
32
|
+
def initialize(expected_keys, **options)
|
33
|
+
validate_expected_keys(expected_keys)
|
34
|
+
|
35
|
+
expected_keys =
|
36
|
+
if expected_keys.is_a?(Array)
|
37
|
+
Set.new(expected_keys)
|
38
|
+
else
|
39
|
+
expected_keys
|
40
|
+
end
|
41
|
+
|
42
|
+
super(expected_keys: expected_keys, **options)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [true, false] true if the object responds to #[] and #keys and the
|
46
|
+
# object has at least one key that is not in expected_keys.
|
47
|
+
def does_not_match?(actual)
|
48
|
+
return false unless hash?(actual)
|
49
|
+
|
50
|
+
!(Set.new(actual.keys) <= expected_keys) # rubocop:disable Style/InverseMethods
|
51
|
+
end
|
52
|
+
|
53
|
+
# (see Stannum::Constraints::Base#errors_for)
|
54
|
+
def errors_for(actual, errors: nil)
|
55
|
+
errors ||= Stannum::Errors.new
|
56
|
+
|
57
|
+
unless actual.respond_to?(:keys)
|
58
|
+
return add_invalid_hash_error(actual: actual, errors: errors)
|
59
|
+
end
|
60
|
+
|
61
|
+
each_extra_key(actual) do |key, value|
|
62
|
+
errors[key].add(type, value: value)
|
63
|
+
end
|
64
|
+
|
65
|
+
errors
|
66
|
+
end
|
67
|
+
|
68
|
+
# @return [Array] the expected keys.
|
69
|
+
def expected_keys
|
70
|
+
keys = options[:expected_keys]
|
71
|
+
|
72
|
+
return keys unless keys.is_a?(Proc)
|
73
|
+
|
74
|
+
Set.new(keys.call)
|
75
|
+
end
|
76
|
+
|
77
|
+
# @return [true, false] true if the object responds to #[] and #keys and the
|
78
|
+
# object does not have any key that is not in expected_keys.
|
79
|
+
def matches?(actual)
|
80
|
+
return false unless actual.respond_to?(:keys)
|
81
|
+
|
82
|
+
Set.new(actual.keys) <= expected_keys
|
83
|
+
end
|
84
|
+
alias match? matches?
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def add_invalid_hash_error(actual:, errors:)
|
89
|
+
Stannum::Constraints::Signature
|
90
|
+
.new(:keys)
|
91
|
+
.errors_for(actual, errors: errors)
|
92
|
+
end
|
93
|
+
|
94
|
+
def each_extra_key(actual)
|
95
|
+
expected = expected_keys
|
96
|
+
|
97
|
+
actual.each_key do |key|
|
98
|
+
next if expected.include?(key)
|
99
|
+
|
100
|
+
yield key, actual[key]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def hash?(actual)
|
105
|
+
actual.respond_to?(:[]) && actual.respond_to?(:keys)
|
106
|
+
end
|
107
|
+
|
108
|
+
def valid_key?(key)
|
109
|
+
key.is_a?(String) || key.is_a?(Symbol)
|
110
|
+
end
|
111
|
+
|
112
|
+
def validate_expected_keys(expected_keys)
|
113
|
+
expected_keys = expected_keys.call if expected_keys.is_a?(Proc)
|
114
|
+
|
115
|
+
unless expected_keys.is_a?(Array)
|
116
|
+
raise ArgumentError,
|
117
|
+
'expected_keys must be an Array or a Proc',
|
118
|
+
caller(1..-1)
|
119
|
+
end
|
120
|
+
|
121
|
+
return if expected_keys.all? { |key| valid_key?(key) }
|
122
|
+
|
123
|
+
raise ArgumentError, 'key must be a String or Symbol', caller(1..-1)
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/hashes'
|
4
|
+
|
5
|
+
module Stannum::Constraints::Hashes
|
6
|
+
# Constraint for validating an indifferent hash key.
|
7
|
+
#
|
8
|
+
# To be a valid indifferent Hash key, an object must be a String or a Symbol
|
9
|
+
# and cannot be empty.
|
10
|
+
#
|
11
|
+
# @example With nil
|
12
|
+
# constraint = Stannum::Constraints::Hashes::IndifferentKey.new
|
13
|
+
# constraint.matches?(nil) #=> false
|
14
|
+
# constraint.errors_for(nil)
|
15
|
+
# #=> [{ type: 'absent', data: {}, path: [], message: nil }]
|
16
|
+
#
|
17
|
+
# @example With an Object
|
18
|
+
# constraint = Stannum::Constraints::Hashes::IndifferentKey.new
|
19
|
+
# constraint.matches?(Object.new.freeze) #=> false
|
20
|
+
# constraint.errors_for(Object.new.freeze)
|
21
|
+
# #=> [{ type: 'is_not_string_or_symbol', data: {}, path: [], message: nil }]
|
22
|
+
#
|
23
|
+
# @example With an empty String
|
24
|
+
# constraint = Stannum::Constraints::Hashes::IndifferentKey.new
|
25
|
+
# constraint.matches?('') #=> false
|
26
|
+
# constraint.errors_for('')
|
27
|
+
# #=> [{ type: 'absent', data: {}, path: [], message: nil }]
|
28
|
+
#
|
29
|
+
# @example With a String
|
30
|
+
# constraint = Stannum::Constraints::Hashes::IndifferentKey.new
|
31
|
+
# constraint.matches?('a string') #=> true
|
32
|
+
#
|
33
|
+
# @example With an empty Symbol
|
34
|
+
# constraint = Stannum::Constraints::Hashes::IndifferentKey.new
|
35
|
+
# constraint.matches?(:'') #=> false
|
36
|
+
# constraint.errors_for(:'')
|
37
|
+
# #=> [{ type: 'absent', data: {}, path: [], message: nil }]
|
38
|
+
#
|
39
|
+
# @example With a Symbol
|
40
|
+
# constraint = Stannum::Constraints::Hashes::IndifferentKey.new
|
41
|
+
# constraint.matches?(:a_symbol) #=> true
|
42
|
+
class IndifferentKey < Stannum::Constraints::Base
|
43
|
+
# The :type of the error generated for a matching object.
|
44
|
+
NEGATED_TYPE = 'stannum.constraints.hashes.is_string_or_symbol'
|
45
|
+
|
46
|
+
# The :type of the error generated for a non-matching object.
|
47
|
+
TYPE = 'stannum.constraints.hashes.is_not_string_or_symbol'
|
48
|
+
|
49
|
+
# (see Stannum::Constraints::Base#errors_for)
|
50
|
+
def errors_for(actual, errors: nil)
|
51
|
+
errors ||= Stannum::Errors.new
|
52
|
+
|
53
|
+
return errors.add(Stannum::Constraints::Presence::TYPE) if actual.nil?
|
54
|
+
|
55
|
+
return super unless indifferent_key_type?(actual)
|
56
|
+
|
57
|
+
return errors unless actual.empty?
|
58
|
+
|
59
|
+
errors.add(Stannum::Constraints::Presence::TYPE)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @return [true, false] true if the object is a non-empty String or Symbol.
|
63
|
+
def matches?(actual)
|
64
|
+
indifferent_key_type?(actual) && !actual.empty?
|
65
|
+
end
|
66
|
+
alias match? matches?
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def indifferent_key_type?(actual)
|
71
|
+
actual.is_a?(String) || actual.is_a?(Symbol)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# Namespace for Hash-specific constraints.
|
7
|
+
module Hashes
|
8
|
+
autoload :ExtraKeys, 'stannum/constraints/hashes/extra_keys'
|
9
|
+
autoload :IndifferentKey, 'stannum/constraints/hashes/indifferent_key'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# An Identity constraint checks for the exact object given.
|
7
|
+
#
|
8
|
+
# @example Using an Identity constraint
|
9
|
+
# string = 'Greetings, programs!'
|
10
|
+
# constraint = Stannum::Constraints::Identity.new(string)
|
11
|
+
#
|
12
|
+
# constraint.matches?(nil) #=> false
|
13
|
+
# constraint.matches?('a string') #=> false
|
14
|
+
# constraint.matches?(string.dup) #=> false
|
15
|
+
# constraint.matches?(string) #=> true
|
16
|
+
class Identity < Stannum::Constraints::Base
|
17
|
+
# The :type of the error generated for a matching object.
|
18
|
+
NEGATED_TYPE = 'stannum.constraints.is_value'
|
19
|
+
|
20
|
+
# The :type of the error generated for a non-matching object.
|
21
|
+
TYPE = 'stannum.constraints.is_not_value'
|
22
|
+
|
23
|
+
# @param expected_value [Object] The expected object.
|
24
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
25
|
+
# constraint. Defaults to an empty Hash.
|
26
|
+
def initialize(expected_value, **options)
|
27
|
+
@expected_value = expected_value
|
28
|
+
|
29
|
+
super(expected_value: expected_value, **options)
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [Object] the expected object.
|
33
|
+
attr_reader :expected_value
|
34
|
+
|
35
|
+
# Checks that the object is the expected value.
|
36
|
+
#
|
37
|
+
# @return [true, false] true if the object is the expected value, otherwise
|
38
|
+
# false.
|
39
|
+
#
|
40
|
+
# @see Stannum::Constraint#matches?
|
41
|
+
def matches?(actual)
|
42
|
+
expected_value.equal?(actual)
|
43
|
+
end
|
44
|
+
alias match? matches?
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/base'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# An example constraint that does not match any object, even nil.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# constraint = Stannum::Constraints::Nothing.new
|
10
|
+
# constraint.matches?(Object.new)
|
11
|
+
# #=> false
|
12
|
+
# constraint.does_not_match?(Object.new)
|
13
|
+
# #=> true
|
14
|
+
class Nothing < Stannum::Constraints::Base
|
15
|
+
# The :type of the error generated for a non-matching object.
|
16
|
+
TYPE = 'stannum.constraints.anything'
|
17
|
+
|
18
|
+
# Returns false for all objects.
|
19
|
+
#
|
20
|
+
# @return [false] in all cases.
|
21
|
+
#
|
22
|
+
# @see Stannum::Constraint#matches?
|
23
|
+
def matches?(_actual)
|
24
|
+
false
|
25
|
+
end
|
26
|
+
alias match? matches?
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/base'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# A presence constraint asserts that the object is not nil and not empty.
|
7
|
+
#
|
8
|
+
# @example Using a Presence constraint
|
9
|
+
# constraint = Stannum::Constraints::Presence.new
|
10
|
+
#
|
11
|
+
# constraint.matches?(nil) #=> false
|
12
|
+
# constraint.matches?(Object.new) #=> true
|
13
|
+
#
|
14
|
+
# @example Using a Presence constraint with an Array
|
15
|
+
# constraint.matches?([]) #=> false
|
16
|
+
# constraint.matches?([1, 2, 3]) #=> true
|
17
|
+
#
|
18
|
+
# @example Using a Presence constraint with an Hash
|
19
|
+
# constraint.matches?({}) #=> false
|
20
|
+
# constraint.matches?({ key: 'value' }) #=> true
|
21
|
+
class Presence < Stannum::Constraints::Base
|
22
|
+
# The :type of the error generated for a matching object.
|
23
|
+
NEGATED_TYPE = 'stannum.constraints.present'
|
24
|
+
|
25
|
+
# The :type of the error generated for a non-matching object.
|
26
|
+
TYPE = 'stannum.constraints.absent'
|
27
|
+
|
28
|
+
# Checks that the object is not nil and not empty.
|
29
|
+
#
|
30
|
+
# @return [true, false] false if the object is nil or empty, otherwise true.
|
31
|
+
#
|
32
|
+
# @see Stannum::Constraint#matches?
|
33
|
+
def matches?(actual)
|
34
|
+
return false if actual.nil?
|
35
|
+
|
36
|
+
return false if actual.respond_to?(:empty?) && actual.empty?
|
37
|
+
|
38
|
+
true
|
39
|
+
end
|
40
|
+
alias match? matches?
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# Constraint for matching objects by the methods they respond to.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# constraint = Stannum::Constraints::Signature.new(:[], :keys)
|
10
|
+
#
|
11
|
+
# constraint.matches?(Object.new) #=> false
|
12
|
+
# constraint.matches?([]) #=> false
|
13
|
+
# constraint.matches?({}) #=> true
|
14
|
+
class Signature < Stannum::Constraints::Base
|
15
|
+
# The :type of the error generated for a matching object.
|
16
|
+
NEGATED_TYPE = 'stannum.constraints.has_methods'
|
17
|
+
|
18
|
+
# The :type of the error generated for a non-matching object.
|
19
|
+
TYPE = 'stannum.constraints.does_not_have_methods'
|
20
|
+
|
21
|
+
# @param expected_methods [Array<String, Symbol>] The methods the object is
|
22
|
+
# expected to respond to.
|
23
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
24
|
+
# constraint. Defaults to an empty Hash.
|
25
|
+
def initialize(*expected_methods, **options)
|
26
|
+
validate_expected_methods(expected_methods)
|
27
|
+
|
28
|
+
@expected_methods = expected_methods
|
29
|
+
|
30
|
+
super(expected_methods: expected_methods, **options)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Array<String, Symbol>] the methods the object is expected to
|
34
|
+
# respond to.
|
35
|
+
attr_reader :expected_methods
|
36
|
+
|
37
|
+
# @return [true, false] true if the object does not respond to any of the
|
38
|
+
# expected methods; otherwise false.
|
39
|
+
def does_not_match?(actual)
|
40
|
+
each_missing_method(actual).to_a == expected_methods
|
41
|
+
end
|
42
|
+
|
43
|
+
# (see Stannum::Constraints::Base#errors_for)
|
44
|
+
def errors_for(actual, errors: nil)
|
45
|
+
(errors || Stannum::Errors.new)
|
46
|
+
.add(
|
47
|
+
type,
|
48
|
+
methods: expected_methods,
|
49
|
+
missing: each_missing_method(actual).to_a
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [true, false] true if the object responds to all of the expected
|
54
|
+
# methods; otherwise false.
|
55
|
+
def matches?(actual)
|
56
|
+
each_missing_method(actual).none?
|
57
|
+
end
|
58
|
+
alias match? matches?
|
59
|
+
|
60
|
+
# (see Stannum::Constraints::Base#negated_errors_for)
|
61
|
+
def negated_errors_for(actual, errors: nil)
|
62
|
+
(errors || Stannum::Errors.new)
|
63
|
+
.add(
|
64
|
+
negated_type,
|
65
|
+
methods: expected_methods,
|
66
|
+
missing: each_missing_method(actual).to_a
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def each_missing_method(actual)
|
73
|
+
return enum_for(:each_missing_method, actual) unless block_given?
|
74
|
+
|
75
|
+
expected_methods.each do |method_name|
|
76
|
+
yield method_name unless actual.respond_to?(method_name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def validate_expected_methods(expected_methods)
|
81
|
+
if expected_methods.empty?
|
82
|
+
raise ArgumentError, 'expected methods can\'t be blank', caller(1..-1)
|
83
|
+
end
|
84
|
+
|
85
|
+
return if expected_methods.all? do |method_name|
|
86
|
+
method_name.is_a?(String) || method_name.is_a?(Symbol)
|
87
|
+
end
|
88
|
+
|
89
|
+
raise ArgumentError, 'expected method must be a String or Symbol'
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/signatures'
|
4
|
+
|
5
|
+
module Stannum::Constraints::Signatures
|
6
|
+
# Constraint for matching map-like objects.
|
7
|
+
class Map < Stannum::Constraints::Signature
|
8
|
+
EXPECTED_METHODS = %i[[] each keys].freeze
|
9
|
+
private_constant :EXPECTED_METHODS
|
10
|
+
|
11
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
12
|
+
# constraint. Defaults to an empty Hash.
|
13
|
+
def initialize(**options)
|
14
|
+
super(*EXPECTED_METHODS, **options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/signatures'
|
4
|
+
|
5
|
+
module Stannum::Constraints::Signatures
|
6
|
+
# Constraint for matching tuple-like objects.
|
7
|
+
class Tuple < Stannum::Constraints::Signature
|
8
|
+
EXPECTED_METHODS = %i[[] each each_index].freeze
|
9
|
+
private_constant :EXPECTED_METHODS
|
10
|
+
|
11
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
12
|
+
# constraint. Defaults to an empty Hash.
|
13
|
+
def initialize(**options)
|
14
|
+
super(*EXPECTED_METHODS, **options)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints'
|
4
|
+
|
5
|
+
module Stannum::Constraints
|
6
|
+
# Namespace for constraints that match objects by methods.
|
7
|
+
module Signatures
|
8
|
+
autoload :Map, 'stannum/constraints/signatures/map'
|
9
|
+
autoload :Tuple, 'stannum/constraints/signatures/tuple'
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'stannum/constraints/tuples'
|
4
|
+
|
5
|
+
module Stannum::Constraints::Tuples
|
6
|
+
# Constraint for validating the length of an indexed object.
|
7
|
+
#
|
8
|
+
# @example
|
9
|
+
# constraint = Stannum::Constraints::Tuples::ExtraItems.new(3)
|
10
|
+
#
|
11
|
+
# constraint.matches?([]) #=> true
|
12
|
+
# constraint.matches?([1]) #=> true
|
13
|
+
# constraint.matches?([1, 2, 3]) #=> true
|
14
|
+
# constraint.matches?([1, 2, 3, 4]) #=> false
|
15
|
+
class ExtraItems < Stannum::Constraints::Base
|
16
|
+
# The :type of the error generated for a matching object.
|
17
|
+
NEGATED_TYPE = 'stannum.constraints.tuples.no_extra_items'
|
18
|
+
|
19
|
+
# The :type of the error generated for a non-matching object.
|
20
|
+
TYPE = 'stannum.constraints.tuples.extra_items'
|
21
|
+
|
22
|
+
# @param expected_count [Integer, Proc] The number of expected items. If a
|
23
|
+
# Proc, will be evaluated each time the constraint is matched.
|
24
|
+
# @param options [Hash<Symbol, Object>] Configuration options for the
|
25
|
+
# constraint. Defaults to an empty Hash.
|
26
|
+
def initialize(expected_count, **options)
|
27
|
+
super(expected_count: expected_count, **options)
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [true, false] true if the object responds to #size and the object
|
31
|
+
# size is greater than the number of expected items; otherwise false.
|
32
|
+
def does_not_match?(actual)
|
33
|
+
return false unless actual.respond_to?(:size)
|
34
|
+
|
35
|
+
actual.size > expected_count
|
36
|
+
end
|
37
|
+
|
38
|
+
# (see Stannum::Constraints::Base#errors_for)
|
39
|
+
def errors_for(actual, errors: nil)
|
40
|
+
errors ||= Stannum::Errors.new
|
41
|
+
|
42
|
+
unless actual.respond_to?(:size)
|
43
|
+
return add_invalid_tuple_error(actual: actual, errors: errors)
|
44
|
+
end
|
45
|
+
|
46
|
+
each_extra_item(actual) do |item, index|
|
47
|
+
errors[index].add(type, value: item)
|
48
|
+
end
|
49
|
+
|
50
|
+
errors
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Integer] the number of expected items.
|
54
|
+
def expected_count
|
55
|
+
count = options[:expected_count]
|
56
|
+
|
57
|
+
count.is_a?(Proc) ? count.call : count
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [true, false] true if the object responds to #size and the object
|
61
|
+
# size is less than or equal to than the number of expected items;
|
62
|
+
# otherwise false.
|
63
|
+
def matches?(actual)
|
64
|
+
return false unless actual.respond_to?(:size)
|
65
|
+
|
66
|
+
actual.size <= expected_count
|
67
|
+
end
|
68
|
+
alias match? matches?
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def add_invalid_tuple_error(actual:, errors:)
|
73
|
+
Stannum::Constraints::Signature
|
74
|
+
.new(:size)
|
75
|
+
.errors_for(actual, errors: errors)
|
76
|
+
end
|
77
|
+
|
78
|
+
def each_extra_item(actual, &block)
|
79
|
+
return if matches?(actual)
|
80
|
+
|
81
|
+
actual[expected_count..-1].each.with_index(expected_count, &block)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|