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
@@ -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 }
|