stannum 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/README.md +85 -21
  4. data/config/locales/en.rb +17 -3
  5. data/lib/stannum/constraints/base.rb +11 -4
  6. data/lib/stannum/constraints/hashes/extra_keys.rb +10 -2
  7. data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
  8. data/lib/stannum/constraints/hashes.rb +6 -2
  9. data/lib/stannum/constraints/parameters/extra_arguments.rb +23 -0
  10. data/lib/stannum/constraints/parameters/extra_keywords.rb +29 -0
  11. data/lib/stannum/constraints/parameters.rb +11 -0
  12. data/lib/stannum/constraints/properties/base.rb +124 -0
  13. data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
  14. data/lib/stannum/constraints/properties/match_property.rb +117 -0
  15. data/lib/stannum/constraints/properties/matching.rb +112 -0
  16. data/lib/stannum/constraints/properties.rb +17 -0
  17. data/lib/stannum/constraints/tuples/extra_items.rb +1 -1
  18. data/lib/stannum/constraints/type.rb +1 -1
  19. data/lib/stannum/constraints/types/hash_type.rb +6 -2
  20. data/lib/stannum/constraints.rb +2 -0
  21. data/lib/stannum/contracts/builder.rb +13 -2
  22. data/lib/stannum/contracts/hash_contract.rb +14 -0
  23. data/lib/stannum/contracts/indifferent_hash_contract.rb +13 -0
  24. data/lib/stannum/contracts/parameters/arguments_contract.rb +2 -7
  25. data/lib/stannum/contracts/parameters/keywords_contract.rb +2 -7
  26. data/lib/stannum/contracts/tuple_contract.rb +1 -1
  27. data/lib/stannum/entities/attributes.rb +218 -0
  28. data/lib/stannum/entities/constraints.rb +177 -0
  29. data/lib/stannum/entities/properties.rb +186 -0
  30. data/lib/stannum/entities.rb +13 -0
  31. data/lib/stannum/entity.rb +83 -0
  32. data/lib/stannum/errors.rb +3 -3
  33. data/lib/stannum/messages/default_loader.rb +95 -0
  34. data/lib/stannum/messages/default_strategy.rb +31 -50
  35. data/lib/stannum/messages.rb +1 -0
  36. data/lib/stannum/rspec/match_errors_matcher.rb +6 -6
  37. data/lib/stannum/rspec/validate_parameter_matcher.rb +10 -9
  38. data/lib/stannum/schema.rb +78 -37
  39. data/lib/stannum/struct.rb +12 -346
  40. data/lib/stannum/support/coercion.rb +19 -0
  41. data/lib/stannum/version.rb +1 -1
  42. data/lib/stannum.rb +3 -0
  43. metadata +29 -19
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/properties'
4
+ require 'stannum/constraints/properties/matching'
5
+
6
+ module Stannum::Constraints::Properties
7
+ # Compares the properties of the given object with the specified property.
8
+ #
9
+ # If none of the property values equal the expected value, the constraint will
10
+ # match the object; otherwise, if there are any matching values, the
11
+ # constraint will not match.
12
+ #
13
+ # @example Using an Properties::Match constraint
14
+ # UpdatePassword = Struct.new(:old_password, :new_password)
15
+ # constraint = Stannum::Constraints::Properties::DoNotMatchProperty.new(
16
+ # :old_password,
17
+ # :new_password
18
+ # )
19
+ #
20
+ # params = UpdatePassword.new('tronlives', 'ifightfortheusers')
21
+ # constraint.matches?(params)
22
+ # #=> true
23
+ #
24
+ # params = UpdatePassword.new('tronlives', 'tronlives')
25
+ # constraint.matches?(params)
26
+ # #=> false
27
+ # constraint.errors_for(params)
28
+ # #=> [
29
+ # {
30
+ # path: [:confirmation],
31
+ # type: 'stannum.constraints.is_equal_to',
32
+ # data: { expected: '[FILTERED]', actual: '[FILTERED]' }
33
+ # }
34
+ # ]
35
+ class DoNotMatchProperty < Stannum::Constraints::Properties::Matching
36
+ # The :type of the error generated for a matching object.
37
+ NEGATED_TYPE = Stannum::Constraints::Equality::TYPE
38
+
39
+ # The :type of the error generated for a non-matching object.
40
+ TYPE = Stannum::Constraints::Equality::NEGATED_TYPE
41
+
42
+ # @return [true, false] true if the property values match the reference
43
+ # property value; otherwise false.
44
+ def does_not_match?(actual)
45
+ return false unless can_match_properties?(actual)
46
+
47
+ expected = expected_value(actual)
48
+
49
+ return false if skip_property?(expected)
50
+
51
+ each_non_matching_property(
52
+ actual: actual,
53
+ expected: expected,
54
+ include_all: true
55
+ )
56
+ .none?
57
+ end
58
+
59
+ # (see Stannum::Constraints::Base#errors_for)
60
+ def errors_for(actual, errors: nil)
61
+ errors ||= Stannum::Errors.new
62
+
63
+ return invalid_object_errors(errors) unless can_match_properties?(actual)
64
+
65
+ expected = expected_value(actual)
66
+ matching = each_matching_property(actual: actual, expected: expected)
67
+
68
+ return generic_errors(errors) if matching.count.zero?
69
+
70
+ matching.each do |property_name, _|
71
+ errors[property_name].add(type, message: message)
72
+ end
73
+
74
+ errors
75
+ end
76
+
77
+ # @return [true, false] false if any of the property values match the
78
+ # reference property value; otherwise true.
79
+ def matches?(actual)
80
+ return false unless can_match_properties?(actual)
81
+
82
+ expected = expected_value(actual)
83
+
84
+ return true if skip_property?(expected)
85
+
86
+ each_matching_property(actual: actual, expected: expected).none?
87
+ end
88
+ alias match? matches?
89
+
90
+ # (see Stannum::Constraints::Base#negated_errors_for)
91
+ def negated_errors_for(actual, errors: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
92
+ errors ||= Stannum::Errors.new
93
+
94
+ return invalid_object_errors(errors) unless can_match_properties?(actual)
95
+
96
+ expected = expected_value(actual)
97
+ matching = each_non_matching_property(
98
+ actual: actual,
99
+ expected: expected,
100
+ include_all: true
101
+ )
102
+
103
+ return generic_errors(errors) if matching.count.zero?
104
+
105
+ matching.each do |property_name, value|
106
+ errors[property_name].add(
107
+ negated_type,
108
+ message: negated_message,
109
+ expected: filter_parameters? ? '[FILTERED]' : expected_value(actual),
110
+ actual: filter_parameters? ? '[FILTERED]' : value
111
+ )
112
+ end
113
+
114
+ errors
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/properties'
4
+ require 'stannum/constraints/properties/matching'
5
+
6
+ module Stannum::Constraints::Properties
7
+ # Compares the properties of the given object with the specified property.
8
+ #
9
+ # If all of the property values equal the expected value, the constraint will
10
+ # match the object; otherwise, if there are any non-matching values, the
11
+ # constraint will not match.
12
+ #
13
+ # @example Using an Properties::Match constraint
14
+ # ConfirmPassword = Struct.new(:password, :confirmation)
15
+ # constraint = Stannum::Constraints::Properties::MatchProperty.new(
16
+ # :password,
17
+ # :confirmation
18
+ # )
19
+ #
20
+ # params = ConfirmPassword.new('tronlives', 'ifightfortheusers')
21
+ # constraint.matches?(params)
22
+ # #=> false
23
+ # constraint.errors_for(params)
24
+ # #=> [
25
+ # {
26
+ # path: [:confirmation],
27
+ # type: 'stannum.constraints.is_not_equal_to',
28
+ # data: { expected: '[FILTERED]', actual: '[FILTERED]' }
29
+ # }
30
+ # ]
31
+ #
32
+ # params = ConfirmPassword.new('tronlives', 'tronlives')
33
+ # constraint.matches?(params)
34
+ # #=> true
35
+ class MatchProperty < Stannum::Constraints::Properties::Matching
36
+ # The :type of the error generated for a matching object.
37
+ NEGATED_TYPE = Stannum::Constraints::Equality::NEGATED_TYPE
38
+
39
+ # The :type of the error generated for a non-matching object.
40
+ TYPE = Stannum::Constraints::Equality::TYPE
41
+
42
+ # @return [true, false] false if any of the property values match the
43
+ # reference property value; otherwise true.
44
+ def does_not_match?(actual)
45
+ return false unless can_match_properties?(actual)
46
+
47
+ expected = expected_value(actual)
48
+
49
+ return false if skip_property?(expected)
50
+
51
+ each_matching_property(
52
+ actual: actual,
53
+ expected: expected,
54
+ include_all: true
55
+ )
56
+ .none?
57
+ end
58
+
59
+ # (see Stannum::Constraints::Base#errors_for)
60
+ def errors_for(actual, errors: nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
61
+ errors ||= Stannum::Errors.new
62
+
63
+ return invalid_object_errors(errors) unless can_match_properties?(actual)
64
+
65
+ expected = expected_value(actual)
66
+ matching = each_non_matching_property(actual: actual, expected: expected)
67
+
68
+ return generic_errors(errors) if matching.count.zero?
69
+
70
+ matching.each do |property_name, value|
71
+ errors[property_name].add(
72
+ type,
73
+ message: message,
74
+ expected: filter_parameters? ? '[FILTERED]' : expected_value(actual),
75
+ actual: filter_parameters? ? '[FILTERED]' : value
76
+ )
77
+ end
78
+
79
+ errors
80
+ end
81
+
82
+ # @return [true, false] true if the property values match the reference
83
+ # property value; otherwise false.
84
+ def matches?(actual)
85
+ return false unless can_match_properties?(actual)
86
+
87
+ expected = expected_value(actual)
88
+
89
+ return true if skip_property?(expected)
90
+
91
+ each_non_matching_property(actual: actual, expected: expected).none?
92
+ end
93
+ alias match? matches?
94
+
95
+ # (see Stannum::Constraints::Base#negated_errors_for)
96
+ def negated_errors_for(actual, errors: nil) # rubocop:disable Metrics/MethodLength
97
+ errors ||= Stannum::Errors.new
98
+
99
+ return invalid_object_errors(errors) unless can_match_properties?(actual)
100
+
101
+ expected = expected_value(actual)
102
+ matching = each_matching_property(
103
+ actual: actual,
104
+ expected: expected,
105
+ include_all: true
106
+ )
107
+
108
+ return generic_errors(errors) if matching.count.zero?
109
+
110
+ matching.each do |property_name, _|
111
+ errors[property_name].add(negated_type, message: negated_message)
112
+ end
113
+
114
+ errors
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/properties'
4
+ require 'stannum/constraints/properties/base'
5
+
6
+ module Stannum::Constraints::Properties
7
+ # Abstract base class for property matching constraints.
8
+ class Matching < Stannum::Constraints::Properties::Base
9
+ # @param reference_name [String, Symbol] the name of the reference property
10
+ # to compare to.
11
+ # @param property_names [Array<String, Symbol>] the name or names of the
12
+ # properties to compare.
13
+ # @param options [Hash<Symbol, Object>] configuration options for the
14
+ # constraint. Defaults to an empty Hash.
15
+ #
16
+ # @option options allow_empty [true, false] if true, will match against an
17
+ # object with empty property values, such as an empty string.
18
+ # @option options allow_nil [true, false] if true, will match against an
19
+ # object with nil property values.
20
+ def initialize(reference_name, *property_names, **options)
21
+ @reference_name = reference_name
22
+
23
+ validate_reference_name
24
+
25
+ super(*property_names, reference_name: reference_name, **options)
26
+ end
27
+
28
+ # @return [String, Symbol] the name of the reference property to compare to.
29
+ attr_reader :reference_name
30
+
31
+ private
32
+
33
+ def each_matching_property( # rubocop:disable Metrics/MethodLength
34
+ actual:,
35
+ expected:,
36
+ include_all: false,
37
+ &block
38
+ )
39
+ unless block_given?
40
+ return to_enum(
41
+ __method__,
42
+ actual: actual,
43
+ expected: expected,
44
+ include_all: include_all
45
+ )
46
+ end
47
+
48
+ enumerator = each_property(actual)
49
+
50
+ unless include_all
51
+ enumerator = enumerator.reject { |_, value| skip_property?(value) }
52
+ end
53
+
54
+ enumerator = enumerator.select { |_, value| valid?(expected, value) }
55
+
56
+ block_given? ? enumerator.each(&block) : enumerator
57
+ end
58
+
59
+ def each_non_matching_property( # rubocop:disable Metrics/MethodLength
60
+ actual:,
61
+ expected:,
62
+ include_all: false,
63
+ &block
64
+ )
65
+ unless block_given?
66
+ return to_enum(
67
+ __method__,
68
+ actual: actual,
69
+ expected: expected,
70
+ include_all: include_all
71
+ )
72
+ end
73
+
74
+ enumerator = each_property(actual)
75
+
76
+ unless include_all
77
+ enumerator = enumerator.reject { |_, value| skip_property?(value) }
78
+ end
79
+
80
+ enumerator = enumerator.reject { |_, value| valid?(expected, value) }
81
+
82
+ block_given? ? enumerator.each(&block) : enumerator
83
+ end
84
+
85
+ def expected_value(actual)
86
+ actual[reference_name]
87
+ end
88
+
89
+ def filter_parameters?
90
+ return @filter_parameters unless @filter_parameters.nil?
91
+
92
+ filters = filtered_parameters.map { |param| Regexp.new(param.to_s) }
93
+
94
+ @filter_parameters =
95
+ [reference_name, *property_names].any? do |property_name|
96
+ filters.any? { |filter| filter.match?(property_name.to_s) }
97
+ end
98
+ end
99
+
100
+ def generic_errors(errors)
101
+ errors.add(Stannum::Constraints::Base::NEGATED_TYPE)
102
+ end
103
+
104
+ def valid?(expected, value)
105
+ value == expected
106
+ end
107
+
108
+ def validate_reference_name
109
+ tools.assertions.validate_name(reference_name, as: 'reference name')
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints'
4
+
5
+ module Stannum::Constraints
6
+ # Namespace for Object property-specific constraints.
7
+ module Properties
8
+ autoload :Base,
9
+ 'stannum/constraints/properties/base'
10
+ autoload :DoNotMatchProperty,
11
+ 'stannum/constraints/properties/do_not_match_property'
12
+ autoload :MatchProperty,
13
+ 'stannum/constraints/properties/match_property'
14
+ autoload :Matching,
15
+ 'stannum/constraints/properties/matching'
16
+ end
17
+ end
@@ -78,7 +78,7 @@ module Stannum::Constraints::Tuples
78
78
  def each_extra_item(actual, &block)
79
79
  return if matches?(actual)
80
80
 
81
- actual[expected_count..-1].each.with_index(expected_count, &block)
81
+ actual[expected_count..].each.with_index(expected_count, &block)
82
82
  end
83
83
  end
84
84
  end
@@ -107,7 +107,7 @@ module Stannum::Constraints
107
107
 
108
108
  raise ArgumentError,
109
109
  'expected type must be a Class or Module',
110
- caller[1..-1]
110
+ caller[1..]
111
111
  end
112
112
  end
113
113
  end
@@ -184,13 +184,17 @@ module Stannum::Constraints::Types
184
184
 
185
185
  def update_key_errors_for(actual:, errors:)
186
186
  non_matching_keys(actual).each do |key|
187
- key_type.errors_for(key, errors: errors[:keys][key])
187
+ mapped_key = Stannum::Support::Coercion.error_key(key)
188
+
189
+ key_type.errors_for(key, errors: errors[:keys][mapped_key])
188
190
  end
189
191
  end
190
192
 
191
193
  def update_value_errors_for(actual:, errors:)
192
194
  non_matching_values(actual).each do |key, value|
193
- value_type.errors_for(value, errors: errors[key])
195
+ mapped_key = Stannum::Support::Coercion.error_key(key)
196
+
197
+ value_type.errors_for(value, errors: errors[mapped_key])
194
198
  end
195
199
  end
196
200
 
@@ -15,7 +15,9 @@ module Stannum
15
15
  autoload :Hashes, 'stannum/constraints/hashes'
16
16
  autoload :Identity, 'stannum/constraints/identity'
17
17
  autoload :Nothing, 'stannum/constraints/nothing'
18
+ autoload :Parameters, 'stannum/constraints/parameters'
18
19
  autoload :Presence, 'stannum/constraints/presence'
20
+ autoload :Properties, 'stannum/constraints/properties'
19
21
  autoload :Signature, 'stannum/constraints/signature'
20
22
  autoload :Signatures, 'stannum/constraints/signatures'
21
23
  autoload :Tuples, 'stannum/constraints/tuples'
@@ -18,6 +18,17 @@ module Stannum::Contracts
18
18
  # @return [Stannum::Contract] The contract to which constraints are added.
19
19
  attr_reader :contract
20
20
 
21
+ # Concatenate the constraints from the given other contract.
22
+ #
23
+ # @param other [Stannum::Contract] the other contract.
24
+ #
25
+ # @return [self] the contract builder.
26
+ #
27
+ # @see Stannum::Contracts::Base#concat.
28
+ def concat(other)
29
+ contract.concat(other)
30
+ end
31
+
21
32
  # Adds a constraint to the contract.
22
33
  #
23
34
  # @overload constraint(constraint, **options)
@@ -47,8 +58,8 @@ module Stannum::Contracts
47
58
  private
48
59
 
49
60
  def ambiguous_values_error(constraint)
50
- 'expected either a block or a constraint instance, but received both a' \
51
- " block and #{constraint.inspect}"
61
+ 'expected either a block or a constraint instance, but received both a ' \
62
+ "block and #{constraint.inspect}"
52
63
  end
53
64
 
54
65
  def resolve_constraint(constraint = nil, **options, &block)
@@ -121,6 +121,20 @@ module Stannum::Contracts
121
121
  options[:value_type]
122
122
  end
123
123
 
124
+ protected
125
+
126
+ def map_errors(errors, **options)
127
+ return super unless options[:property_type] == :key
128
+
129
+ property_name = options.fetch(:property_name, options[:property])
130
+ property_name = property_name.nil? ? [nil] : Array(property_name)
131
+ property_name = property_name.map do |key|
132
+ Stannum::Support::Coercion.error_key(key)
133
+ end
134
+
135
+ errors.dig(*property_name)
136
+ end
137
+
124
138
  private
125
139
 
126
140
  def add_type_constraint
@@ -74,5 +74,18 @@ module Stannum::Contracts
74
74
  actual.fetch(property) { actual[property.to_s] }
75
75
  end
76
76
  end
77
+
78
+ private
79
+
80
+ def add_extra_keys_constraint
81
+ return if options[:allow_extra_keys]
82
+
83
+ keys = -> { expected_keys }
84
+
85
+ add_constraint(
86
+ Stannum::Constraints::Hashes::IndifferentExtraKeys.new(keys),
87
+ concatenatable: false
88
+ )
89
+ end
77
90
  end
78
91
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'stannum/constraints/parameters/extra_arguments'
3
4
  require 'stannum/contracts/parameters'
4
5
  require 'stannum/support/coercion'
5
6
 
@@ -8,9 +9,6 @@ module Stannum::Contracts::Parameters
8
9
  #
9
10
  # An ArgumentsContract constrains the arguments given for a method.
10
11
  class ArgumentsContract < Stannum::Contracts::TupleContract
11
- # The :type of the error generated for extra arguments.
12
- EXTRA_ARGUMENTS_TYPE = 'stannum.constraints.parameters.extra_arguments'
13
-
14
12
  # Value used when arguments array does not have a value for the given index.
15
13
  UNDEFINED = Object.new.freeze
16
14
 
@@ -154,10 +152,7 @@ module Stannum::Contracts::Parameters
154
152
  count = -> { expected_count }
155
153
 
156
154
  @variadic_constraint = Stannum::Constraints::Delegator.new(
157
- Stannum::Constraints::Tuples::ExtraItems.new(
158
- count,
159
- type: EXTRA_ARGUMENTS_TYPE
160
- )
155
+ Stannum::Constraints::Parameters::ExtraArguments.new(count)
161
156
  )
162
157
 
163
158
  add_constraint @variadic_constraint
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'stannum/constraints/parameters/extra_keywords'
3
4
  require 'stannum/contracts/indifferent_hash_contract'
4
5
  require 'stannum/contracts/parameters'
5
6
 
@@ -8,9 +9,6 @@ module Stannum::Contracts::Parameters
8
9
  #
9
10
  # A KeywordsContract constrains the keywords given for a method.
10
11
  class KeywordsContract < Stannum::Contracts::IndifferentHashContract
11
- # The :type of the error generated for extra keywords.
12
- EXTRA_KEYWORDS_TYPE = 'stannum.constraints.parameters.extra_keywords'
13
-
14
12
  # Value used when keywords hash does not have a value for the given key.
15
13
  UNDEFINED = Object.new.freeze
16
14
 
@@ -156,10 +154,7 @@ module Stannum::Contracts::Parameters
156
154
  keys = -> { expected_keys }
157
155
 
158
156
  @variadic_constraint = Stannum::Constraints::Delegator.new(
159
- Stannum::Constraints::Hashes::ExtraKeys.new(
160
- keys,
161
- type: EXTRA_KEYWORDS_TYPE
162
- )
157
+ Stannum::Constraints::Parameters::ExtraKeywords.new(keys)
163
158
  )
164
159
 
165
160
  add_constraint @variadic_constraint
@@ -168,7 +168,7 @@ module Stannum::Contracts
168
168
 
169
169
  index = 1 + definition.options.fetch(:property, -1)
170
170
 
171
- index > count ? index : count
171
+ [index, count].max
172
172
  end
173
173
  end
174
174