assertion 0.1.0 → 0.2.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 +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
|