stannum 0.1.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +85 -21
- data/config/locales/en.rb +17 -3
- data/lib/stannum/constraints/base.rb +11 -4
- data/lib/stannum/constraints/hashes/extra_keys.rb +10 -2
- data/lib/stannum/constraints/hashes/indifferent_extra_keys.rb +47 -0
- data/lib/stannum/constraints/hashes.rb +6 -2
- data/lib/stannum/constraints/parameters/extra_arguments.rb +23 -0
- data/lib/stannum/constraints/parameters/extra_keywords.rb +29 -0
- data/lib/stannum/constraints/parameters.rb +11 -0
- data/lib/stannum/constraints/properties/base.rb +124 -0
- data/lib/stannum/constraints/properties/do_not_match_property.rb +117 -0
- data/lib/stannum/constraints/properties/match_property.rb +117 -0
- data/lib/stannum/constraints/properties/matching.rb +112 -0
- data/lib/stannum/constraints/properties.rb +17 -0
- data/lib/stannum/constraints/tuples/extra_items.rb +1 -1
- data/lib/stannum/constraints/type.rb +1 -1
- data/lib/stannum/constraints/types/hash_type.rb +6 -2
- data/lib/stannum/constraints.rb +2 -0
- data/lib/stannum/contracts/builder.rb +13 -2
- data/lib/stannum/contracts/hash_contract.rb +14 -0
- data/lib/stannum/contracts/indifferent_hash_contract.rb +13 -0
- data/lib/stannum/contracts/parameters/arguments_contract.rb +2 -7
- data/lib/stannum/contracts/parameters/keywords_contract.rb +2 -7
- data/lib/stannum/contracts/tuple_contract.rb +1 -1
- data/lib/stannum/entities/attributes.rb +218 -0
- data/lib/stannum/entities/constraints.rb +177 -0
- data/lib/stannum/entities/properties.rb +186 -0
- data/lib/stannum/entities.rb +13 -0
- data/lib/stannum/entity.rb +83 -0
- data/lib/stannum/errors.rb +3 -3
- data/lib/stannum/messages/default_loader.rb +95 -0
- data/lib/stannum/messages/default_strategy.rb +31 -50
- data/lib/stannum/messages.rb +1 -0
- data/lib/stannum/rspec/match_errors_matcher.rb +6 -6
- data/lib/stannum/rspec/validate_parameter_matcher.rb +10 -9
- data/lib/stannum/schema.rb +78 -37
- data/lib/stannum/struct.rb +12 -346
- data/lib/stannum/support/coercion.rb +19 -0
- data/lib/stannum/version.rb +1 -1
- data/lib/stannum.rb +3 -0
- metadata +29 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b13aa2cda5a99f544b2e7c281147f51b4e4fa02e394e4b6d017dcc2b1f03705d
|
4
|
+
data.tar.gz: e84dc0c10ccfd8989dcec729c485136c099257079df57cb44e82befb42ebf9b8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
- [
|
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::
|
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
|
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.
|
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
|
-
####
|
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="
|
728
|
+
<a id="entities"></a>
|
729
729
|
|
730
|
-
###
|
730
|
+
### Entities
|
731
731
|
|
732
|
-
While constraints and contracts are used to validate data,
|
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
|
-
|
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
|
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::
|
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
|
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
|
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
|
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
|
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
|
-
|
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::
|
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
|
-
|
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::
|
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="
|
830
|
+
<a id="entities-validation"></a>
|
827
831
|
|
828
832
|
#### Validation
|
829
833
|
|
830
|
-
Each `Stannum::
|
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:
|
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:
|
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
|
-
#
|
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 [
|
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,
|
9
|
-
|
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
|