assertion 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +9 -0
- data/.metrics +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +19 -0
- data/.yardopts +3 -0
- data/Gemfile +9 -0
- data/Guardfile +18 -0
- data/LICENSE +21 -0
- data/README.md +222 -0
- data/Rakefile +29 -0
- data/assertion.gemspec +27 -0
- data/config/metrics/STYLEGUIDE +230 -0
- data/config/metrics/cane.yml +5 -0
- data/config/metrics/churn.yml +6 -0
- data/config/metrics/flay.yml +2 -0
- data/config/metrics/metric_fu.yml +15 -0
- data/config/metrics/reek.yml +1 -0
- data/config/metrics/roodi.yml +24 -0
- data/config/metrics/rubocop.yml +72 -0
- data/config/metrics/saikuro.yml +3 -0
- data/config/metrics/simplecov.yml +6 -0
- data/config/metrics/yardstick.yml +37 -0
- data/lib/assertion.rb +79 -0
- data/lib/assertion/base.rb +186 -0
- data/lib/assertion/exceptions/invalid_error.rb +36 -0
- data/lib/assertion/exceptions/name_error.rb +29 -0
- data/lib/assertion/exceptions/not_implemented_error.rb +29 -0
- data/lib/assertion/inversion.rb +64 -0
- data/lib/assertion/inverter.rb +62 -0
- data/lib/assertion/state.rb +79 -0
- data/lib/assertion/transprocs/i18n.rb +55 -0
- data/lib/assertion/transprocs/inflector.rb +39 -0
- data/lib/assertion/transprocs/list.rb +30 -0
- data/lib/assertion/version.rb +9 -0
- data/spec/integration/assertion_spec.rb +50 -0
- data/spec/integration/en.yml +10 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/unit/assertion/base_spec.rb +221 -0
- data/spec/unit/assertion/exceptions/invalid_error_spec.rb +40 -0
- data/spec/unit/assertion/exceptions/name_error_spec.rb +26 -0
- data/spec/unit/assertion/exceptions/not_implemented_error_spec.rb +26 -0
- data/spec/unit/assertion/inversion_spec.rb +89 -0
- data/spec/unit/assertion/inverter_spec.rb +80 -0
- data/spec/unit/assertion/state_spec.rb +224 -0
- data/spec/unit/assertion/transprocs/i18n/to_scope_spec.rb +19 -0
- data/spec/unit/assertion/transprocs/i18n/translate_spec.rb +28 -0
- data/spec/unit/assertion/transprocs/inflector/to_path_spec.rb +19 -0
- data/spec/unit/assertion/transprocs/inflector/to_snake_path_spec.rb +19 -0
- data/spec/unit/assertion/transprocs/inflector/to_snake_spec.rb +19 -0
- data/spec/unit/assertion/transprocs/list/symbolize_spec.rb +19 -0
- data/spec/unit/assertion_spec.rb +65 -0
- metadata +171 -0
@@ -0,0 +1,36 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# The exception to be raised by invalid assertions' `validate!` method call
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
#
|
9
|
+
class InvalidError < RuntimeError
|
10
|
+
|
11
|
+
# @!scope class
|
12
|
+
# @!method new(*names)
|
13
|
+
# Creates an exception instance
|
14
|
+
#
|
15
|
+
# @param [Symbol, Array<Symbol>] names Wrong names of attribute(s)
|
16
|
+
#
|
17
|
+
# @return [Assertion::InvalidError]
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def initialize(*messages)
|
23
|
+
@messages = messages.flatten.freeze
|
24
|
+
super
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
# @!attribute [r] messages
|
29
|
+
#
|
30
|
+
# @return [Array<String>] The list of error messages
|
31
|
+
#
|
32
|
+
attr_reader :messages
|
33
|
+
|
34
|
+
end # class InvalidError
|
35
|
+
|
36
|
+
end # module Assertion
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# The exception to be raised when a Assertion attribute uses reserved name(s)
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
#
|
9
|
+
class NameError < ::NameError
|
10
|
+
|
11
|
+
# @!scope class
|
12
|
+
# @!method new(*names)
|
13
|
+
# Creates an exception instance
|
14
|
+
#
|
15
|
+
# @param [Symbol, Array<Symbol>] names Wrong names of attribute(s)
|
16
|
+
#
|
17
|
+
# @return [Assertion::NameError]
|
18
|
+
#
|
19
|
+
# @api private
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def initialize(*names)
|
23
|
+
super "Wrong name(s) for attribute(s): #{names.join(", ")}"
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
end # class NameError
|
28
|
+
|
29
|
+
end # module Assertion
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# The exception to be raised when a assertion is applied to some data
|
6
|
+
# before its `check` method has been implemented
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
#
|
10
|
+
class NotImplementedError < ::NotImplementedError
|
11
|
+
|
12
|
+
# @!scope class
|
13
|
+
# @!method new(klass, name)
|
14
|
+
# Creates an exception instance
|
15
|
+
#
|
16
|
+
# @param [Class] klass The class that should implement the instance method
|
17
|
+
# @param [Symbol] name The name of the method to be implemented
|
18
|
+
#
|
19
|
+
# @return [Assertion::NotImplementedError]
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def initialize(klass, name)
|
23
|
+
super "#{klass.name}##{name} method not implemented"
|
24
|
+
freeze
|
25
|
+
end
|
26
|
+
|
27
|
+
end # class NotImplementedError
|
28
|
+
|
29
|
+
end # module Assertion
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# Describes the inversion of the assertion object
|
6
|
+
#
|
7
|
+
# The inversion decorates the source assertion switching its message
|
8
|
+
# (from wrong to right) and reverting its `check`.
|
9
|
+
#
|
10
|
+
# @example
|
11
|
+
# Adult = Assertion.about :name, :age do
|
12
|
+
# age >= 18
|
13
|
+
# end
|
14
|
+
#
|
15
|
+
# assertion = Adult.new
|
16
|
+
# inversion = Inversion.new(assertion)
|
17
|
+
#
|
18
|
+
# assertion.call.valid? == inversion.call.invalid? # => true
|
19
|
+
#
|
20
|
+
# @api private
|
21
|
+
#
|
22
|
+
class Inversion < Base
|
23
|
+
|
24
|
+
# @!scope class
|
25
|
+
# @!method new(assertion)
|
26
|
+
# Creates the inversion for the selected assertion object
|
27
|
+
#
|
28
|
+
# @param [Assertion::Base] assertion The assertion being inverted
|
29
|
+
#
|
30
|
+
# @return [Assertion::Inversion]
|
31
|
+
|
32
|
+
# @private
|
33
|
+
def initialize(assertion)
|
34
|
+
@assertion = assertion
|
35
|
+
freeze
|
36
|
+
end
|
37
|
+
|
38
|
+
# @!attribute [r] assertion
|
39
|
+
#
|
40
|
+
# @return [Assertion::Base] The assertion being inverted
|
41
|
+
#
|
42
|
+
attr_reader :assertion
|
43
|
+
|
44
|
+
# The translated message describing the state of assertion
|
45
|
+
#
|
46
|
+
# @param [Boolean] state
|
47
|
+
#
|
48
|
+
# @return [String]
|
49
|
+
#
|
50
|
+
def message(state = nil)
|
51
|
+
assertion.message(!state)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Checks the current state of the assertion
|
55
|
+
#
|
56
|
+
# @return [Boolean]
|
57
|
+
#
|
58
|
+
def check
|
59
|
+
!assertion.check
|
60
|
+
end
|
61
|
+
|
62
|
+
end # class Inversion
|
63
|
+
|
64
|
+
end # module Assertion
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# Builds inversions for instances of some `Assertion::Base` subclass
|
6
|
+
#
|
7
|
+
# @example
|
8
|
+
# Adult = Assertion.about :name, :age do
|
9
|
+
# age >= 18
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# joe = OpenStruct.new(name: "Joe", age: 40)
|
13
|
+
#
|
14
|
+
# child = Inverter.new(Adult)
|
15
|
+
# child[name: "Joe"].validate!
|
16
|
+
# # => #<Assertion::InvalidError @messages=["Joe is an adult (age 40)"]>
|
17
|
+
#
|
18
|
+
class Inverter
|
19
|
+
|
20
|
+
# @!attribute [r] source
|
21
|
+
#
|
22
|
+
# @return [Class] The `Assertion::Base` sublcass to build negators for
|
23
|
+
#
|
24
|
+
attr_reader :source
|
25
|
+
|
26
|
+
# @!scope class
|
27
|
+
# @!method new(source)
|
28
|
+
# Creates an immutable inversion object for the `Assertion::Base` subclass
|
29
|
+
#
|
30
|
+
# @param [Class] source
|
31
|
+
#
|
32
|
+
# @return [Assertion::Inverter]
|
33
|
+
|
34
|
+
# @private
|
35
|
+
def initialize(source)
|
36
|
+
@source = source
|
37
|
+
freeze
|
38
|
+
end
|
39
|
+
|
40
|
+
# Initializes a [#source] object and builds a negator for it
|
41
|
+
#
|
42
|
+
# @param [Hash] hash The hash of attributes to apply the assertion to
|
43
|
+
#
|
44
|
+
# @return [Assertion::Inverter::Inversion]
|
45
|
+
#
|
46
|
+
def new(hash = {})
|
47
|
+
Inversion.new source.new(hash)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Initializes an assertion, builds its inversion, and applies it to the data
|
51
|
+
#
|
52
|
+
# @param (see #new)
|
53
|
+
#
|
54
|
+
# @return (see Assertion::Base#call)
|
55
|
+
#
|
56
|
+
def [](hash = {})
|
57
|
+
new(hash).call
|
58
|
+
end
|
59
|
+
|
60
|
+
end # class Inverter
|
61
|
+
|
62
|
+
end # module Assertion
|
@@ -0,0 +1,79 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module Assertion
|
4
|
+
|
5
|
+
# Describes the state of the assertion applied to given arguments
|
6
|
+
# (the result of the checkup)
|
7
|
+
#
|
8
|
+
# @api public
|
9
|
+
#
|
10
|
+
class State
|
11
|
+
|
12
|
+
# @!scope class
|
13
|
+
# @!method new(state, *messages)
|
14
|
+
# Creates the immutable state instance with a corresponding error messages
|
15
|
+
#
|
16
|
+
# @param [Boolean] state
|
17
|
+
# @param [String, Array<String>] messages
|
18
|
+
#
|
19
|
+
# @return [Assertion::State]
|
20
|
+
|
21
|
+
# @private
|
22
|
+
def initialize(state, *messages)
|
23
|
+
@state = state
|
24
|
+
@messages = (state ? [] : messages.flatten.uniq).freeze
|
25
|
+
freeze
|
26
|
+
end
|
27
|
+
|
28
|
+
# @!attribute [r] messages
|
29
|
+
#
|
30
|
+
# @return [Array<String>] error messages
|
31
|
+
#
|
32
|
+
attr_reader :messages
|
33
|
+
|
34
|
+
# Check whether a stated assertion is satisfied by its attributes
|
35
|
+
#
|
36
|
+
# @return [Boolean]
|
37
|
+
#
|
38
|
+
def valid?
|
39
|
+
!invalid?
|
40
|
+
end
|
41
|
+
|
42
|
+
# Check whether a stated assertion is not satisfied by its attributes
|
43
|
+
#
|
44
|
+
# @return [Boolean]
|
45
|
+
#
|
46
|
+
def invalid?
|
47
|
+
!@state
|
48
|
+
end
|
49
|
+
|
50
|
+
# Check whether a stated assertion is satisfied by its attributes
|
51
|
+
#
|
52
|
+
# @return [true]
|
53
|
+
#
|
54
|
+
# @raise [Assertion::InvalidError]
|
55
|
+
# When a assertion is not satisfied (validation fails)
|
56
|
+
#
|
57
|
+
def validate!
|
58
|
+
invalid? ? fail(InvalidError.new messages) : true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Composes the state with the other state
|
62
|
+
#
|
63
|
+
# @param [Assertion::State] other
|
64
|
+
#
|
65
|
+
# @return [Assertion::State]
|
66
|
+
# The composed state that carries messages from both the states
|
67
|
+
#
|
68
|
+
# @alias >>
|
69
|
+
# @alias +
|
70
|
+
#
|
71
|
+
def &(other)
|
72
|
+
self.class.new(valid? & other.valid?, messages + other.messages)
|
73
|
+
end
|
74
|
+
alias_method :>>, :&
|
75
|
+
alias_method :+, :&
|
76
|
+
|
77
|
+
end # class State
|
78
|
+
|
79
|
+
end # module Assertion
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module Assertion
|
2
|
+
|
3
|
+
# The collection of pure functions for translating strings in the gem-specific
|
4
|
+
# scopes of `Assertion::Base` subclasses.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
module I18n
|
9
|
+
|
10
|
+
extend ::Transproc::Registry
|
11
|
+
|
12
|
+
uses :to_snake_path, from: Inflector, as: :snake
|
13
|
+
|
14
|
+
# Converts the name of the class to the corresponding gem-specific scope
|
15
|
+
#
|
16
|
+
# @example
|
17
|
+
# fn = I18n[:scope]
|
18
|
+
# fn["Foo::BarBaz"]
|
19
|
+
# # => [:assertion, :"foo/bar_baz"]
|
20
|
+
#
|
21
|
+
# @param [String] name The name of the class
|
22
|
+
#
|
23
|
+
# @return [Array<Symbol>] The `I18n`-compatible gem-specific scope
|
24
|
+
#
|
25
|
+
def scope(name)
|
26
|
+
[:assertion, snake(name).to_sym]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Translates the key with hash of attributes in a given scope
|
30
|
+
#
|
31
|
+
# @example
|
32
|
+
# # config/locales/en.yml
|
33
|
+
# # ---
|
34
|
+
# # en:
|
35
|
+
# # assertion:
|
36
|
+
# # foo:
|
37
|
+
# # qux: "message %{bar}"
|
38
|
+
#
|
39
|
+
# fn = I18n[:translate, [:assertion, :foo], bar: :BAZ]
|
40
|
+
# fn[:qux]
|
41
|
+
# # => "message BAZ"
|
42
|
+
#
|
43
|
+
# @param [key] key The key to be translated
|
44
|
+
# @param [Array<Symbol>] scope The I18n scope for the translations
|
45
|
+
# @param [Hash] hash The hash of attributes for the translation
|
46
|
+
#
|
47
|
+
# @return [String] The translated string
|
48
|
+
#
|
49
|
+
def translate(key, scope, hash)
|
50
|
+
::I18n.t(key, hash.merge(scope: scope))
|
51
|
+
end
|
52
|
+
|
53
|
+
end # module I18n
|
54
|
+
|
55
|
+
end # module Assertion
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Assertion
|
2
|
+
|
3
|
+
# The collection of pure functions for converting constants
|
4
|
+
# to corresponding path names.
|
5
|
+
#
|
6
|
+
# @api private
|
7
|
+
#
|
8
|
+
module Inflector
|
9
|
+
|
10
|
+
extend ::Transproc::Registry
|
11
|
+
|
12
|
+
# @private
|
13
|
+
def to_snake(name)
|
14
|
+
name.gsub(/([a-z])([A-Z])/, '\1_\2').gsub(/_+/, "_").downcase
|
15
|
+
end
|
16
|
+
|
17
|
+
# @private
|
18
|
+
def to_path(name)
|
19
|
+
name.split(%r{\:\:|-|/}).reject(&:empty?).join("/")
|
20
|
+
end
|
21
|
+
|
22
|
+
# Converts the name of the constant to the corresponding path
|
23
|
+
#
|
24
|
+
# @example
|
25
|
+
# fn = Inflector[:to_snake_path]
|
26
|
+
# fn["::Foo::BarBaz"]
|
27
|
+
# # => "foo/bar_baz"
|
28
|
+
#
|
29
|
+
# @param [String] name The name of the constant
|
30
|
+
#
|
31
|
+
# @return [String] The path
|
32
|
+
#
|
33
|
+
def to_snake_path(name)
|
34
|
+
to_path(to_snake(name))
|
35
|
+
end
|
36
|
+
|
37
|
+
end # module Inflector
|
38
|
+
|
39
|
+
end # module Assertion
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module Assertion
|
2
|
+
|
3
|
+
# The collection of pure functions for converting arrays
|
4
|
+
#
|
5
|
+
# @api private
|
6
|
+
#
|
7
|
+
module List
|
8
|
+
|
9
|
+
extend ::Transproc::Registry
|
10
|
+
|
11
|
+
# Converts the nested array of strings and symbols into the flat
|
12
|
+
# array of unique symbols
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# fn = List[:symbolize]
|
16
|
+
# source = [:foo, ["foo", "bar"], :bar, "baz"]
|
17
|
+
# fn[source]
|
18
|
+
# # => [:foo, :bar, :baz]
|
19
|
+
#
|
20
|
+
# @param [Array<String, Symbol, Array>] array
|
21
|
+
#
|
22
|
+
# @return [Array<Symbol>]
|
23
|
+
#
|
24
|
+
def symbolize(array)
|
25
|
+
array.flatten.map(&:to_sym).uniq
|
26
|
+
end
|
27
|
+
|
28
|
+
end # module List
|
29
|
+
|
30
|
+
end # module Assertion
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
describe Assertion do
|
4
|
+
|
5
|
+
let(:load_path) { Dir[File.expand_path "../*.yml", __FILE__] }
|
6
|
+
|
7
|
+
around do |example|
|
8
|
+
old_locale, I18n.locale = I18n.locale, :en
|
9
|
+
old_path, I18n.load_path = I18n.load_path, load_path
|
10
|
+
I18n.backend.load_translations
|
11
|
+
|
12
|
+
example.run
|
13
|
+
|
14
|
+
I18n.locale = old_locale
|
15
|
+
I18n.load_path = old_path
|
16
|
+
end
|
17
|
+
|
18
|
+
it "works" do
|
19
|
+
IsMale = Assertion.about :name, :gender do
|
20
|
+
gender == :male
|
21
|
+
end
|
22
|
+
|
23
|
+
IsAdult = Assertion.about :name, :age do
|
24
|
+
age.to_i >= 18
|
25
|
+
end
|
26
|
+
|
27
|
+
jane = { name: "Jane", gender: :female, age: 19 }
|
28
|
+
|
29
|
+
jane_is_a_male = IsMale[jane]
|
30
|
+
jane_is_a_female = IsMale.not[jane]
|
31
|
+
jane_is_an_adult = IsAdult[jane]
|
32
|
+
jane_is_a_child = IsAdult.not[jane]
|
33
|
+
|
34
|
+
jane_is_a_women = jane_is_a_female & jane_is_an_adult
|
35
|
+
expect(jane_is_a_women).to be_valid
|
36
|
+
expect { jane_is_a_women.validate! }.not_to raise_error
|
37
|
+
|
38
|
+
jane_is_a_boy = jane_is_a_male & jane_is_a_child
|
39
|
+
expect(jane_is_a_boy).not_to be_valid
|
40
|
+
expect(jane_is_a_boy.messages)
|
41
|
+
.to eql ["Jane is a female", "Jane is an adult (age 19)"]
|
42
|
+
expect { jane_is_a_boy.validate! }.to raise_error Assertion::InvalidError
|
43
|
+
end
|
44
|
+
|
45
|
+
after do
|
46
|
+
Object.send :remove_const, :IsAdult
|
47
|
+
Object.send :remove_const, :IsMale
|
48
|
+
end
|
49
|
+
|
50
|
+
end # describe Assertion
|