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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/Gemfile +0 -2
  4. data/Guardfile +6 -6
  5. data/README.md +8 -6
  6. data/assertion.gemspec +1 -1
  7. data/config/metrics/flay.yml +1 -1
  8. data/lib/assertion.rb +29 -80
  9. data/lib/assertion/base.rb +38 -64
  10. data/lib/assertion/base_dsl.rb +75 -0
  11. data/lib/assertion/dsl.rb +77 -0
  12. data/lib/assertion/guard.rb +1 -30
  13. data/lib/assertion/guard_dsl.rb +39 -0
  14. data/lib/assertion/{transprocs/inflector.rb → inflector.rb} +0 -0
  15. data/lib/assertion/inversion.rb +3 -3
  16. data/lib/assertion/inverter.rb +2 -2
  17. data/lib/assertion/translator.rb +95 -0
  18. data/lib/assertion/version.rb +1 -1
  19. data/spec/integration/guard_spec.rb +1 -1
  20. data/spec/shared/en.yml +4 -4
  21. data/spec/unit/assertion/base_spec.rb +124 -14
  22. data/spec/unit/assertion/guard_spec.rb +37 -13
  23. data/spec/unit/assertion/{transprocs/inflector → inflector}/to_path_spec.rb +0 -0
  24. data/spec/unit/assertion/{transprocs/inflector → inflector}/to_snake_path_spec.rb +0 -0
  25. data/spec/unit/assertion/{transprocs/inflector → inflector}/to_snake_spec.rb +0 -0
  26. data/spec/unit/assertion/{exceptions/invalid_error_spec.rb → invalid_error_spec.rb} +0 -0
  27. data/spec/unit/assertion/inversion_spec.rb +5 -5
  28. data/spec/unit/assertion/inverter_spec.rb +2 -2
  29. data/spec/unit/assertion/translator_spec.rb +57 -0
  30. metadata +23 -20
  31. data/lib/assertion/attributes.rb +0 -54
  32. data/lib/assertion/messages.rb +0 -65
  33. data/lib/assertion/transprocs/list.rb +0 -30
  34. data/spec/unit/assertion/attributes_spec.rb +0 -97
  35. data/spec/unit/assertion/messages_spec.rb +0 -41
  36. 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
@@ -32,29 +32,7 @@ module Assertion
32
32
  #
33
33
  class Guard
34
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
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
@@ -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 wrong to right) and reverting its `check`.
8
+ # (from falsey to truthy) and reverting its `check`.
9
9
  #
10
10
  # @example
11
- # Adult = Assertion.about :name, :age do
11
+ # IsAdult = Assertion.about :name, :age do
12
12
  # age >= 18
13
13
  # end
14
14
  #
15
- # assertion = Adult.new
15
+ # assertion = IsAdult.new
16
16
  # inversion = Inversion.new(assertion)
17
17
  #
18
18
  # assertion.call.valid? == inversion.call.invalid? # => true
@@ -5,13 +5,13 @@ module Assertion
5
5
  # Builds inversions for instances of some `Assertion::Base` subclass
6
6
  #
7
7
  # @example
8
- # Adult = Assertion.about :name, :age do
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(Adult)
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
@@ -4,6 +4,6 @@ module Assertion
4
4
 
5
5
  # The semantic version of the module.
6
6
  # @see http://semver.org/ Semantic versioning 2.0
7
- VERSION = "0.1.0".freeze
7
+ VERSION = "0.2.0".freeze
8
8
 
9
9
  end # module Assertion
@@ -13,7 +13,7 @@ describe Assertion do
13
13
  end
14
14
 
15
15
  AdultOnly = Assertion.guards :user do
16
- IsAdult[user]
16
+ IsAdult[user.to_h]
17
17
  end
18
18
 
19
19
  andrew = OpenStruct.new(name: "Andrew", age: 13, city: "Moscow")
@@ -3,8 +3,8 @@
3
3
  en:
4
4
  assertion:
5
5
  is_adult:
6
- right: "%{name} is an adult (age %{age})"
7
- wrong: "%{name} is a child (age %{age})"
6
+ truthy: "%{name} is an adult (age %{age})"
7
+ falsey: "%{name} is a child (age %{age})"
8
8
  is_male:
9
- right: "%{name} is a male"
10
- wrong: "%{name} is a female"
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
- before { allow(klass).to receive(:new).with(params) { assertion } }
133
+ context "with params" do
68
134
 
69
- it "checks the assertion for given attributes" do
70
- expect(subject).to eql state
71
- end
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 }