stannum 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +105 -0
  5. data/LICENSE +22 -0
  6. data/README.md +1327 -0
  7. data/config/locales/en.rb +47 -0
  8. data/lib/stannum/attribute.rb +115 -0
  9. data/lib/stannum/constraint.rb +65 -0
  10. data/lib/stannum/constraints/absence.rb +42 -0
  11. data/lib/stannum/constraints/anything.rb +28 -0
  12. data/lib/stannum/constraints/base.rb +285 -0
  13. data/lib/stannum/constraints/boolean.rb +33 -0
  14. data/lib/stannum/constraints/delegator.rb +71 -0
  15. data/lib/stannum/constraints/enum.rb +64 -0
  16. data/lib/stannum/constraints/equality.rb +47 -0
  17. data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
  18. data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
  19. data/lib/stannum/constraints/hashes.rb +11 -0
  20. data/lib/stannum/constraints/identity.rb +46 -0
  21. data/lib/stannum/constraints/nothing.rb +28 -0
  22. data/lib/stannum/constraints/presence.rb +42 -0
  23. data/lib/stannum/constraints/signature.rb +92 -0
  24. data/lib/stannum/constraints/signatures/map.rb +17 -0
  25. data/lib/stannum/constraints/signatures/tuple.rb +17 -0
  26. data/lib/stannum/constraints/signatures.rb +11 -0
  27. data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
  28. data/lib/stannum/constraints/tuples.rb +10 -0
  29. data/lib/stannum/constraints/type.rb +113 -0
  30. data/lib/stannum/constraints/types/array_type.rb +148 -0
  31. data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
  32. data/lib/stannum/constraints/types/date_time_type.rb +16 -0
  33. data/lib/stannum/constraints/types/date_type.rb +16 -0
  34. data/lib/stannum/constraints/types/float_type.rb +14 -0
  35. data/lib/stannum/constraints/types/hash_type.rb +205 -0
  36. data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
  37. data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
  38. data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
  39. data/lib/stannum/constraints/types/integer_type.rb +14 -0
  40. data/lib/stannum/constraints/types/nil_type.rb +20 -0
  41. data/lib/stannum/constraints/types/proc_type.rb +14 -0
  42. data/lib/stannum/constraints/types/string_type.rb +14 -0
  43. data/lib/stannum/constraints/types/symbol_type.rb +14 -0
  44. data/lib/stannum/constraints/types/time_type.rb +14 -0
  45. data/lib/stannum/constraints/types.rb +25 -0
  46. data/lib/stannum/constraints/union.rb +85 -0
  47. data/lib/stannum/constraints.rb +26 -0
  48. data/lib/stannum/contract.rb +243 -0
  49. data/lib/stannum/contracts/array_contract.rb +108 -0
  50. data/lib/stannum/contracts/base.rb +597 -0
  51. data/lib/stannum/contracts/builder.rb +72 -0
  52. data/lib/stannum/contracts/definition.rb +74 -0
  53. data/lib/stannum/contracts/hash_contract.rb +136 -0
  54. data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
  55. data/lib/stannum/contracts/map_contract.rb +199 -0
  56. data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
  57. data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
  58. data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
  59. data/lib/stannum/contracts/parameters.rb +15 -0
  60. data/lib/stannum/contracts/parameters_contract.rb +530 -0
  61. data/lib/stannum/contracts/tuple_contract.rb +213 -0
  62. data/lib/stannum/contracts.rb +19 -0
  63. data/lib/stannum/errors.rb +730 -0
  64. data/lib/stannum/messages/default_strategy.rb +124 -0
  65. data/lib/stannum/messages.rb +25 -0
  66. data/lib/stannum/parameter_validation.rb +216 -0
  67. data/lib/stannum/rspec/match_errors.rb +17 -0
  68. data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
  69. data/lib/stannum/rspec/validate_parameter.rb +23 -0
  70. data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
  71. data/lib/stannum/rspec.rb +8 -0
  72. data/lib/stannum/schema.rb +131 -0
  73. data/lib/stannum/struct.rb +444 -0
  74. data/lib/stannum/support/coercion.rb +114 -0
  75. data/lib/stannum/support/optional.rb +69 -0
  76. data/lib/stannum/support.rb +8 -0
  77. data/lib/stannum/version.rb +57 -0
  78. data/lib/stannum.rb +27 -0
  79. metadata +216 -0
@@ -0,0 +1,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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints'
4
+
5
+ module Stannum::Constraints
6
+ # Namespace for tuple-specific constraints.
7
+ module Tuples
8
+ autoload :ExtraItems, 'stannum/constraints/tuples/extra_items'
9
+ end
10
+ end