assertion 0.1.0 → 0.2.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 +17 -0
- data/Gemfile +0 -2
- data/Guardfile +6 -6
- data/README.md +8 -6
- data/assertion.gemspec +1 -1
- data/config/metrics/flay.yml +1 -1
- data/lib/assertion.rb +29 -80
- data/lib/assertion/base.rb +38 -64
- data/lib/assertion/base_dsl.rb +75 -0
- data/lib/assertion/dsl.rb +77 -0
- data/lib/assertion/guard.rb +1 -30
- data/lib/assertion/guard_dsl.rb +39 -0
- data/lib/assertion/{transprocs/inflector.rb → inflector.rb} +0 -0
- data/lib/assertion/inversion.rb +3 -3
- data/lib/assertion/inverter.rb +2 -2
- data/lib/assertion/translator.rb +95 -0
- data/lib/assertion/version.rb +1 -1
- data/spec/integration/guard_spec.rb +1 -1
- data/spec/shared/en.yml +4 -4
- data/spec/unit/assertion/base_spec.rb +124 -14
- data/spec/unit/assertion/guard_spec.rb +37 -13
- data/spec/unit/assertion/{transprocs/inflector → inflector}/to_path_spec.rb +0 -0
- data/spec/unit/assertion/{transprocs/inflector → inflector}/to_snake_path_spec.rb +0 -0
- data/spec/unit/assertion/{transprocs/inflector → inflector}/to_snake_spec.rb +0 -0
- data/spec/unit/assertion/{exceptions/invalid_error_spec.rb → invalid_error_spec.rb} +0 -0
- data/spec/unit/assertion/inversion_spec.rb +5 -5
- data/spec/unit/assertion/inverter_spec.rb +2 -2
- data/spec/unit/assertion/translator_spec.rb +57 -0
- metadata +23 -20
- data/lib/assertion/attributes.rb +0 -54
- data/lib/assertion/messages.rb +0 -65
- data/lib/assertion/transprocs/list.rb +0 -30
- data/spec/unit/assertion/attributes_spec.rb +0 -97
- data/spec/unit/assertion/messages_spec.rb +0 -41
- data/spec/unit/assertion/transprocs/list/symbolize_spec.rb +0 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5cd46bf51eb8357641668f5f3762bf0fb5a0dbd7
|
4
|
+
data.tar.gz: 048a9c8f663aed972a4ee8e0ded514a7310a6742
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ec1de6dade9383cd776f497fe0bfbaf452a1a60ae3ed5b341fe8741fbf891a4f43bf3ac08cbfa7fa1541aae1ce71937ba350cbd3932ae0f1975a01f7ac859e79
|
7
|
+
data.tar.gz: 7fe6666427458a4291988444e4a333219297cde70cd1aa592d2d3624625d8bc0c39bb647eb25bc1c49c8a1963e609f927443ef6ba51d4cd35ee45a0e0c4d5a81
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,20 @@
|
|
1
|
+
## v0.2.0 2015-06-22
|
2
|
+
|
3
|
+
### Changed (backward-incompatible!)
|
4
|
+
|
5
|
+
* Renamed translation keys from `:right`/`:wrong` to `:truthy`/`:falsey` for consistency (nepalez)
|
6
|
+
|
7
|
+
### Internal
|
8
|
+
|
9
|
+
* Attributes are added to `Base` and `Guard` at the moment of definition,
|
10
|
+
instead of the initializations (addresses efficiency issue #1) (nepalez)
|
11
|
+
* `Translator` replaced `Messages` and `List`(transproc) to invert dependency
|
12
|
+
of `Base` from translations. (nepalez)
|
13
|
+
* Class-level DSL features are extracted from `Assertion`, `Base` and `Guard`
|
14
|
+
to the separate modules `DSL`, `BaseDSL`, `GuardDSL` correspondingly (nepalez)
|
15
|
+
|
16
|
+
[Compare v0.1.0...v0.2.0](https://github.com/nepalez/assertion/compare/v0.1.0...v0.2.0)
|
17
|
+
|
1
18
|
## v0.1.0 2015-06-20
|
2
19
|
|
3
20
|
### Added
|
data/Gemfile
CHANGED
data/Guardfile
CHANGED
@@ -4,15 +4,15 @@ guard :rspec, cmd: "bundle exec rspec" do
|
|
4
4
|
|
5
5
|
watch(%r{^spec/.+_spec\.rb$})
|
6
6
|
|
7
|
-
watch(%r{^lib/
|
8
|
-
"spec/unit
|
7
|
+
watch(%r{^lib/(.+)\.rb}) do |m|
|
8
|
+
"spec/unit/#{m[1]}_spec.rb"
|
9
9
|
end
|
10
10
|
|
11
|
-
watch(
|
12
|
-
|
13
|
-
|
11
|
+
watch("lib/assertion/dsl.rb") { "spec/unit/assertion_spec.rb" }
|
12
|
+
watch("lib/assertion/base_dsl.rb") { "spec/unit/assertion/base_spec.rb" }
|
13
|
+
watch("lib/assertion/guard_dsl.rb") { "spec/unit/assertion/guard_spec.rb" }
|
14
|
+
watch("lib/assertion/inflector.rb") { "spec/unit/assertion/inflector" }
|
14
15
|
|
15
|
-
watch("lib/assertion.rb") { "spec" }
|
16
16
|
watch("spec/spec_helper.rb") { "spec" }
|
17
17
|
|
18
18
|
end # guard :rspec
|
data/README.md
CHANGED
@@ -24,6 +24,8 @@ 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
|
+
[About](https://github.com/mbj/mutant/issues/356) 100% [mutant]-covered.
|
28
|
+
|
27
29
|
### Basic Usage
|
28
30
|
|
29
31
|
Define an assertion by inheriting it from the `Assertion::Base` class with attributes to which it should be applied.
|
@@ -49,7 +51,7 @@ IsAdult = Assertion.about :age, :name do
|
|
49
51
|
end
|
50
52
|
```
|
51
53
|
|
52
|
-
Define translations to describe both the *
|
54
|
+
Define translations to describe both the *truthy* and *falsey* states of the assertion.
|
53
55
|
|
54
56
|
All the attributes are available in translations (that's why we declared the `name` as an attribute):
|
55
57
|
|
@@ -59,8 +61,8 @@ All the attributes are available in translations (that's why we declared the `na
|
|
59
61
|
en:
|
60
62
|
assertion:
|
61
63
|
is_adult:
|
62
|
-
|
63
|
-
|
64
|
+
truthy: "%{name} is already an adult (age %{age})"
|
65
|
+
falsey: "%{name} is a child yet (age %{age})"
|
64
66
|
```
|
65
67
|
|
66
68
|
Check a state of an assertion for some argument(s), using class method `[]`:
|
@@ -110,8 +112,8 @@ end
|
|
110
112
|
en:
|
111
113
|
assertion:
|
112
114
|
is_male:
|
113
|
-
|
114
|
-
|
115
|
+
truthy: "%{name} is a male"
|
116
|
+
falsey: "%{name} is a female"
|
115
117
|
```
|
116
118
|
|
117
119
|
Use method `&` (or its aliases `+` or `>>`) to compose assertion states:
|
@@ -166,7 +168,7 @@ Naming Convention
|
|
166
168
|
|
167
169
|
This is not necessary, but for verbosity you could follow the rules:
|
168
170
|
|
169
|
-
* use the
|
171
|
+
* use the prefixex `Is` (`Are`) for assertions (like `IsAdult`, `AreConsistent` etc.)
|
170
172
|
* use the suffix `Only` for guards (like `AdultOnly`)
|
171
173
|
|
172
174
|
Edge Cases
|
data/assertion.gemspec
CHANGED
@@ -18,7 +18,7 @@ Gem::Specification.new do |gem|
|
|
18
18
|
|
19
19
|
gem.required_ruby_version = "~> 1.9"
|
20
20
|
|
21
|
-
gem.add_runtime_dependency "transproc", "~> 0.2"
|
21
|
+
gem.add_runtime_dependency "transproc", "~> 0.2", "> 0.2.3"
|
22
22
|
gem.add_runtime_dependency "i18n", "~> 0.7"
|
23
23
|
|
24
24
|
gem.add_development_dependency "hexx-rspec", "~> 0.4"
|
data/config/metrics/flay.yml
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
---
|
2
|
-
minimum_score:
|
2
|
+
minimum_score: 7
|
data/lib/assertion.rb
CHANGED
@@ -2,109 +2,58 @@
|
|
2
2
|
|
3
3
|
require "transproc"
|
4
4
|
|
5
|
-
require_relative "assertion/
|
6
|
-
require_relative "assertion/transprocs/list"
|
7
|
-
|
5
|
+
require_relative "assertion/inflector"
|
8
6
|
require_relative "assertion/invalid_error"
|
9
|
-
require_relative "assertion/
|
10
|
-
require_relative "assertion/messages"
|
11
|
-
|
7
|
+
require_relative "assertion/translator"
|
12
8
|
require_relative "assertion/state"
|
9
|
+
require_relative "assertion/base_dsl"
|
13
10
|
require_relative "assertion/base"
|
14
11
|
require_relative "assertion/inversion"
|
15
12
|
require_relative "assertion/inverter"
|
13
|
+
require_relative "assertion/guard_dsl"
|
16
14
|
require_relative "assertion/guard"
|
15
|
+
require_relative "assertion/dsl"
|
17
16
|
|
18
|
-
# The module
|
19
|
-
#
|
17
|
+
# The module declares:
|
18
|
+
#
|
19
|
+
# * assertions about objects
|
20
|
+
# * guards (validations) for objects
|
20
21
|
#
|
21
|
-
# @example
|
22
|
+
# @example Assertion
|
22
23
|
# # config/locales/en.yml
|
23
24
|
# # ---
|
24
25
|
# # en:
|
25
26
|
# # assertion:
|
26
|
-
# #
|
27
|
-
# #
|
28
|
-
# #
|
27
|
+
# # is_adult:
|
28
|
+
# # truthy: "%{name} is an adult (age %{age})"
|
29
|
+
# # falsey: "%{name} is a child (age %{age})"
|
29
30
|
#
|
30
|
-
#
|
31
|
+
# IsAdult = Assertion.about :name, :age do
|
31
32
|
# age >= 18
|
32
33
|
# end
|
33
34
|
#
|
34
|
-
# joe =
|
35
|
-
#
|
35
|
+
# joe = OpenStruct.new(name: 'Joe', age: 13)
|
36
|
+
# IsAdult[joe.to_h].validate!
|
36
37
|
# # => #<Assertion::InvalidError @messages=["Joe is a child (age 13)"]>
|
37
38
|
#
|
38
|
-
# jane =
|
39
|
-
#
|
39
|
+
# jane = OpenStruct.new(name: 'Jane', age: 22)
|
40
|
+
# IsAdult.not[jane.to_h].validate!
|
40
41
|
# # => #<Assertion::InvalidError @messages=["Jane is an adult (age 22)"]
|
41
42
|
#
|
43
|
+
# @example Guard
|
44
|
+
# AdultOnly = Assertion.guards :user do
|
45
|
+
# IsAdult[user.to_h]
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# AdultOnly[joe]
|
49
|
+
# # => #<Assertion::InvalidError @messages=["Joe is a child (age 13)"]>
|
50
|
+
# AdultOnly[jane]
|
51
|
+
# # => #<OpenStruct @name="Jane", @age=22>
|
52
|
+
#
|
42
53
|
# @api public
|
43
54
|
#
|
44
55
|
module Assertion
|
45
56
|
|
46
|
-
|
47
|
-
# and implementation of the `#check` method.
|
48
|
-
#
|
49
|
-
# @example
|
50
|
-
# IsMan = Assertion.about :age, :gender do
|
51
|
-
# (age >= 18) && (gender == :male)
|
52
|
-
# end
|
53
|
-
#
|
54
|
-
# # This is the same as:
|
55
|
-
# class IsMan < Assertion::Base
|
56
|
-
# attribute :age, :gender
|
57
|
-
#
|
58
|
-
# def check
|
59
|
-
# (age >= 18) && (gender == :male)
|
60
|
-
# end
|
61
|
-
# end
|
62
|
-
#
|
63
|
-
# @param [Symbol, Array<Symbol>] attributes
|
64
|
-
# The list of attributes for the new assertion
|
65
|
-
# @param [Proc] block
|
66
|
-
# The content for the `check` method
|
67
|
-
#
|
68
|
-
# @return [Class] The specific assertion class
|
69
|
-
#
|
70
|
-
def self.about(*attributes, &block)
|
71
|
-
klass = Class.new(Base)
|
72
|
-
klass.public_send(:attribute, attributes)
|
73
|
-
klass.__send__(:define_method, :check, &block) if block_given?
|
74
|
-
|
75
|
-
klass
|
76
|
-
end
|
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
|
57
|
+
extend DSL
|
109
58
|
|
110
59
|
end # module Assertion
|
data/lib/assertion/base.rb
CHANGED
@@ -19,7 +19,7 @@ module Assertion
|
|
19
19
|
# * [.not] can be used to provide the assertion opposite to the initial one.
|
20
20
|
#
|
21
21
|
# @example
|
22
|
-
# class
|
22
|
+
# class IsAdult < Assertion::Base
|
23
23
|
# attribute :name, :age
|
24
24
|
#
|
25
25
|
# def check
|
@@ -27,70 +27,32 @@ module Assertion
|
|
27
27
|
# end
|
28
28
|
# end
|
29
29
|
#
|
30
|
-
# child =
|
30
|
+
# child = IsAdult.not
|
31
31
|
#
|
32
32
|
# jane = { name: "Jane", age: 12 }
|
33
|
-
#
|
33
|
+
# IsAdult[jane].valid? # => false
|
34
34
|
# child[jane].valid? # => true
|
35
35
|
#
|
36
36
|
class Base
|
37
37
|
|
38
|
-
extend
|
39
|
-
include Messages
|
38
|
+
extend BaseDSL
|
40
39
|
|
41
|
-
#
|
40
|
+
# The translator of states for the current class
|
42
41
|
#
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
#
|
49
|
-
# @return [Assertion::State]
|
50
|
-
# The object that describes the state of the assertion
|
51
|
-
# applied to given attributes
|
52
|
-
#
|
53
|
-
def [](hash = {})
|
54
|
-
new(hash).call
|
55
|
-
end
|
56
|
-
|
57
|
-
# Initializes the intermediate inverter with `new` and `[]` methods
|
58
|
-
#
|
59
|
-
# The inverter can be used to initialize the assertion, that describes
|
60
|
-
# just the opposite statement to the current one
|
61
|
-
#
|
62
|
-
# @example
|
63
|
-
# Adult = Assertion.about :name, :age do
|
64
|
-
# age >= 18
|
65
|
-
# end
|
66
|
-
#
|
67
|
-
# joe = { name: 'Joe', age: 19 }
|
68
|
-
#
|
69
|
-
# Adult[joe].valid? # => true
|
70
|
-
# Adult.not[joe].valid? # => false
|
71
|
-
#
|
72
|
-
# @return [Assertion::Inverter]
|
73
|
-
#
|
74
|
-
def not
|
75
|
-
Inverter.new(self)
|
76
|
-
end
|
77
|
-
|
78
|
-
private
|
79
|
-
|
80
|
-
def __forbidden_attributes__
|
81
|
-
[:check]
|
82
|
-
end
|
83
|
-
|
84
|
-
end # eigenclass
|
42
|
+
# @return [Assertion::Translator]
|
43
|
+
#
|
44
|
+
def self.translator
|
45
|
+
@translator ||= Translator.new(self)
|
46
|
+
end
|
85
47
|
|
86
48
|
# @!attribute [r] attributes
|
87
49
|
#
|
88
50
|
# @example
|
89
|
-
#
|
51
|
+
# IsAdult = Assertion.about :name, :age do
|
90
52
|
# age >= 18
|
91
53
|
# end
|
92
54
|
#
|
93
|
-
# adult =
|
55
|
+
# adult = IsAdult[name: "Joe", age: 15, gender: :male]
|
94
56
|
# adult.attributes # => { name: "Joe", age: 15 }
|
95
57
|
#
|
96
58
|
# @return [Hash]
|
@@ -98,29 +60,41 @@ module Assertion
|
|
98
60
|
#
|
99
61
|
attr_reader :attributes
|
100
62
|
|
63
|
+
# @!scope class
|
64
|
+
# @!method new(args = {})
|
65
|
+
# Initializes an assertion for the current object
|
66
|
+
#
|
67
|
+
# @param [Hash] args The arguments to check
|
68
|
+
#
|
69
|
+
# @return [Assertion::Base]
|
70
|
+
|
101
71
|
# @private
|
102
72
|
def initialize(args = {})
|
103
|
-
|
104
|
-
|
73
|
+
keys = self.class.attributes
|
74
|
+
@attributes = Hash[keys.zip(args.values_at(*keys))]
|
105
75
|
freeze
|
106
76
|
end
|
107
77
|
|
108
|
-
#
|
109
|
-
# been applied to the current attributes
|
78
|
+
# Returns the message describing the current state
|
110
79
|
#
|
111
|
-
# @
|
112
|
-
# The state of the assertion being applied to its attributes
|
80
|
+
# @param [Boolean] state The state to describe
|
113
81
|
#
|
114
|
-
|
115
|
-
|
116
|
-
|
82
|
+
# @return [String]
|
83
|
+
#
|
84
|
+
def message(state)
|
85
|
+
self.class.translator.call(state, attributes)
|
117
86
|
end
|
118
87
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
88
|
+
# Calls the assertion checkup and returns the resulting state
|
89
|
+
#
|
90
|
+
# The state is a unified composable object, unaware of the
|
91
|
+
# structure and attributes of the specific assertion.
|
92
|
+
#
|
93
|
+
# @return [Assertion::State]
|
94
|
+
# The state of the assertion being applied to its attributes
|
95
|
+
#
|
96
|
+
def call
|
97
|
+
State.new check, message(false)
|
124
98
|
end
|
125
99
|
|
126
100
|
end # class Base
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# Provides methods to describe and apply assertions
|
6
|
+
#
|
7
|
+
module BaseDSL
|
8
|
+
|
9
|
+
# Initializes an assertion with some attributes (data) and then calls it
|
10
|
+
#
|
11
|
+
# @param (see Assertion::Base.new)
|
12
|
+
#
|
13
|
+
# @return (see Assertion::Base#call)
|
14
|
+
#
|
15
|
+
def [](args = nil)
|
16
|
+
new(args).call
|
17
|
+
end
|
18
|
+
|
19
|
+
# Initializes the intermediate inverter with `new` and `[]` methods
|
20
|
+
#
|
21
|
+
# The inverter can be used to initialize the assertion, that describes
|
22
|
+
# just the opposite statement to the current one
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# IsAdult = Assertion.about :name, :age do
|
26
|
+
# age >= 18
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# joe = { name: 'Joe', age: 19 }
|
30
|
+
#
|
31
|
+
# IsAdult[joe].valid? # => true
|
32
|
+
# IsAdult.not[joe].valid? # => false
|
33
|
+
#
|
34
|
+
# @return [Assertion::Inverter]
|
35
|
+
#
|
36
|
+
def not
|
37
|
+
Inverter.new(self)
|
38
|
+
end
|
39
|
+
|
40
|
+
# List of attributes defined for the assertion
|
41
|
+
#
|
42
|
+
# @return [Array<Symbol>]
|
43
|
+
#
|
44
|
+
def attributes
|
45
|
+
@attributes ||= []
|
46
|
+
end
|
47
|
+
|
48
|
+
# Declares new attribute(s) by name(s)
|
49
|
+
#
|
50
|
+
# @param [#to_sym, Array<#to_sym>] names
|
51
|
+
#
|
52
|
+
# @return [undefined]
|
53
|
+
#
|
54
|
+
# @raise [NameError]
|
55
|
+
# When an instance method with one of given names is already exist.
|
56
|
+
#
|
57
|
+
def attribute(*names)
|
58
|
+
names.flatten.map(&:to_sym).each(&method(:__add_attribute__))
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def __add_attribute__(name)
|
64
|
+
__check_attribute__(name)
|
65
|
+
attributes << define_method(name) { attributes[name] }
|
66
|
+
end
|
67
|
+
|
68
|
+
def __check_attribute__(name)
|
69
|
+
return unless (instance_methods << :check).include? name
|
70
|
+
fail NameError.new "#{self}##{name} is already defined"
|
71
|
+
end
|
72
|
+
|
73
|
+
end # module BaseDSL
|
74
|
+
|
75
|
+
end # module Assertion
|