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
@@ -0,0 +1,77 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# Provides methods to build assertions and guards
|
6
|
+
#
|
7
|
+
module DSL
|
8
|
+
|
9
|
+
# Builds the subclass of `Assertion::Base` with predefined `attributes`
|
10
|
+
# and implementation of the `#check` method.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# IsMan = Assertion.about :age, :gender do
|
14
|
+
# (age >= 18) && (gender == :male)
|
15
|
+
# end
|
16
|
+
#
|
17
|
+
# # This is the same as:
|
18
|
+
# class IsMan < Assertion::Base
|
19
|
+
# attribute :age, :gender
|
20
|
+
#
|
21
|
+
# def check
|
22
|
+
# (age >= 18) && (gender == :male)
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @param [Symbol, Array<Symbol>] attributes
|
27
|
+
# The list of attributes for the new assertion
|
28
|
+
# @param [Proc] block
|
29
|
+
# The content for the `check` method
|
30
|
+
#
|
31
|
+
# @return [Class] The specific assertion class
|
32
|
+
#
|
33
|
+
def about(*attributes, &block)
|
34
|
+
__build__(Base, attributes, :check, &block)
|
35
|
+
end
|
36
|
+
|
37
|
+
# Builds the subclass of `Assertion::Guard` with given attribute
|
38
|
+
# (alias for the `object`) and implementation of the `#state` method.
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# VoterOnly = Assertion.guards :user do
|
42
|
+
# IsAdult[user.attributes] & IsCitizen[user.attributes]
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# # This is the same as:
|
46
|
+
# class VoterOnly < Assertion::Guard
|
47
|
+
# alias_method :user, :object
|
48
|
+
#
|
49
|
+
# def state
|
50
|
+
# IsAdult[user.attributes] & IsCitizen[user.attributes]
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# @param [Symbol] attribute
|
55
|
+
# The alias for the `object` attribute
|
56
|
+
# @param [Proc] block
|
57
|
+
# The content for the `state` method
|
58
|
+
#
|
59
|
+
# @return [Class] The specific guard class
|
60
|
+
#
|
61
|
+
def guards(attribute = nil, &block)
|
62
|
+
__build__(Guard, attribute, :state, &block)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def __build__(type, attributes, name, &block)
|
68
|
+
klass = Class.new(type)
|
69
|
+
klass.public_send(:attribute, attributes) if attributes
|
70
|
+
klass.__send__(:define_method, name, &block) if block_given?
|
71
|
+
|
72
|
+
klass
|
73
|
+
end
|
74
|
+
|
75
|
+
end # module DSL
|
76
|
+
|
77
|
+
end # module Assertion
|
data/lib/assertion/guard.rb
CHANGED
@@ -32,29 +32,7 @@ module Assertion
|
|
32
32
|
#
|
33
33
|
class Guard
|
34
34
|
|
35
|
-
extend
|
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
|
35
|
+
extend GuardDSL
|
58
36
|
|
59
37
|
# @!attribute [r] object
|
60
38
|
#
|
@@ -73,7 +51,6 @@ module Assertion
|
|
73
51
|
# @private
|
74
52
|
def initialize(object)
|
75
53
|
@object = object
|
76
|
-
self.class.attributes.each(&method(:__set_attribute__))
|
77
54
|
freeze
|
78
55
|
end
|
79
56
|
|
@@ -88,12 +65,6 @@ module Assertion
|
|
88
65
|
object
|
89
66
|
end
|
90
67
|
|
91
|
-
private
|
92
|
-
|
93
|
-
def __set_attribute__(name)
|
94
|
-
singleton_class.instance_eval { alias_method name, :object }
|
95
|
-
end
|
96
|
-
|
97
68
|
end # class Guard
|
98
69
|
|
99
70
|
end # module Assertion
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Assertion
|
2
|
+
|
3
|
+
# Provides methods to describe and apply guards
|
4
|
+
#
|
5
|
+
module GuardDSL
|
6
|
+
|
7
|
+
# Initializes and guard for the provided object and calls it immediately
|
8
|
+
#
|
9
|
+
# @param [Object] object The object whose state should be tested
|
10
|
+
#
|
11
|
+
# @return (see Assertion::Guard#call)
|
12
|
+
#
|
13
|
+
# @raise (see Assertion::Guard#call)
|
14
|
+
#
|
15
|
+
def [](object)
|
16
|
+
new(object).call
|
17
|
+
end
|
18
|
+
|
19
|
+
# Adds alias to the [#object] method
|
20
|
+
#
|
21
|
+
# @param [#to_sym] name
|
22
|
+
#
|
23
|
+
# @return [undefined]
|
24
|
+
#
|
25
|
+
def attribute(name)
|
26
|
+
__check_attribute__(name)
|
27
|
+
alias_method name, :object
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def __check_attribute__(key)
|
33
|
+
return unless (instance_methods << :state).include? key.to_sym
|
34
|
+
fail NameError.new "#{self}##{key} is already defined"
|
35
|
+
end
|
36
|
+
|
37
|
+
end # module GuardDSL
|
38
|
+
|
39
|
+
end # module Assertion
|
File without changes
|
data/lib/assertion/inversion.rb
CHANGED
@@ -5,14 +5,14 @@ module Assertion
|
|
5
5
|
# Describes the inversion of the assertion object
|
6
6
|
#
|
7
7
|
# The inversion decorates the source assertion switching its message
|
8
|
-
# (from
|
8
|
+
# (from falsey to truthy) and reverting its `check`.
|
9
9
|
#
|
10
10
|
# @example
|
11
|
-
#
|
11
|
+
# IsAdult = Assertion.about :name, :age do
|
12
12
|
# age >= 18
|
13
13
|
# end
|
14
14
|
#
|
15
|
-
# assertion =
|
15
|
+
# assertion = IsAdult.new
|
16
16
|
# inversion = Inversion.new(assertion)
|
17
17
|
#
|
18
18
|
# assertion.call.valid? == inversion.call.invalid? # => true
|
data/lib/assertion/inverter.rb
CHANGED
@@ -5,13 +5,13 @@ module Assertion
|
|
5
5
|
# Builds inversions for instances of some `Assertion::Base` subclass
|
6
6
|
#
|
7
7
|
# @example
|
8
|
-
#
|
8
|
+
# IsAdult = Assertion.about :name, :age do
|
9
9
|
# age >= 18
|
10
10
|
# end
|
11
11
|
#
|
12
12
|
# joe = OpenStruct.new(name: "Joe", age: 40)
|
13
13
|
#
|
14
|
-
# child = Inverter.new(
|
14
|
+
# child = Inverter.new(IsAdult)
|
15
15
|
# child[name: "Joe"].validate!
|
16
16
|
# # => #<Assertion::InvalidError @messages=["Joe is an adult (age 40)"]>
|
17
17
|
#
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "i18n"
|
4
|
+
|
5
|
+
module Assertion
|
6
|
+
|
7
|
+
# Module defines how to translate messages describing the desired state
|
8
|
+
# of the current assertion
|
9
|
+
#
|
10
|
+
# You need to declare a hash of attributes to be added to the translation.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# class MyClass
|
14
|
+
# include Assertion::Translator
|
15
|
+
# def attributes
|
16
|
+
# {}
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# item = MyClass.new
|
21
|
+
# item.message(true)
|
22
|
+
# # => "translation missing: en.assertion.my_class.truthy"
|
23
|
+
# item.message(false)
|
24
|
+
# # => "translation missing: en.assertion.my_class.falsey"
|
25
|
+
#
|
26
|
+
# @author Andrew Kozin <Andrew.Kozin@gmail.com>
|
27
|
+
#
|
28
|
+
class Translator
|
29
|
+
|
30
|
+
# The gem-specific root scope for translations
|
31
|
+
#
|
32
|
+
# @return [Symbol]
|
33
|
+
#
|
34
|
+
ROOT = :assertion
|
35
|
+
|
36
|
+
# The states to be translated with their dictionary names
|
37
|
+
#
|
38
|
+
# @return [Hash<Object => Symbol>]
|
39
|
+
#
|
40
|
+
DICTIONARY = { true => :truthy, false => :falsey }
|
41
|
+
|
42
|
+
# Provides a scope for the class
|
43
|
+
#
|
44
|
+
# @param [Class] klass
|
45
|
+
#
|
46
|
+
# @return [Array<Symbol>]
|
47
|
+
#
|
48
|
+
def self.scope(klass)
|
49
|
+
[ROOT, Inflector[:to_snake_path][klass.name].to_sym]
|
50
|
+
end
|
51
|
+
|
52
|
+
# @!attribute [r] scope
|
53
|
+
#
|
54
|
+
# @return [Array<Symbol>] the scope for translations
|
55
|
+
#
|
56
|
+
attr_reader :scope
|
57
|
+
|
58
|
+
# @!attribute [r] scope
|
59
|
+
#
|
60
|
+
# @return [Class] the assertion whose state should be translated
|
61
|
+
#
|
62
|
+
attr_reader :assertion
|
63
|
+
|
64
|
+
# @!scope class
|
65
|
+
# @!method new(assertion)
|
66
|
+
# Creates a state translator for the given assertion class
|
67
|
+
#
|
68
|
+
# @param [Class] assertion
|
69
|
+
#
|
70
|
+
# @return [Assertion::Translator]
|
71
|
+
|
72
|
+
# @private
|
73
|
+
def initialize(assertion)
|
74
|
+
@assertion = assertion
|
75
|
+
@scope = self.class.scope(assertion)
|
76
|
+
freeze
|
77
|
+
end
|
78
|
+
|
79
|
+
# Returns the message describing the desired state of given assertion
|
80
|
+
#
|
81
|
+
# The translation is provided for the gem-specific scope for the
|
82
|
+
# current class
|
83
|
+
#
|
84
|
+
# @param [Boolean] state The state of the assertion
|
85
|
+
# @param [Hash] args The hash of arguments to be avaliable in a translation
|
86
|
+
#
|
87
|
+
# @return [String] The translation
|
88
|
+
#
|
89
|
+
def call(state, args = {})
|
90
|
+
I18n.translate DICTIONARY[state], args.merge(scope: scope)
|
91
|
+
end
|
92
|
+
|
93
|
+
end # class Translator
|
94
|
+
|
95
|
+
end # module Assertion
|
data/lib/assertion/version.rb
CHANGED
data/spec/shared/en.yml
CHANGED
@@ -3,8 +3,8 @@
|
|
3
3
|
en:
|
4
4
|
assertion:
|
5
5
|
is_adult:
|
6
|
-
|
7
|
-
|
6
|
+
truthy: "%{name} is an adult (age %{age})"
|
7
|
+
falsey: "%{name} is a child (age %{age})"
|
8
8
|
is_male:
|
9
|
-
|
10
|
-
|
9
|
+
truthy: "%{name} is a male"
|
10
|
+
falsey: "%{name} is a female"
|
@@ -5,14 +5,6 @@ describe Assertion::Base do
|
|
5
5
|
let(:klass) { Class.new(described_class) }
|
6
6
|
before { allow(klass).to receive(:name) { "Test" } }
|
7
7
|
|
8
|
-
it "can declare attributes" do
|
9
|
-
expect(klass).to be_kind_of Assertion::Attributes
|
10
|
-
end
|
11
|
-
|
12
|
-
it "can translate states" do
|
13
|
-
expect(klass).to include Assertion::Messages
|
14
|
-
end
|
15
|
-
|
16
8
|
describe ".new" do
|
17
9
|
|
18
10
|
let(:klass) { Class.new(described_class) { attribute :foo, :bar } }
|
@@ -45,6 +37,82 @@ describe Assertion::Base do
|
|
45
37
|
|
46
38
|
end # describe .new
|
47
39
|
|
40
|
+
describe ".attributes" do
|
41
|
+
|
42
|
+
subject { klass.attributes }
|
43
|
+
it { is_expected.to eql [] }
|
44
|
+
|
45
|
+
end # describe .attributes
|
46
|
+
|
47
|
+
describe ".attribute" do
|
48
|
+
|
49
|
+
shared_examples "defining attributes" do
|
50
|
+
|
51
|
+
it "registers attributes" do
|
52
|
+
expect { subject }.to change { klass.attributes }.to [:foo, :bar]
|
53
|
+
end
|
54
|
+
|
55
|
+
it "declares attributes" do
|
56
|
+
subject
|
57
|
+
assertion = klass.new(foo: :FOO, bar: :BAR, baz: :BAZ)
|
58
|
+
expect(assertion.attributes).to eql(foo: :FOO, bar: :BAR)
|
59
|
+
expect(assertion.foo).to eql :FOO
|
60
|
+
expect(assertion.bar).to eql :BAR
|
61
|
+
end
|
62
|
+
|
63
|
+
end # shared examples
|
64
|
+
|
65
|
+
shared_examples "raising NameError" do |with: nil|
|
66
|
+
|
67
|
+
it "fails" do
|
68
|
+
expect { subject }.to raise_error do |exception|
|
69
|
+
expect(exception).to be_kind_of NameError
|
70
|
+
expect(exception.message).to eql "#{klass}##{with} is already defined"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
end # shared examples
|
75
|
+
|
76
|
+
context "with a single name" do
|
77
|
+
|
78
|
+
subject do
|
79
|
+
klass.attribute :foo
|
80
|
+
klass.attribute "bar"
|
81
|
+
end
|
82
|
+
it_behaves_like "defining attributes"
|
83
|
+
|
84
|
+
end # context
|
85
|
+
|
86
|
+
context "with a list of names" do
|
87
|
+
|
88
|
+
subject { klass.attribute :foo, :bar }
|
89
|
+
it_behaves_like "defining attributes"
|
90
|
+
|
91
|
+
end # context
|
92
|
+
|
93
|
+
context "with an array of names" do
|
94
|
+
|
95
|
+
subject { klass.attribute %w(foo bar) }
|
96
|
+
it_behaves_like "defining attributes"
|
97
|
+
|
98
|
+
end # context
|
99
|
+
|
100
|
+
context ":check" do
|
101
|
+
|
102
|
+
subject { klass.attribute :check }
|
103
|
+
it_behaves_like "raising NameError", with: :check
|
104
|
+
|
105
|
+
end # context
|
106
|
+
|
107
|
+
context ":call" do
|
108
|
+
|
109
|
+
subject { klass.attribute :call }
|
110
|
+
it_behaves_like "raising NameError", with: :call
|
111
|
+
|
112
|
+
end # context
|
113
|
+
|
114
|
+
end # describe .attribute
|
115
|
+
|
48
116
|
describe ".not" do
|
49
117
|
|
50
118
|
subject { klass.not }
|
@@ -58,20 +126,46 @@ describe Assertion::Base do
|
|
58
126
|
|
59
127
|
describe ".[]" do
|
60
128
|
|
61
|
-
subject { klass[params] }
|
62
|
-
|
63
129
|
let(:params) { { foo: :FOO } }
|
64
130
|
let(:state) { double }
|
65
131
|
let(:assertion) { double call: state }
|
66
132
|
|
67
|
-
|
133
|
+
context "with params" do
|
68
134
|
|
69
|
-
|
70
|
-
|
71
|
-
|
135
|
+
subject { klass[params] }
|
136
|
+
|
137
|
+
it "checks the assertion for given attributes" do
|
138
|
+
allow(klass).to receive(:new).with(params) { assertion }
|
139
|
+
expect(subject).to eql state
|
140
|
+
end
|
141
|
+
|
142
|
+
end # context
|
143
|
+
|
144
|
+
context "without params" do
|
145
|
+
|
146
|
+
subject { klass[] }
|
147
|
+
|
148
|
+
it "checks the assertion" do
|
149
|
+
allow(klass).to receive(:new) { assertion }
|
150
|
+
expect(subject).to eql state
|
151
|
+
end
|
152
|
+
|
153
|
+
end # context
|
72
154
|
|
73
155
|
end # describe .[]
|
74
156
|
|
157
|
+
describe ".translator" do
|
158
|
+
|
159
|
+
subject { klass.translator }
|
160
|
+
|
161
|
+
it { is_expected.to be_kind_of Assertion::Translator }
|
162
|
+
|
163
|
+
it "refers to the current class" do
|
164
|
+
expect(subject.assertion).to eql klass
|
165
|
+
end
|
166
|
+
|
167
|
+
end # describe .translator
|
168
|
+
|
75
169
|
describe "#attributes" do
|
76
170
|
|
77
171
|
let(:attrs) { { foo: :FOO, bar: :BAR } }
|
@@ -83,6 +177,22 @@ describe Assertion::Base do
|
|
83
177
|
|
84
178
|
end # describe #attributes
|
85
179
|
|
180
|
+
describe "#message" do
|
181
|
+
|
182
|
+
let(:state) { double }
|
183
|
+
let(:attrs) { { foo: :FOO, bar: :BAR } }
|
184
|
+
let(:klass) { Class.new(described_class) { attribute :foo, :bar } }
|
185
|
+
let(:assertion) { klass.new attrs }
|
186
|
+
let(:translator) { double call: nil }
|
187
|
+
|
188
|
+
it "calls a translator with state and attributes" do
|
189
|
+
allow(klass).to receive(:translator) { translator }
|
190
|
+
expect(translator).to receive(:call).with(state, attrs)
|
191
|
+
assertion.message(state)
|
192
|
+
end
|
193
|
+
|
194
|
+
end # describe #message
|
195
|
+
|
86
196
|
describe "#call" do
|
87
197
|
|
88
198
|
subject { assertion.call }
|