assertion 0.0.1
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 +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
|