assertion 0.0.1

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