assertion 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +20 -0
- data/README.md +54 -43
- data/lib/assertion.rb +37 -6
- data/lib/assertion/attributes.rb +54 -0
- data/lib/assertion/base.rb +7 -65
- data/lib/assertion/guard.rb +99 -0
- data/lib/assertion/{exceptions/invalid_error.rb → invalid_error.rb} +5 -1
- data/lib/assertion/inversion.rb +1 -1
- data/lib/assertion/messages.rb +65 -0
- data/lib/assertion/state.rb +1 -1
- data/lib/assertion/transprocs/inflector.rb +2 -0
- data/lib/assertion/version.rb +1 -1
- data/spec/integration/assertion_spec.rb +3 -12
- data/spec/integration/guard_spec.rb +29 -0
- data/spec/{integration → shared}/en.yml +1 -1
- data/spec/shared/i18n.rb +21 -0
- data/spec/unit/assertion/attributes_spec.rb +97 -0
- data/spec/unit/assertion/base_spec.rb +6 -103
- data/spec/unit/assertion/exceptions/invalid_error_spec.rb +11 -10
- data/spec/unit/assertion/guard_spec.rb +82 -0
- data/spec/unit/assertion/messages_spec.rb +41 -0
- data/spec/unit/assertion_spec.rb +69 -12
- metadata +18 -15
- data/lib/assertion/exceptions/name_error.rb +0 -29
- data/lib/assertion/exceptions/not_implemented_error.rb +0 -29
- data/lib/assertion/transprocs/i18n.rb +0 -55
- data/spec/unit/assertion/exceptions/name_error_spec.rb +0 -26
- data/spec/unit/assertion/exceptions/not_implemented_error_spec.rb +0 -26
- data/spec/unit/assertion/transprocs/i18n/to_scope_spec.rb +0 -19
- data/spec/unit/assertion/transprocs/i18n/translate_spec.rb +0 -28
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9fbab813ababd1c0224226d00aa2144dc682b6d
|
4
|
+
data.tar.gz: 30d48a47833d65670e480b29196caa1aec783040
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 521a9d990f59e2236344f712df369f211e3635c9f6828da4883135962b2f6e51a0e006e0fc18612d9e3474c24a5bdf3681585982c665eec6e8caa21de58961e4
|
7
|
+
data.tar.gz: 3e3dacb62cc68dd317e674cbad9424607d4b4a12638445e54a41534f747f9c0af7c07edf1a13728d9b2223349b713f72d716cb9f6cb7525dd7f8e332b46a8978
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
## v0.1.0 2015-06-20
|
2
|
+
|
3
|
+
### Added
|
4
|
+
|
5
|
+
* New `Guard` base class for guarding objects (nepalez)
|
6
|
+
* New `Assertion.guards` builder method to provide specific guards (nepalez)
|
7
|
+
* Custom `Assertion::InvalidError#inspect` method that shows `#messages` (nepalez)
|
8
|
+
|
9
|
+
### Deleted
|
10
|
+
|
11
|
+
* Removed `Assertion::NotImplementedError` (use `NoMethodError` instead) (nepalez)
|
12
|
+
* Removed `Assertion::NameError` (use generic `NameError` instead) (nepalez)
|
13
|
+
|
14
|
+
### Internal
|
15
|
+
|
16
|
+
* Moved all message-related features to the `Assertion::Messages` (nepalez)
|
17
|
+
* Removed `Assertion::I18n` (not the part of public API) (nepalez)
|
18
|
+
* Extracted attribute DSL from `Assertion::Base` to `Assertion::Attributes` (nepalez)
|
19
|
+
|
20
|
+
[Compare v0.0.1...v0.1.0](https://github.com/nepalez/assertion/compare/v0.0.1...v0.1.0)
|
data/README.md
CHANGED
@@ -20,38 +20,38 @@ Immutable assertions and validations for PORO.
|
|
20
20
|
Synopsis
|
21
21
|
--------
|
22
22
|
|
23
|
-
The primary goal of the gem is to
|
23
|
+
The primary goal of the gem is to make assertions about <decoupled> objects.
|
24
24
|
|
25
25
|
No `ActiveSupport`, no mutation of any instances.
|
26
26
|
|
27
27
|
### Basic Usage
|
28
28
|
|
29
|
-
Define
|
30
|
-
Then implement the method `check` that should return
|
29
|
+
Define an assertion by inheriting it from the `Assertion::Base` class with attributes to which it should be applied.
|
30
|
+
Then implement the method `check` that should return a boolean value.
|
31
31
|
|
32
|
-
You can do it either in
|
32
|
+
You can do it either in the classic style:
|
33
33
|
|
34
34
|
```ruby
|
35
35
|
class IsAdult < Assertion::Base
|
36
36
|
attribute :age, :name
|
37
37
|
|
38
38
|
def check
|
39
|
-
age >= 18
|
39
|
+
age.to_i >= 18
|
40
40
|
end
|
41
41
|
end
|
42
42
|
```
|
43
43
|
|
44
|
-
or
|
44
|
+
or with more verbose builder:
|
45
45
|
|
46
46
|
```ruby
|
47
47
|
IsAdult = Assertion.about :age, :name do
|
48
|
-
age >= 18
|
48
|
+
age.to_i >= 18
|
49
49
|
end
|
50
50
|
```
|
51
51
|
|
52
|
-
Define translations
|
52
|
+
Define translations to describe both the *right* and *wrong* states of the assertion.
|
53
53
|
|
54
|
-
All the
|
54
|
+
All the attributes are available in translations (that's why we declared the `name` as an attribute):
|
55
55
|
|
56
56
|
```yaml
|
57
57
|
# config/locales/en.yml
|
@@ -63,7 +63,7 @@ en:
|
|
63
63
|
wrong: "%{name} is a child yet (age %{age})"
|
64
64
|
```
|
65
65
|
|
66
|
-
Check a state of
|
66
|
+
Check a state of an assertion for some argument(s), using class method `[]`:
|
67
67
|
|
68
68
|
```ruby
|
69
69
|
john = { name: 'John', age: 10, gender: :male }
|
@@ -81,9 +81,10 @@ state.messages # => ["John is a child yet (age 10)"]
|
|
81
81
|
state.validate! # => #<Assertion::InvalidError @messages=["John is a child yet (age 10)"]>
|
82
82
|
```
|
83
83
|
|
84
|
-
|
84
|
+
Inversion
|
85
|
+
---------
|
85
86
|
|
86
|
-
Use the `.not` *class* method to negate
|
87
|
+
Use the `.not` *class* method to negate the assertion:
|
87
88
|
|
88
89
|
```ruby
|
89
90
|
jack = { name: 'Jack', age: 21, gender: :male }
|
@@ -92,7 +93,8 @@ IsAdult.not[jack]
|
|
92
93
|
# => #<Assertion::State @state=false, @messages=["Jack is already an adult (age 21)"]>
|
93
94
|
```
|
94
95
|
|
95
|
-
|
96
|
+
Composition
|
97
|
+
-----------
|
96
98
|
|
97
99
|
You can compose assertion states (results):
|
98
100
|
|
@@ -121,54 +123,63 @@ state = IsAdult[jane] & IsMale[jane]
|
|
121
123
|
# => #<Assertion::State @state=false, @messages=["Jane is a child yet (age 16)", "Jane is a female"]>
|
122
124
|
```
|
123
125
|
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
If you are used to "rails style" validations, provide a validator from assertions:
|
126
|
+
Guards
|
127
|
+
------
|
128
128
|
|
129
|
-
|
130
|
-
class User
|
131
|
-
include Virtus.model
|
129
|
+
The guard class is a lean wrapper around the state of its object.
|
132
130
|
|
133
|
-
|
134
|
-
attribute :age, Integer
|
135
|
-
attribute :gender, Symbol
|
136
|
-
end
|
131
|
+
It defines the `#state` for the object and checks if the state is valid:
|
137
132
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
133
|
+
```ruby
|
134
|
+
class VoterOnly < Assertion::Guard
|
135
|
+
alias_method :user, :object
|
142
136
|
|
143
137
|
def state
|
144
|
-
IsAdult[attributes] &
|
138
|
+
IsAdult[user.attributes] & IsCitizen[user.attributes]
|
145
139
|
end
|
146
140
|
end
|
147
|
-
|
148
|
-
judy = Women.new(name: "Judy", age: 15, gender: :female)
|
149
|
-
judy.valdate!
|
150
|
-
# => #<Assertion::InvalidError @messages=["Judy is a child yet (age 15)"]>
|
151
141
|
```
|
152
142
|
|
153
|
-
|
154
|
-
----------
|
143
|
+
Or using the verbose builder `Assertion.guards`:
|
155
144
|
|
156
|
-
|
145
|
+
```ruby
|
146
|
+
VoterOnly = Assertion.guards :user do
|
147
|
+
IsAdult[user.attributes] & IsCitizen[user.attributes]
|
148
|
+
end
|
149
|
+
```
|
157
150
|
|
158
|
-
|
151
|
+
When the guard is called for some object, its calls `#validate!` and then returns the source object. That simple.
|
159
152
|
|
160
153
|
```ruby
|
161
|
-
|
154
|
+
jack = OpenStruct.new(name: "Jack", age: 15, citizen: true)
|
155
|
+
john = OpenStruct.new(name: "John", age: 34, citizen: true)
|
156
|
+
|
157
|
+
voter = VoterOnly[jack]
|
158
|
+
# => #<Assertion::InvalidError @messages=["Jack is a child yet (age 15)"]
|
162
159
|
|
163
|
-
|
164
|
-
# => #<
|
160
|
+
voter = VoterOnly[john]
|
161
|
+
# => #<OpenStruct @name="John", @age=34>
|
165
162
|
```
|
166
163
|
|
167
|
-
|
164
|
+
Naming Convention
|
165
|
+
-----------------
|
166
|
+
|
167
|
+
This is not necessary, but for verbosity you could follow the rules:
|
168
|
+
|
169
|
+
* use the prefix `Is` for assertions (like `IsAdult`)
|
170
|
+
* use the suffix `Only` for guards (like `AdultOnly`)
|
171
|
+
|
172
|
+
Edge Cases
|
173
|
+
----------
|
174
|
+
|
175
|
+
You cannot define attributes with names already defined as istance methods:
|
168
176
|
|
169
177
|
```ruby
|
170
|
-
IsAdult = Assertion.about :check
|
171
|
-
# => #<Assertion::NameError @message="Wrong name(s) for attribute(s): check
|
178
|
+
IsAdult = Assertion.about :check
|
179
|
+
# => #<Assertion::NameError @message="Wrong name(s) for attribute(s): check">
|
180
|
+
|
181
|
+
AdultOnly = Assertion.guards :state
|
182
|
+
# => #<Assertion::NameError @message="Wrong name(s) for attribute(s): state">
|
172
183
|
```
|
173
184
|
|
174
185
|
Installation
|
data/lib/assertion.rb
CHANGED
@@ -1,20 +1,19 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
3
|
require "transproc"
|
4
|
-
require "i18n"
|
5
4
|
|
6
5
|
require_relative "assertion/transprocs/inflector"
|
7
|
-
require_relative "assertion/transprocs/i18n"
|
8
6
|
require_relative "assertion/transprocs/list"
|
9
7
|
|
10
|
-
require_relative "assertion/
|
11
|
-
require_relative "assertion/
|
12
|
-
require_relative "assertion/
|
8
|
+
require_relative "assertion/invalid_error"
|
9
|
+
require_relative "assertion/attributes"
|
10
|
+
require_relative "assertion/messages"
|
13
11
|
|
14
12
|
require_relative "assertion/state"
|
15
13
|
require_relative "assertion/base"
|
16
14
|
require_relative "assertion/inversion"
|
17
15
|
require_relative "assertion/inverter"
|
16
|
+
require_relative "assertion/guard"
|
18
17
|
|
19
18
|
# The module allows declaring assertions (assertions) about various objects,
|
20
19
|
# and apply (validate) them to concrete data.
|
@@ -66,7 +65,7 @@ module Assertion
|
|
66
65
|
# @param [Proc] block
|
67
66
|
# The content for the `check` method
|
68
67
|
#
|
69
|
-
# @return [
|
68
|
+
# @return [Class] The specific assertion class
|
70
69
|
#
|
71
70
|
def self.about(*attributes, &block)
|
72
71
|
klass = Class.new(Base)
|
@@ -76,4 +75,36 @@ module Assertion
|
|
76
75
|
klass
|
77
76
|
end
|
78
77
|
|
78
|
+
# Builds the subclass of `Assertion::Guard` with given attribute
|
79
|
+
# (alias for the `object`) and implementation of the `#state` method.
|
80
|
+
#
|
81
|
+
# @example
|
82
|
+
# VoterOnly = Assertion.guards :user do
|
83
|
+
# IsAdult[user.attributes] & IsCitizen[user.attributes]
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# # This is the same as:
|
87
|
+
# class VoterOnly < Assertion::Guard
|
88
|
+
# alias_method :user, :object
|
89
|
+
#
|
90
|
+
# def state
|
91
|
+
# IsAdult[user.attributes] & IsCitizen[user.attributes]
|
92
|
+
# end
|
93
|
+
# end
|
94
|
+
#
|
95
|
+
# @param [Symbol] attribute
|
96
|
+
# The alias for the `object` attribute
|
97
|
+
# @param [Proc] block
|
98
|
+
# The content for the `state` method
|
99
|
+
#
|
100
|
+
# @return [Class] The specific guard class
|
101
|
+
#
|
102
|
+
def self.guards(attribute = nil, &block)
|
103
|
+
klass = Class.new(Guard)
|
104
|
+
klass.public_send(:attribute, attribute) if attribute
|
105
|
+
klass.__send__(:define_method, :state, &block) if block_given?
|
106
|
+
|
107
|
+
klass
|
108
|
+
end
|
109
|
+
|
79
110
|
end # module Assertion
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# Module Attributes provides features to define and store a list of attributes
|
6
|
+
# shared by the [Assertion::Base] and [Assertion::Guard] classes
|
7
|
+
#
|
8
|
+
module Attributes
|
9
|
+
|
10
|
+
# List of attributes, defined for the class
|
11
|
+
#
|
12
|
+
# @return [Array<Symbol>]
|
13
|
+
#
|
14
|
+
def attributes
|
15
|
+
@attributes ||= []
|
16
|
+
end
|
17
|
+
|
18
|
+
# Adds a new attribute or a list of attributes to the class
|
19
|
+
#
|
20
|
+
# @param [Symbol, Array<Symbol>] names
|
21
|
+
#
|
22
|
+
# @return [undefined]
|
23
|
+
#
|
24
|
+
# @raise [NameError]
|
25
|
+
# When the name is either used by instance attribute,
|
26
|
+
# or forbidden as a name of the method to be implemented later
|
27
|
+
# (not as an attribute)
|
28
|
+
#
|
29
|
+
def attribute(*names)
|
30
|
+
@attributes = List[:symbolize][attributes + names]
|
31
|
+
__check_attributes__
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
# Names of the methods that should be reserved to be used later
|
37
|
+
#
|
38
|
+
# @return [Array<Symbol>]
|
39
|
+
#
|
40
|
+
# @abstract
|
41
|
+
#
|
42
|
+
def __forbidden_attributes__
|
43
|
+
[]
|
44
|
+
end
|
45
|
+
|
46
|
+
def __check_attributes__
|
47
|
+
names = attributes & (instance_methods + __forbidden_attributes__)
|
48
|
+
return if names.empty?
|
49
|
+
fail NameError.new "Wrong name(s) for attribute(s): #{names.join(", ")}"
|
50
|
+
end
|
51
|
+
|
52
|
+
end # module Attributes
|
53
|
+
|
54
|
+
end # module Assertion
|
data/lib/assertion/base.rb
CHANGED
@@ -35,33 +35,14 @@ module Assertion
|
|
35
35
|
#
|
36
36
|
class Base
|
37
37
|
|
38
|
+
extend Attributes
|
39
|
+
include Messages
|
40
|
+
|
38
41
|
# Class DSL
|
39
42
|
#
|
40
43
|
class << self
|
41
44
|
|
42
|
-
#
|
43
|
-
#
|
44
|
-
# @return [Array<Symbol>]
|
45
|
-
#
|
46
|
-
def attributes
|
47
|
-
@attributes ||= []
|
48
|
-
end
|
49
|
-
|
50
|
-
# Adds a new attribute or a list of attributes to the class
|
51
|
-
#
|
52
|
-
# @param [Symbol, Array<Symbol>] names
|
53
|
-
#
|
54
|
-
# @return [undefined]
|
55
|
-
#
|
56
|
-
# @raise [Assertion::NameError]
|
57
|
-
# When the name is already used by instance attribute
|
58
|
-
#
|
59
|
-
def attribute(*names)
|
60
|
-
@attributes = List[:symbolize][attributes + names]
|
61
|
-
__check__
|
62
|
-
end
|
63
|
-
|
64
|
-
# Initializes a assertion with some attributes (data) and then calls it
|
45
|
+
# Initializes an assertion with some attributes (data) and then calls it
|
65
46
|
#
|
66
47
|
# @param [Hash] hash
|
67
48
|
#
|
@@ -96,10 +77,8 @@ module Assertion
|
|
96
77
|
|
97
78
|
private
|
98
79
|
|
99
|
-
|
100
|
-
|
101
|
-
wrong = attributes & instance_methods
|
102
|
-
fail(NameError.new wrong) if wrong.any?
|
80
|
+
def __forbidden_attributes__
|
81
|
+
[:check]
|
103
82
|
end
|
104
83
|
|
105
84
|
end # eigenclass
|
@@ -126,28 +105,6 @@ module Assertion
|
|
126
105
|
freeze
|
127
106
|
end
|
128
107
|
|
129
|
-
# Returns the translated message about the current state of the assertion
|
130
|
-
# applied to its attributes
|
131
|
-
#
|
132
|
-
# @param [Symbol] state The state to be described
|
133
|
-
#
|
134
|
-
# @return [String] The message
|
135
|
-
#
|
136
|
-
def message(state = nil)
|
137
|
-
I18n[:translate, __scope__, attributes][state ? :right : :wrong]
|
138
|
-
end
|
139
|
-
|
140
|
-
# Checks whether the assertion is right for the current attributes
|
141
|
-
#
|
142
|
-
# @return [Boolean]
|
143
|
-
#
|
144
|
-
# @raise [Check::NotImplementedError]
|
145
|
-
# When the [#check] method hasn't been implemented
|
146
|
-
#
|
147
|
-
def check
|
148
|
-
fail NotImplementedError.new(self.class, :check)
|
149
|
-
end
|
150
|
-
|
151
108
|
# Calls the assertion checkup and returns the state of the assertion having
|
152
109
|
# been applied to the current attributes
|
153
110
|
#
|
@@ -156,26 +113,11 @@ module Assertion
|
|
156
113
|
#
|
157
114
|
def call
|
158
115
|
state = check
|
159
|
-
State.new state, message
|
116
|
+
State.new state, message(false)
|
160
117
|
end
|
161
118
|
|
162
119
|
private
|
163
120
|
|
164
|
-
# The scope for translating messages using the `I18n` module
|
165
|
-
#
|
166
|
-
# @return [Array<Symbol>]
|
167
|
-
#
|
168
|
-
def __scope__
|
169
|
-
I18n[:scope][self.class.name]
|
170
|
-
end
|
171
|
-
|
172
|
-
# Defines the object attribute and assigns given value to it
|
173
|
-
#
|
174
|
-
# @param [Symbol] name The name of the attribute
|
175
|
-
# @param [Object] value The value of the attribute
|
176
|
-
#
|
177
|
-
# @return [undefined]
|
178
|
-
#
|
179
121
|
def __set_attribute__(name, value)
|
180
122
|
attributes[name] = value
|
181
123
|
singleton_class.__send__(:define_method, name) { value }
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# The base class for object guards
|
6
|
+
#
|
7
|
+
# The guard defines a desired state for the object and checks
|
8
|
+
# if that state is valid.
|
9
|
+
#
|
10
|
+
# Its `call` method either returns the guarded object, or
|
11
|
+
# (when its state is invalid) raises an exception
|
12
|
+
#
|
13
|
+
# The class DSL also defines a `.[]` shortcut to initialize
|
14
|
+
# and call the guard for given object immediately.
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# class AdultOnly < Assertion::Guard
|
18
|
+
# alias_method :user, :object
|
19
|
+
#
|
20
|
+
# def state
|
21
|
+
# IsAdult[user.attributes]
|
22
|
+
# end
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# jack = User.new name: "Jack", age: 10
|
26
|
+
# john = User.new name: "John", age: 59
|
27
|
+
#
|
28
|
+
# AdultOnly[jack]
|
29
|
+
# # => #<Assertion::InvalidError @messages=["Jack is a child (age 10)"]>
|
30
|
+
# AdultOnly[john]
|
31
|
+
# # => #<User @name="John", @age=59>
|
32
|
+
#
|
33
|
+
class Guard
|
34
|
+
|
35
|
+
extend Attributes
|
36
|
+
|
37
|
+
class << self
|
38
|
+
|
39
|
+
# Initializes and guard for the provided object and calls it immediately
|
40
|
+
#
|
41
|
+
# @param [Object] object The object whose state should be tested
|
42
|
+
#
|
43
|
+
# @return (see #call)
|
44
|
+
#
|
45
|
+
# @raise (see #call)
|
46
|
+
#
|
47
|
+
def [](object)
|
48
|
+
new(object).call
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def __forbidden_attributes__
|
54
|
+
[:state]
|
55
|
+
end
|
56
|
+
|
57
|
+
end # eigenclass
|
58
|
+
|
59
|
+
# @!attribute [r] object
|
60
|
+
#
|
61
|
+
# @return [Object] The object whose state should be tested
|
62
|
+
#
|
63
|
+
attr_reader :object
|
64
|
+
|
65
|
+
# @!scope class
|
66
|
+
# @!method new(object)
|
67
|
+
# Creates the guard instance for the provided object
|
68
|
+
#
|
69
|
+
# @param [Object] object
|
70
|
+
#
|
71
|
+
# @return [Assertion::Guard]
|
72
|
+
|
73
|
+
# @private
|
74
|
+
def initialize(object)
|
75
|
+
@object = object
|
76
|
+
self.class.attributes.each(&method(:__set_attribute__))
|
77
|
+
freeze
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validates the state of the [#object] and returns valid object back
|
81
|
+
#
|
82
|
+
# @return (see #object)
|
83
|
+
#
|
84
|
+
# @raise [Assertion::InvalidError] if the [#object] is invalid
|
85
|
+
#
|
86
|
+
def call
|
87
|
+
state.validate!
|
88
|
+
object
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def __set_attribute__(name)
|
94
|
+
singleton_class.instance_eval { alias_method name, :object }
|
95
|
+
end
|
96
|
+
|
97
|
+
end # class Guard
|
98
|
+
|
99
|
+
end # module Assertion
|