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