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.
Files changed (55) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +2 -0
  3. data/.gitignore +9 -0
  4. data/.metrics +9 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +2 -0
  7. data/.travis.yml +19 -0
  8. data/.yardopts +3 -0
  9. data/Gemfile +9 -0
  10. data/Guardfile +18 -0
  11. data/LICENSE +21 -0
  12. data/README.md +222 -0
  13. data/Rakefile +29 -0
  14. data/assertion.gemspec +27 -0
  15. data/config/metrics/STYLEGUIDE +230 -0
  16. data/config/metrics/cane.yml +5 -0
  17. data/config/metrics/churn.yml +6 -0
  18. data/config/metrics/flay.yml +2 -0
  19. data/config/metrics/metric_fu.yml +15 -0
  20. data/config/metrics/reek.yml +1 -0
  21. data/config/metrics/roodi.yml +24 -0
  22. data/config/metrics/rubocop.yml +72 -0
  23. data/config/metrics/saikuro.yml +3 -0
  24. data/config/metrics/simplecov.yml +6 -0
  25. data/config/metrics/yardstick.yml +37 -0
  26. data/lib/assertion.rb +79 -0
  27. data/lib/assertion/base.rb +186 -0
  28. data/lib/assertion/exceptions/invalid_error.rb +36 -0
  29. data/lib/assertion/exceptions/name_error.rb +29 -0
  30. data/lib/assertion/exceptions/not_implemented_error.rb +29 -0
  31. data/lib/assertion/inversion.rb +64 -0
  32. data/lib/assertion/inverter.rb +62 -0
  33. data/lib/assertion/state.rb +79 -0
  34. data/lib/assertion/transprocs/i18n.rb +55 -0
  35. data/lib/assertion/transprocs/inflector.rb +39 -0
  36. data/lib/assertion/transprocs/list.rb +30 -0
  37. data/lib/assertion/version.rb +9 -0
  38. data/spec/integration/assertion_spec.rb +50 -0
  39. data/spec/integration/en.yml +10 -0
  40. data/spec/spec_helper.rb +12 -0
  41. data/spec/unit/assertion/base_spec.rb +221 -0
  42. data/spec/unit/assertion/exceptions/invalid_error_spec.rb +40 -0
  43. data/spec/unit/assertion/exceptions/name_error_spec.rb +26 -0
  44. data/spec/unit/assertion/exceptions/not_implemented_error_spec.rb +26 -0
  45. data/spec/unit/assertion/inversion_spec.rb +89 -0
  46. data/spec/unit/assertion/inverter_spec.rb +80 -0
  47. data/spec/unit/assertion/state_spec.rb +224 -0
  48. data/spec/unit/assertion/transprocs/i18n/to_scope_spec.rb +19 -0
  49. data/spec/unit/assertion/transprocs/i18n/translate_spec.rb +28 -0
  50. data/spec/unit/assertion/transprocs/inflector/to_path_spec.rb +19 -0
  51. data/spec/unit/assertion/transprocs/inflector/to_snake_path_spec.rb +19 -0
  52. data/spec/unit/assertion/transprocs/inflector/to_snake_spec.rb +19 -0
  53. data/spec/unit/assertion/transprocs/list/symbolize_spec.rb +19 -0
  54. data/spec/unit/assertion_spec.rb +65 -0
  55. 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,9 @@
1
+ # encoding: utf-8
2
+
3
+ module Assertion
4
+
5
+ # The semantic version of the module.
6
+ # @see http://semver.org/ Semantic versioning 2.0
7
+ VERSION = "0.0.1".freeze
8
+
9
+ 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