stannum 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +21 -0
  3. data/CODE_OF_CONDUCT.md +132 -0
  4. data/DEVELOPMENT.md +105 -0
  5. data/LICENSE +22 -0
  6. data/README.md +1327 -0
  7. data/config/locales/en.rb +47 -0
  8. data/lib/stannum/attribute.rb +115 -0
  9. data/lib/stannum/constraint.rb +65 -0
  10. data/lib/stannum/constraints/absence.rb +42 -0
  11. data/lib/stannum/constraints/anything.rb +28 -0
  12. data/lib/stannum/constraints/base.rb +285 -0
  13. data/lib/stannum/constraints/boolean.rb +33 -0
  14. data/lib/stannum/constraints/delegator.rb +71 -0
  15. data/lib/stannum/constraints/enum.rb +64 -0
  16. data/lib/stannum/constraints/equality.rb +47 -0
  17. data/lib/stannum/constraints/hashes/extra_keys.rb +126 -0
  18. data/lib/stannum/constraints/hashes/indifferent_key.rb +74 -0
  19. data/lib/stannum/constraints/hashes.rb +11 -0
  20. data/lib/stannum/constraints/identity.rb +46 -0
  21. data/lib/stannum/constraints/nothing.rb +28 -0
  22. data/lib/stannum/constraints/presence.rb +42 -0
  23. data/lib/stannum/constraints/signature.rb +92 -0
  24. data/lib/stannum/constraints/signatures/map.rb +17 -0
  25. data/lib/stannum/constraints/signatures/tuple.rb +17 -0
  26. data/lib/stannum/constraints/signatures.rb +11 -0
  27. data/lib/stannum/constraints/tuples/extra_items.rb +84 -0
  28. data/lib/stannum/constraints/tuples.rb +10 -0
  29. data/lib/stannum/constraints/type.rb +113 -0
  30. data/lib/stannum/constraints/types/array_type.rb +148 -0
  31. data/lib/stannum/constraints/types/big_decimal_type.rb +16 -0
  32. data/lib/stannum/constraints/types/date_time_type.rb +16 -0
  33. data/lib/stannum/constraints/types/date_type.rb +16 -0
  34. data/lib/stannum/constraints/types/float_type.rb +14 -0
  35. data/lib/stannum/constraints/types/hash_type.rb +205 -0
  36. data/lib/stannum/constraints/types/hash_with_indifferent_keys.rb +21 -0
  37. data/lib/stannum/constraints/types/hash_with_string_keys.rb +21 -0
  38. data/lib/stannum/constraints/types/hash_with_symbol_keys.rb +21 -0
  39. data/lib/stannum/constraints/types/integer_type.rb +14 -0
  40. data/lib/stannum/constraints/types/nil_type.rb +20 -0
  41. data/lib/stannum/constraints/types/proc_type.rb +14 -0
  42. data/lib/stannum/constraints/types/string_type.rb +14 -0
  43. data/lib/stannum/constraints/types/symbol_type.rb +14 -0
  44. data/lib/stannum/constraints/types/time_type.rb +14 -0
  45. data/lib/stannum/constraints/types.rb +25 -0
  46. data/lib/stannum/constraints/union.rb +85 -0
  47. data/lib/stannum/constraints.rb +26 -0
  48. data/lib/stannum/contract.rb +243 -0
  49. data/lib/stannum/contracts/array_contract.rb +108 -0
  50. data/lib/stannum/contracts/base.rb +597 -0
  51. data/lib/stannum/contracts/builder.rb +72 -0
  52. data/lib/stannum/contracts/definition.rb +74 -0
  53. data/lib/stannum/contracts/hash_contract.rb +136 -0
  54. data/lib/stannum/contracts/indifferent_hash_contract.rb +78 -0
  55. data/lib/stannum/contracts/map_contract.rb +199 -0
  56. data/lib/stannum/contracts/parameters/arguments_contract.rb +185 -0
  57. data/lib/stannum/contracts/parameters/keywords_contract.rb +174 -0
  58. data/lib/stannum/contracts/parameters/signature_contract.rb +29 -0
  59. data/lib/stannum/contracts/parameters.rb +15 -0
  60. data/lib/stannum/contracts/parameters_contract.rb +530 -0
  61. data/lib/stannum/contracts/tuple_contract.rb +213 -0
  62. data/lib/stannum/contracts.rb +19 -0
  63. data/lib/stannum/errors.rb +730 -0
  64. data/lib/stannum/messages/default_strategy.rb +124 -0
  65. data/lib/stannum/messages.rb +25 -0
  66. data/lib/stannum/parameter_validation.rb +216 -0
  67. data/lib/stannum/rspec/match_errors.rb +17 -0
  68. data/lib/stannum/rspec/match_errors_matcher.rb +93 -0
  69. data/lib/stannum/rspec/validate_parameter.rb +23 -0
  70. data/lib/stannum/rspec/validate_parameter_matcher.rb +506 -0
  71. data/lib/stannum/rspec.rb +8 -0
  72. data/lib/stannum/schema.rb +131 -0
  73. data/lib/stannum/struct.rb +444 -0
  74. data/lib/stannum/support/coercion.rb +114 -0
  75. data/lib/stannum/support/optional.rb +69 -0
  76. data/lib/stannum/support.rb +8 -0
  77. data/lib/stannum/version.rb +57 -0
  78. data/lib/stannum.rb +27 -0
  79. metadata +216 -0
@@ -0,0 +1,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