assertion 0.0.1 → 0.1.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 +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
|