assertion 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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 }