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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e4e097d69d42e93658309a2d4415742bdbeb66d48981f89a0a8b486c6935582
4
- data.tar.gz: 2fdf05503a9fd0e480c30fe45283c1e21dae6701cf79599a0a39ace8717d587f
3
+ metadata.gz: b13aa2cda5a99f544b2e7c281147f51b4e4fa02e394e4b6d017dcc2b1f03705d
4
+ data.tar.gz: e84dc0c10ccfd8989dcec729c485136c099257079df57cb44e82befb42ebf9b8
5
5
  SHA512:
6
- metadata.gz: a650ec61bfc9997fef0ba824670b570c07e57089bde90c81a63fda2a5caaa3a6edfcf2075f2b4060bc25299858715bb8e97eca0548d4d729b9300a864c5cb141
7
- data.tar.gz: 50ed1712df35e1ac05d88c0661be728e10d77df2fecfd6b521e5b3a35c1045e54f4a88c0c9a715d14f24ea4ab5e6f9c25c17c5bf526abf3271a0a19bb1df5617
6
+ metadata.gz: bd159ef522b377cb71e4ebc58d8e46987906a1b3a36a5e905965a5cef9dfcf09b3d3b6818519659a05a19b894b516af8d37ee17a60beb4c72fe4a5df04b2b97d
7
+ data.tar.gz: 806b0a8e067afee2185ba5164d85127543f4e3e821a2ca5582afa13135e6f1b4469f9c5cfb6461bfe3f376e17ec9d2c4580780f3b89d574764856593a660b763
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Constraints
6
+
7
+ - Implemented `Stannum::Constraints::Properties::MatchProperty`
8
+ - Implemented `Stannum::Constraints::Properties::DoNotMatchProperty`
9
+
10
+ ### Contracts
11
+
12
+ - Added `#concat` to `Stannum::Contracts::Builder`.
13
+
14
+ ### Entities
15
+
16
+ Implemented `Stannum::Entity`, a replacement for the existing `Stannum::Struct`.
17
+
18
+ Entitise are largely identical to structs, except for the constructor signature - entities require properties to be passed as keyword parameters, rather than as an attributes hash. Entities (and now Structs) are defined using composable modules.
19
+
20
+ - Implemented `Stannum::Entities::Attributes`.
21
+ - Implemented `Stannum::Entities::Constraints`.
22
+ - Implemented `Stannum::Entities::Properties`.
23
+
24
+ `Stannum::Struct` is now deprecated, and will be removed in a future release.
25
+
26
+ ## 0.2.0
27
+
28
+ ### Constraints
29
+
30
+ #### Parameter Constraints
31
+
32
+ - Implemented `Stannum::Constraints::Parameters::ExtraArguments`
33
+ - Implemented `Stannum::Constraints::Parameters::ExtraKeywords`
34
+
3
35
  ## 0.1.0
4
36
 
5
37
  Initial version.
data/README.md CHANGED
@@ -7,7 +7,7 @@ Stannum defines the following objects:
7
7
  - [Constraints](#constraints): A validator object that responds to `#match`, `#matches?` and `#errors_for` for a given object.
8
8
  - [Contracts](#contracts): A collection of constraints about an object or its properties. Obeys the `Constraint` interface.
9
9
  - [Errors](#errors): Data object for storing validation errors. Supports arbitrary nesting of errors.
10
- - [Structs](#structs): Defines a mutable data object with a specified set of typed attributes.
10
+ - [Entities](#entities): Defines a mutable data object with a specified set of typed attributes.
11
11
 
12
12
  ## About
13
13
 
@@ -17,11 +17,11 @@ First and foremost, Stannum provides you with the tools to validate your data. U
17
17
 
18
18
  Finally, you can combine your constraints into a `Stannum::Contract` to combine multiple validations of your object and its properties. Stannum provides pre-defined contracts for asserting on objects, `Array`s, `Hash`es, and even method parameters.
19
19
 
20
- Stannum also defines the `Stannum::Struct` module for defining structured data entities that are not tied to any framework or datastore. Stannum structs have more functionality and a friendlier interface than a core library `Struct`, provide more structure than a `Hash` or hash-like object (such as an `OpenStruct` or `Hashie::Mash`), and are completely independent from the source of the data. Need to load seed data from a YAML configuration file, perform operations in a SQL database, cross-reference with a MongoDB data store, and use an in-memory data array for lightning-fast tests? A `Stannum::Struct` won't fight you every step of the way.
20
+ Stannum also defines the `Stannum::Entity` module for defining structured data entities that are not tied to any framework or datastore. Stannum entities have more functionality and a friendlier interface than a core library `Struct`, provide more structure than a `Hash` or hash-like object (such as an `OpenStruct` or `Hashie::Mash`), and are completely independent from the source of the data. Need to load seed data from a YAML configuration file, perform operations in a SQL database, cross-reference with a MongoDB data store, and use an in-memory data array for lightning-fast tests? A `Stannum::Entity` won't fight you every step of the way.
21
21
 
22
22
  ### Why Stannum?
23
23
 
24
- Stannum is not tied to any framework. You can create constraints and contracts to validate Ruby objects and Structs, data structures such as Arrays, Hashes, and Sets, and even framework objects such as `ActiveRecord::Model`s and `Mongoid::Document`s.
24
+ Stannum is not tied to any framework. You can create constraints and contracts to validate Ruby objects and Entities, data structures such as Arrays, Hashes, and Sets, and even framework objects such as `ActiveRecord::Model`s and `Mongoid::Document`s.
25
25
 
26
26
  Still, most projects and applications use one framework to handle their data. Why use Stannum constraints?
27
27
 
@@ -33,7 +33,7 @@ Still, most projects and applications use one framework to handle their data. Wh
33
33
 
34
34
  ### Compatibility
35
35
 
36
- Stannum is tested against Ruby (MRI) 2.6 through 3.0.
36
+ Stannum is tested against Ruby (MRI) 2.7 through 3.2.
37
37
 
38
38
  ### Documentation
39
39
 
@@ -331,7 +331,7 @@ contract.does_not_match?({ color: 'blue', shape: 'square'})
331
331
 
332
332
  Note that for an object that partially matches the contract, both `#matches?` and `#does_not_match?` methods will return false. If you want to check whether **any** of the constraints do not match the object, use the `#matches?` method and apply the `!` boolean negation operator (or switch from an `if` to an `unless`).
333
333
 
334
- #### Property Constraints
334
+ #### Constraining Properties
335
335
 
336
336
  Constraints can also define constraints on the *properties* of the matched object. This is a powerful feature for defining validations on objects and nested data structures. To define a property constraint, use the `property` macro in a contract constructor block, or use the `#add_property_constraint` method on an existing contract.
337
337
 
@@ -725,16 +725,20 @@ errors.first.message
725
725
 
726
726
  Stannum uses the strategy pattern to determine how error messages are generated. You can pass the `strategy:` keyword to `#with_messages` to force Stannum to use the specified strategy, or set the `Stannum::Messages.strategy` property to define the default for your application. The default strategy for Stannum uses an I18n-like configuration file to define messages based on the type and optionally the data for each error.
727
727
 
728
- <a id="structs"></a>
728
+ <a id="entities"></a>
729
729
 
730
- ### Structs
730
+ ### Entities
731
731
 
732
- While constraints and contracts are used to validate data, structs are used to define and structure that data. Each `Stannum::Struct` contains a specific set of attributes, and each attribute has a type definition that is a `Class` or `Module` or the name of a Class or Module.
732
+ While constraints and contracts are used to validate data, entities are used to define and structure that data. Each `Stannum::Entity` contains a specific set of attributes, and each attribute has a type definition that is a `Class` or `Module` or the name of a Class or Module.
733
733
 
734
- Structs are defined by creating a new class and including `Stannum::Struct`:
734
+ Entities are defined by creating a new class and including `Stannum::Entity`:
735
735
 
736
736
  ```ruby
737
+ require 'stannum'
738
+
737
739
  class Gadget
740
+ include Stannum::Entity
741
+
738
742
  attribute :name, String
739
743
  attribute :description, String, optional: true
740
744
  attribute :quantity, Integer, default: 0
@@ -763,21 +767,21 @@ gadget[:description]
763
767
 
764
768
  Our `Gadget` class has three attributes: `#name`, `#description`, and `#quantity`, which we are defining using the `.attribute` class method.
765
769
 
766
- We can initialize a gadget with values by passing the desired attributes to `.new`. We can read or write the attributes using either dot `.` notation or `#[]` notation. Finally, we can access all of a struct's attributes and values using the `#attributes` method.
770
+ We can initialize a gadget with values by passing the desired attributes to `.new`. We can read or write the attributes using either dot `.` notation or `#[]` notation. Finally, we can access all of a entity's attributes and values using the `#attributes` method.
767
771
 
768
- `Stannum::Struct` defines a number of helper methods for interacting with a struct's attributes:
772
+ `Stannum::Entity` defines a number of helper methods for interacting with a entity's attributes:
769
773
 
770
774
  - `#[](attribute)`: Returns the value of the given attribute.
771
775
  - `#[]=(attribute, value)`: Writes the given value to the given attribute.
772
- - `#assign_attributes(values)`: Updates the struct's attributes using the given values. If an attribute is not given, that value is unchanged.
776
+ - `#assign_attributes(values)`: Updates the entity's attributes using the given values. If an attribute is not given, that value is unchanged.
773
777
  - `#attributes`: Returns a hash containing the attribute keys and values.
774
- - `#attributes=(values)`: Sets the struct's attributes to the given values. If an attribute is not given, that attribute is set to `nil`.
778
+ - `#attributes=(values)`: Sets the entity's attributes to the given values. If an attribute is not given, that attribute is set to `nil`.
775
779
 
776
- For all of the above methods, if a given attribute is invalid or the attribute is not defined on the struct, an `ArgumentError` will be raised.
780
+ For all of the above methods, if a given attribute is invalid or the attribute is not defined on the entity, an `ArgumentError` will be raised.
777
781
 
778
782
  #### Attributes
779
783
 
780
- A struct's attributes are defined using the `.attribute` class method, and can be accessed and enumerated using the `.attributes` class method on the struct class or via the `::Attributes` constant. Internally, each attribute is represented by a `Stannum::Attribute` instance, which stores the attribute's `:name`, `:type`, and `:attributes`.
784
+ A entity's attributes are defined using the `.attribute` class method, and can be accessed and enumerated using the `.attributes` class method on the entity class or via the `::Attributes` constant. Internally, each attribute is represented by a `Stannum::Attribute` instance, which stores the attribute's `:name`, `:type`, and `:attributes`.
781
785
 
782
786
  ```ruby
783
787
  Gadget::Attributes
@@ -796,11 +800,11 @@ Gadget.attributes[:quantity].options
796
800
 
797
801
  ##### Default Values
798
802
 
799
- Structs can define default values for attributes by passing a `:default` value to the `.attribute` call.
803
+ Entities can define default values for attributes by passing a `:default` value to the `.attribute` call.
800
804
 
801
805
  ```ruby
802
806
  class LightsCounter
803
- include Stannum::Struct
807
+ include Stannum::Entity
804
808
 
805
809
  attribute :count, Integer, default: 4
806
810
  end
@@ -811,11 +815,11 @@ LightsCounter.new.count
811
815
 
812
816
  ##### Optional Attributes
813
817
 
814
- Struct classes can also mark attributes as `optional`. When a struct is validated (see [Validation](#structs-validation), below), optional attributes will pass with a value of `nil`.
818
+ Entity classes can also mark attributes as `optional`. When an entity is validated (see [Validation](#entities-validation), below), optional attributes will pass with a value of `nil`.
815
819
 
816
820
  ```ruby
817
821
  class WhereWeAreGoing
818
- include Stannum::Struct
822
+ include Stannum::Entity
819
823
 
820
824
  attribute :roads, Object, optional: true
821
825
  end
@@ -823,11 +827,11 @@ end
823
827
 
824
828
  `Stannum` supports both `:optional` and `:required` as keys. Passing either `optional: true` or `required: false` will mark the attribute as optional. Attributes are required by default.
825
829
 
826
- <a id="structs-validation"></a>
830
+ <a id="entities-validation"></a>
827
831
 
828
832
  #### Validation
829
833
 
830
- Each `Stannum::Struct` automatically generates a contract that can be used to validate instances of the struct class. The contract can be accessed using the `.contract` class method or via the `::Contract` constant.
834
+ Each `Stannum::Entity` automatically generates a contract that can be used to validate instances of the entity class. The contract can be accessed using the `.contract` class method or via the `::Contract` constant.
831
835
 
832
836
  ```ruby
833
837
  class Gadget
@@ -1091,6 +1095,66 @@ constraint.matches?(:a_symbol)
1091
1095
  #=> true
1092
1096
  ```
1093
1097
 
1098
+ #### Property Constraints
1099
+
1100
+ Property constraints match against the properties of the object.
1101
+
1102
+ **Do Not Match Property Constraint**
1103
+
1104
+ Matches if none of the values of the given properties are equal to the value of the expected property.
1105
+
1106
+ ```ruby
1107
+ UpdatePassword = Struct.new(:old_password, :new_password)
1108
+ constraint = Stannum::Constraints::Properties::DoNotMatchProperty.new(
1109
+ :old_password,
1110
+ :new_password
1111
+ )
1112
+
1113
+ params = UpdatePassword.new('tronlives', 'ifightfortheusers')
1114
+ constraint.matches?(params)
1115
+ #=> true
1116
+
1117
+ params = UpdatePassword.new('tronlives', 'tronlives')
1118
+ constraint.matches?(params)
1119
+ #=> false
1120
+ constraint.errors_for(params)
1121
+ #=> [
1122
+ {
1123
+ path: [:confirmation],
1124
+ type: 'stannum.constraints.is_equal_to',
1125
+ data: { expected: '[FILTERED]', actual: '[FILTERED]' }
1126
+ }
1127
+ ]
1128
+ ```
1129
+
1130
+ **Match Property Constraint**
1131
+
1132
+ Matches if all the values of the given properties are equal to the value of the expected property.
1133
+
1134
+ ```ruby
1135
+ ConfirmPassword = Struct.new(:password, :confirmation)
1136
+ constraint = Stannum::Constraints::Properties::MatchProperty.new(
1137
+ :password,
1138
+ :confirmation
1139
+ )
1140
+
1141
+ params = ConfirmPassword.new('tronlives', 'ifightfortheusers')
1142
+ constraint.matches?(params)
1143
+ #=> false
1144
+ constraint.errors_for(params)
1145
+ #=> [
1146
+ {
1147
+ path: [:confirmation],
1148
+ type: 'stannum.constraints.is_not_equal_to',
1149
+ data: { expected: '[FILTERED]', actual: '[FILTERED]' }
1150
+ }
1151
+ ]
1152
+
1153
+ params = ConfirmPassword.new('tronlives', 'tronlives')
1154
+ constraint.matches?(params)
1155
+ #=> true
1156
+ ```
1157
+
1094
1158
  #### Type Constraints
1095
1159
 
1096
1160
  Stannum also defines a set of built-in type constraints. Unless otherwise noted, these are identical to a [Type Constraint](#builtin-constraints-type) with the given Class.
data/config/locales/en.rb CHANGED
@@ -23,13 +23,27 @@
23
23
  is_not_equal_to: 'is not equal to',
24
24
  is_not_in_list: 'is not in the list',
25
25
  is_not_in_union: 'does not match any of the constraints',
26
- is_not_type: ->(_type, data) { "is not a #{data[:type]}" },
26
+ is_not_type: lambda do |_type, data|
27
+ if data[:required]
28
+ "is not a #{data[:type]}"
29
+ else
30
+ "is not a #{data[:type]} or nil"
31
+ end
32
+ end,
27
33
  is_not_value: 'is not the expected value',
28
- is_type: ->(_type, data) { "is a #{data[:type]}" },
34
+ is_type: lambda do |_type, data|
35
+ if data[:required]
36
+ "is a #{data[:type]}"
37
+ else
38
+ "is a #{data[:type]} or nil"
39
+ end
40
+ end,
29
41
  is_value: 'is the expected value',
30
42
  parameters: {
31
43
  extra_arguments: 'has extra arguments',
32
- extra_keywords: 'has extra keywords'
44
+ extra_keywords: 'has extra keywords',
45
+ no_extra_arguments: 'does not have extra arguments',
46
+ no_extra_keywords: 'does not have extra keywords'
33
47
  },
34
48
  tuples: {
35
49
  extra_items: 'has extra items',
@@ -74,6 +74,8 @@ module Stannum::Constraints
74
74
  #
75
75
  # constraint.does_not_match?(object) #=> true
76
76
  #
77
+ # @param actual [Object] The object to match.
78
+ #
77
79
  # @return [true, false] false if the object matches the expected properties
78
80
  # or behavior, otherwise true.
79
81
  #
@@ -143,6 +145,8 @@ module Stannum::Constraints
143
145
  # errors.class #=> Stannum::Errors
144
146
  # errors.to_a #=> [{ type: 'some_error', message: 'some error message' }]
145
147
  #
148
+ # @param actual [Object] The object to match.
149
+ #
146
150
  # @see #errors_for
147
151
  # @see #matches?
148
152
  def match(actual)
@@ -152,8 +156,12 @@ module Stannum::Constraints
152
156
  end
153
157
 
154
158
  # @overload matches?(actual)
159
+ # Checks that the given object matches the constraint.
160
+ #
161
+ # @param actual [Object] The object to match.
155
162
  #
156
- # Checks that the given object matches the constraint.
163
+ # @return [true, false] true if the object matches the expected properties
164
+ # or behavior, otherwise false.
157
165
  #
158
166
  # @example Checking a matching object.
159
167
  # constraint = CustomConstraint.new
@@ -167,9 +175,6 @@ module Stannum::Constraints
167
175
  #
168
176
  # constraint.matches?(object) #=> false
169
177
  #
170
- # @return [true, false] true if the object matches the expected properties
171
- # or behavior, otherwise false.
172
- #
173
178
  # @see #does_not_match?
174
179
  def matches?(_actual)
175
180
  false
@@ -221,6 +226,8 @@ module Stannum::Constraints
221
226
  # false and the generated errors for that object. If the object does not
222
227
  # match the constraint, #negated_match will return true.
223
228
  #
229
+ # @param actual [Object] The object to match.
230
+ #
224
231
  # @example Checking a matching object.
225
232
  # constraint = CustomConstraint.new
226
233
  # object = MatchingObject.new
@@ -1,16 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'stannum/constraints/hashes'
4
+ require 'stannum/support/coercion'
4
5
 
5
6
  module Stannum::Constraints::Hashes
6
7
  # Constraint for validating the keys of a hash-like object.
7
8
  #
9
+ # When using this constraint, the keys must be strings or symbols, and the
10
+ # hash keys must be of the same type. A constraint configured with string keys
11
+ # will not match a hash with symbol keys, and vice versa.
12
+ #
8
13
  # @example
9
- # keys = %[fuel mass size]
14
+ # keys = %i[fuel mass size]
10
15
  # constraint = Stannum::Constraints::Hashes::ExpectedKeys.new(keys)
11
16
  #
12
17
  # constraint.matches?({}) #=> true
13
18
  # constraint.matches?({ fuel: 'Monopropellant' }) #=> true
19
+ # constraint.matches?({ 'fuel' => 'Monopropellant' }) #=> false
14
20
  # constraint.matches?({ electric: true, fuel: 'Xenon' }) #=> false
15
21
  # constraint.matches?({ fuel: 'LF/O', mass: '1 ton', size: 'Medium' })
16
22
  # #=> true
@@ -59,13 +65,15 @@ module Stannum::Constraints::Hashes
59
65
  end
60
66
 
61
67
  each_extra_key(actual) do |key, value|
68
+ key = Stannum::Support::Coercion.error_key(key)
69
+
62
70
  errors[key].add(type, value: value)
63
71
  end
64
72
 
65
73
  errors
66
74
  end
67
75
 
68
- # @return [Array] the expected keys.
76
+ # @return [Set] the expected keys.
69
77
  def expected_keys
70
78
  keys = options[:expected_keys]
71
79
 
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/hashes'
4
+ require 'stannum/constraints/hashes/extra_keys'
5
+
6
+ module Stannum::Constraints::Hashes
7
+ # Constraint for validating the keys of an indifferent hash-like object.
8
+ #
9
+ # When using this constraint, the keys must be strings or symbols, but it does
10
+ # not matter which - a constraint configured with string keys will match a
11
+ # hash with symbol keys, and vice versa.
12
+ #
13
+ # @example
14
+ # keys = %i[fuel mass size]
15
+ # constraint = Stannum::Constraints::Hashes::ExpectedKeys.new(keys)
16
+ #
17
+ # constraint.matches?({}) #=> true
18
+ # constraint.matches?({ fuel: 'Monopropellant' }) #=> true
19
+ # constraint.matches?({ 'fuel' => 'Monopropellant' }) #=> true
20
+ # constraint.matches?({ electric: true, fuel: 'Xenon' }) #=> false
21
+ # constraint.matches?({ fuel: 'LF/O', mass: '1 ton', size: 'Medium' })
22
+ # #=> true
23
+ # constraint.matches?(
24
+ # { fuel: 'LF', mass: '2 tons', nuclear: true, size: 'Medium' }
25
+ # )
26
+ # #=> false
27
+ class IndifferentExtraKeys < Stannum::Constraints::Hashes::ExtraKeys
28
+ # @return [Set] the expected keys.
29
+ def expected_keys
30
+ keys = options[:expected_keys]
31
+
32
+ return indifferent_keys_for(keys) unless keys.is_a?(Proc)
33
+
34
+ indifferent_keys_for(keys.call)
35
+ end
36
+
37
+ private
38
+
39
+ def indifferent_keys_for(keys)
40
+ Set.new(
41
+ keys.reduce([]) do |ary, key|
42
+ ary << key.to_s << key.intern
43
+ end
44
+ )
45
+ end
46
+ end
47
+ end
@@ -5,7 +5,11 @@ require 'stannum/constraints'
5
5
  module Stannum::Constraints
6
6
  # Namespace for Hash-specific constraints.
7
7
  module Hashes
8
- autoload :ExtraKeys, 'stannum/constraints/hashes/extra_keys'
9
- autoload :IndifferentKey, 'stannum/constraints/hashes/indifferent_key'
8
+ autoload :ExtraKeys,
9
+ 'stannum/constraints/hashes/extra_keys'
10
+ autoload :IndifferentExtraKeys,
11
+ 'stannum/constraints/hashes/indifferent_extra_keys'
12
+ autoload :IndifferentKey,
13
+ 'stannum/constraints/hashes/indifferent_key'
10
14
  end
11
15
  end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/parameters'
4
+ require 'stannum/constraints/tuples/extra_items'
5
+
6
+ module Stannum::Constraints::Parameters
7
+ # Validates that the arguments passed to a method have no extra items.
8
+ #
9
+ # @example
10
+ # constraint = Stannum::Constraints::Parameters::ExtraArguments.new(3)
11
+ #
12
+ # constraint.matches?([]) #=> true
13
+ # constraint.matches?([1]) #=> true
14
+ # constraint.matches?([1, 2, 3]) #=> true
15
+ # constraint.matches?([1, 2, 3, 4]) #=> false
16
+ class ExtraArguments < Stannum::Constraints::Tuples::ExtraItems
17
+ # The :type of the error generated for a matching object.
18
+ NEGATED_TYPE = 'stannum.constraints.parameters.no_extra_arguments'
19
+
20
+ # The :type of the error generated for a non-matching object.
21
+ TYPE = 'stannum.constraints.parameters.extra_arguments'
22
+ end
23
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'stannum/constraints/hashes/extra_keys'
4
+ require 'stannum/constraints/parameters'
5
+
6
+ module Stannum::Constraints::Parameters
7
+ # Validates that the keywords passed to a method have no extra keys.
8
+ #
9
+ # @example
10
+ # keys = %[fuel mass size]
11
+ # constraint = Stannum::Constraints::Parameters::ExpectedKeywords.new(keys)
12
+ #
13
+ # constraint.matches?({}) #=> true
14
+ # constraint.matches?({ fuel: 'Monopropellant' }) #=> true
15
+ # constraint.matches?({ electric: true, fuel: 'Xenon' }) #=> false
16
+ # constraint.matches?({ fuel: 'LF/O', mass: '1 ton', size: 'Medium' })
17
+ # #=> true
18
+ # constraint.matches?(
19
+ # { fuel: 'LF', mass: '2 tons', nuclear: true, size: 'Medium' }
20
+ # )
21
+ # #=> false
22
+ class ExtraKeywords < Stannum::Constraints::Hashes::ExtraKeys
23
+ # The :type of the error generated for a matching object.
24
+ NEGATED_TYPE = 'stannum.constraints.parameters.no_extra_keywords'
25
+
26
+ # The :type of the error generated for a non-matching object.
27
+ TYPE = 'stannum.constraints.parameters.extra_keywords'
28
+ end
29
+ 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 method parameters.
7
+ module Parameters
8
+ autoload :ExtraArguments, 'stannum/constraints/parameters/extra_arguments'
9
+ autoload :ExtraKeywords, 'stannum/constraints/parameters/extra_keywords'
10
+ end
11
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbelt'
4
+
5
+ require 'stannum/constraints/properties'
6
+
7
+ module Stannum::Constraints::Properties
8
+ # Abstract base class for property constraints.
9
+ class Base < Stannum::Constraints::Base
10
+ # Default parameter names to filter out of errors.
11
+ FILTERED_PARAMETERS = %i[
12
+ passw
13
+ secret
14
+ token
15
+ _key
16
+ crypt
17
+ salt
18
+ certificate
19
+ otp
20
+ ssn
21
+ ].freeze
22
+
23
+ # @param property_names [Array<String, Symbol>] the name or names of the
24
+ # properties to match.
25
+ # @param options [Hash<Symbol, Object>] configuration options for the
26
+ # constraint. Defaults to an empty Hash.
27
+ #
28
+ # @option options allow_empty [true, false] if true, will match against an
29
+ # object with empty property values, such as an empty string.
30
+ # @option options allow_nil [true, false] if true, will match against an
31
+ # object with nil property values.
32
+ def initialize(*property_names, **options)
33
+ @property_names = property_names
34
+
35
+ validate_property_names
36
+
37
+ super(
38
+ allow_empty: !!options[:allow_empty],
39
+ allow_nil: !!options[:allow_nil],
40
+ property_names: property_names,
41
+ **options
42
+ )
43
+ end
44
+
45
+ # @return [Array<String, Symbol>] the name or names of the properties to
46
+ # match.
47
+ attr_reader :property_names
48
+
49
+ # @return [true, false] if true, will match against an object with empty
50
+ # property values, such as an empty string.
51
+ def allow_empty?
52
+ options[:allow_empty]
53
+ end
54
+
55
+ # @return [true, false] if true, will match against an object with nil
56
+ # property values.
57
+ def allow_nil?
58
+ options[:allow_nil]
59
+ end
60
+
61
+ private
62
+
63
+ def can_match_properties?(actual)
64
+ actual.respond_to?(:[])
65
+ end
66
+
67
+ def each_property(actual)
68
+ return to_enum(__method__, actual) unless block_given?
69
+
70
+ property_names.each do |property_name|
71
+ yield property_name, actual[property_name]
72
+ end
73
+ end
74
+
75
+ def empty?(value)
76
+ value.respond_to?(:empty?) && value.empty?
77
+ end
78
+
79
+ def filter_parameters?
80
+ return @filter_parameters unless @filter_parameters.nil?
81
+
82
+ filters = filtered_parameters.map { |param| Regexp.new(param.to_s) }
83
+
84
+ @filter_parameters =
85
+ property_names.any? do |property_name|
86
+ filters.any? { |filter| filter.match?(property_name.to_s) }
87
+ end
88
+ end
89
+
90
+ def filtered_parameters
91
+ return Rails.configuration.filter_parameters if defined?(Rails)
92
+
93
+ FILTERED_PARAMETERS
94
+ end
95
+
96
+ def invalid_object_errors(errors)
97
+ errors.add(
98
+ Stannum::Constraints::Signature::TYPE,
99
+ methods: %i[[]],
100
+ missing: %i[[]]
101
+ )
102
+ end
103
+
104
+ def skip_property?(value)
105
+ (allow_empty? && empty?(value)) || (allow_nil? && value.nil?)
106
+ end
107
+
108
+ def tools
109
+ SleepingKingStudios::Tools::Toolbelt.instance
110
+ end
111
+
112
+ def validate_property_names
113
+ if property_names.empty?
114
+ raise ArgumentError, "property names can't be empty"
115
+ end
116
+
117
+ property_names.each.with_index do |property_name, index|
118
+ tools
119
+ .assertions
120
+ .validate_name(property_name, as: "property name at #{index}")
121
+ end
122
+ end
123
+ end
124
+ end