service_objects 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 (53) hide show
  1. checksums.yaml +7 -0
  2. data/.coveralls.yml +1 -0
  3. data/.metrics +5 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +2 -0
  6. data/.travis.yml +4 -0
  7. data/.yardopts +3 -0
  8. data/Gemfile +3 -0
  9. data/Guardfile +16 -0
  10. data/LICENSE +21 -0
  11. data/README.md +461 -0
  12. data/Rakefile +17 -0
  13. data/config/metrics/STYLEGUIDE +230 -0
  14. data/config/metrics/cane.yml +5 -0
  15. data/config/metrics/churn.yml +6 -0
  16. data/config/metrics/flay.yml +2 -0
  17. data/config/metrics/metric_fu.yml +14 -0
  18. data/config/metrics/pippi.yml +3 -0
  19. data/config/metrics/reek.yml +1 -0
  20. data/config/metrics/roodi.yml +24 -0
  21. data/config/metrics/rubocop.yml +75 -0
  22. data/config/metrics/saikuro.yml +3 -0
  23. data/config/metrics/simplecov.yml +6 -0
  24. data/config/metrics/yardstick.yml +37 -0
  25. data/lib/service_objects/base.rb +43 -0
  26. data/lib/service_objects/helpers/dependable.rb +63 -0
  27. data/lib/service_objects/helpers/exceptions.rb +64 -0
  28. data/lib/service_objects/helpers/messages.rb +96 -0
  29. data/lib/service_objects/helpers/parameterized.rb +85 -0
  30. data/lib/service_objects/helpers/parameters.rb +71 -0
  31. data/lib/service_objects/helpers/validations.rb +54 -0
  32. data/lib/service_objects/invalid.rb +55 -0
  33. data/lib/service_objects/listener.rb +97 -0
  34. data/lib/service_objects/message.rb +117 -0
  35. data/lib/service_objects/null.rb +26 -0
  36. data/lib/service_objects/utils/normal_hash.rb +34 -0
  37. data/lib/service_objects/version.rb +9 -0
  38. data/lib/service_objects.rb +12 -0
  39. data/service_objects.gemspec +28 -0
  40. data/spec/spec_helper.rb +15 -0
  41. data/spec/tests/base_spec.rb +43 -0
  42. data/spec/tests/helpers/dependable_spec.rb +77 -0
  43. data/spec/tests/helpers/exceptions_spec.rb +112 -0
  44. data/spec/tests/helpers/messages_spec.rb +64 -0
  45. data/spec/tests/helpers/parameterized_spec.rb +136 -0
  46. data/spec/tests/helpers/parameters_spec.rb +71 -0
  47. data/spec/tests/helpers/validations_spec.rb +60 -0
  48. data/spec/tests/invalid_spec.rb +69 -0
  49. data/spec/tests/listener_spec.rb +50 -0
  50. data/spec/tests/message_spec.rb +191 -0
  51. data/spec/tests/null_spec.rb +17 -0
  52. data/spec/tests/utils/normal_hash_spec.rb +16 -0
  53. metadata +182 -0
@@ -0,0 +1,55 @@
1
+ # encoding: utf-8
2
+
3
+ module ServiceObjects
4
+
5
+ # The exception to be risen by invalid services
6
+ #
7
+ # @example
8
+ # object = ServiceObjects::Base.new
9
+ # fail ServiceObjects::Invalid.new object
10
+ class Invalid < ::RuntimeError
11
+
12
+ # @!scope class
13
+ # @!method new(object)
14
+ #
15
+ # Constructs the exception for given service object
16
+ #
17
+ # @example (see ServiceObjects::Invalid)
18
+ #
19
+ # @param [#messages] object
20
+ #
21
+ # @return [ServiceObjects::Invalid]
22
+ def initialize(object)
23
+ @object = object
24
+ validate
25
+ end
26
+
27
+ # @!attribute [r] object
28
+ # Invalid service object
29
+ #
30
+ # @return [#messages]
31
+ attr_reader :object
32
+
33
+ # The array of messages from the invalid {#object}
34
+ #
35
+ # @return [Array<ServiceObjects::Message>]
36
+ def messages
37
+ Array(object.messages).flatten
38
+ end
39
+
40
+ private
41
+
42
+ # Checks if the {#object} respond to {#messages}
43
+ #
44
+ # @raise [TypeError]
45
+ # when the validation fails
46
+ #
47
+ # @return [undefined]
48
+ def validate
49
+ return if object.respond_to? :messages
50
+ fail TypeError.new "#{ object.inspect } doesn't respond to #messages"
51
+ end
52
+
53
+ end # class Invalid
54
+
55
+ end # module ServiceObjects
@@ -0,0 +1,97 @@
1
+ # encoding: utf-8
2
+
3
+ module ServiceObjects
4
+
5
+ # The base class for service listeners
6
+ #
7
+ # @example (see #new)
8
+ #
9
+ # @example (see #finalize)
10
+ #
11
+ # @api public
12
+ class Listener < SimpleDelegator
13
+
14
+ # @!scope class
15
+ # @!method new(object)
16
+ # Listener object constructor
17
+ #
18
+ # Decorates given object by adding methods to be called by service objects
19
+ #
20
+ # @example Decorates an object
21
+ # object = Object.new
22
+ #
23
+ # listener = ServiceObjects::Listener.new object
24
+ # listener.send :__getobj__
25
+ # # => object
26
+ #
27
+ # @param [Object] object
28
+ #
29
+ # @return [ServiceObjects::Listener]
30
+ #
31
+ # @api public
32
+
33
+ # The method called by {#finalize} when no other method has been checked
34
+ #
35
+ # @return [undefined]
36
+ #
37
+ # @abstract
38
+ def otherwise
39
+ end
40
+
41
+ # Calls {#otherwise} in case no existing method has been checked
42
+ #
43
+ # @example Calls {#otherwise} in case no method has been checked
44
+ # class MyListener < ServiceObjects::Listener
45
+ # def on_success
46
+ # "success"
47
+ # end
48
+ #
49
+ # def otherwise
50
+ # "nothing"
51
+ # end
52
+ # end
53
+ #
54
+ # listener = MyListener.new
55
+ # listener.finalize
56
+ # # => "nothing"
57
+ #
58
+ # @example Calls {#otherwise} in case an undefined method has been checked
59
+ #
60
+ # listener = MyListener.new
61
+ # listener.respond_to? :on_error
62
+ # # => false
63
+ #
64
+ # listener.finalize
65
+ # # => "nothing"
66
+ #
67
+ # @example Skips {#otherwise} in case a defined method has been checked
68
+ #
69
+ # listener = MyListener.new
70
+ # listener.respond_to? :on_success
71
+ # # => true
72
+ #
73
+ # listener.finalize
74
+ # # => nil
75
+ #
76
+ # @return [self]
77
+ def finalize
78
+ otherwise unless @notified
79
+
80
+ self
81
+ end
82
+
83
+ # Checks whether the method is defined
84
+ #
85
+ # Remembers the fact that any defined method has been checked
86
+ #
87
+ # @example
88
+ # respond_to? :finalize, false # => true
89
+ #
90
+ # @return [Boolean]
91
+ def respond_to?(*)
92
+ super ? (@notified = true) : false
93
+ end
94
+
95
+ end # class Listener
96
+
97
+ end # module ServiceObjects
@@ -0,0 +1,117 @@
1
+ # encoding: utf-8
2
+ require "json"
3
+
4
+ module ServiceObjects
5
+
6
+ # Describes messages published by service objects
7
+ class Message
8
+ include Comparable
9
+
10
+ # @!attribute [r] type
11
+ # The type of the message
12
+ #
13
+ # @return [String]
14
+ attr_reader :type
15
+
16
+ # @!attribute [r] text
17
+ # The text of the message
18
+ #
19
+ # @return [String]
20
+ attr_reader :text
21
+
22
+ # @!attribute [r] priority
23
+ # The priority level for the message
24
+ #
25
+ # If priority hasn't been set on initialization, sets it to -1.0 for errors,
26
+ # or 0.0 otherwise.
27
+ #
28
+ # @return [Float]
29
+ def priority
30
+ return @custom_priority.to_f if @custom_priority
31
+ type == "error" ? -1.0 : 0.0
32
+ end
33
+
34
+ # @!scope class
35
+ # @!method new(type:, text:, priority:)
36
+ # Constructs and freezes the message object
37
+ #
38
+ # @example
39
+ # Message.new type: "info", text: "foo", priority: 3
40
+ #
41
+ # @param [#to_s] type
42
+ # @param [#to_s] text
43
+ # @param [#to_f] priority
44
+ # optional custom priority of the message (the less the higher)
45
+ #
46
+ # @return [ServiceObjects::Message]
47
+ # the frozen message object
48
+ def initialize(type:, text:, priority: nil)
49
+ @type = type.to_s.freeze
50
+ @text = text.to_s.freeze
51
+ @custom_priority = priority
52
+ freeze
53
+ end
54
+
55
+ # Checks equality of the message to the other object
56
+ #
57
+ # @param [Object] other
58
+ #
59
+ # @return [true]
60
+ # if messages has the same attributes
61
+ # @return [false]
62
+ # if the other object is not a message or has different argument(s)
63
+ def ==(other)
64
+ (self <=> other) == 0
65
+ end
66
+
67
+ # Compares the message with the other object
68
+ #
69
+ # @param [Object] other
70
+ #
71
+ # @return [-1, 0, 1]
72
+ # if the argument is a message
73
+ # @return [nil]
74
+ # if the other object is not a message
75
+ def <=>(other)
76
+ return unless other.is_a? self.class
77
+ [:priority, :type, :text]
78
+ .map { |key| __compare_to__ other, by: key }
79
+ .detect { |value| value } || 0
80
+ end
81
+
82
+ # Converts the message object to hash
83
+ #
84
+ # @return [Hash]
85
+ def to_h
86
+ { type: type, text: text }
87
+ end
88
+
89
+ # Converts the message object to json
90
+ #
91
+ # @return [String]
92
+ def to_json
93
+ to_h.to_json
94
+ end
95
+
96
+ # A human-readable representation of the message
97
+ #
98
+ # @return [String]
99
+ def inspect
100
+ %W(
101
+ #<ServiceObjects::Message:#{ object_id }
102
+ type=\"#{ type }\"
103
+ text=\"#{ text }\"
104
+ priority=#{ priority }>
105
+ ).join(" ")
106
+ end
107
+
108
+ private
109
+
110
+ def __compare_to__(other, by:)
111
+ value = (send(by) <=> other.send(by))
112
+ value == 0 ? nil : value
113
+ end
114
+
115
+ end # class Message
116
+
117
+ end # module ServiceObjects
@@ -0,0 +1,26 @@
1
+ # encoding: utf-8
2
+ require "naught"
3
+
4
+ module ServiceObjects #:nodoc:
5
+
6
+ # Implements Null Object pattern
7
+ #
8
+ # @note
9
+ # The constant is a singleton black hole object that returns
10
+ # itself to any method call
11
+ #
12
+ # @example
13
+ # ServiceObjects::NULL.respond_to? :arbitrary_method
14
+ # # => true
15
+ #
16
+ # ServiceObjects::NULL.arbitrary_method
17
+ # # => ServiceObjects::NULL
18
+ #
19
+ # @see https://github.com/avdi/naught
20
+ # documentation for the 'naught' gem by Avdi Grimm
21
+ NULL = Naught.build do |config|
22
+ config.black_hole
23
+ config.singleton
24
+ end.instance
25
+
26
+ end # module ServiceObjects
@@ -0,0 +1,34 @@
1
+ # encoding: utf-8
2
+
3
+ module ServiceObjects
4
+
5
+ # Utilitites for ServiceObjects module
6
+ module Utils
7
+
8
+ # Constructor of hash with keys, symbolized at any level
9
+ #
10
+ # @example
11
+ # NormalHash.from { "foo" => "bar", "bar" => { "baz", "bar" => "baz" } }
12
+ # # => { foo: "baz", bar: { "baz", bar: "baz" } }
13
+ #
14
+ # @api private
15
+ module NormalHash
16
+
17
+ # Recursively symbolizes keys of hash at any level
18
+ #
19
+ # @param [Hash] hash
20
+ #
21
+ # @return [Hash]
22
+ def self.from(hash)
23
+ return {} unless hash.is_a? Hash
24
+
25
+ hash.inject({}) do |out, (key, val)|
26
+ out.merge(key.to_sym => (val.is_a?(Hash) ? from(val) : val))
27
+ end
28
+ end
29
+
30
+ end # class NormalHash
31
+
32
+ end # module Utils
33
+
34
+ end # module ServiceObjects
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ module ServiceObjects
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 ServiceObjects
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+ require "service_objects/null"
3
+ require "service_objects/utils/normal_hash"
4
+ require "service_objects/listener"
5
+ require "service_objects/message"
6
+ require "service_objects/invalid"
7
+ require "service_objects/base"
8
+
9
+ # The namespace for the 'service_objects' gem code
10
+ module ServiceObjects
11
+
12
+ end # module ServiceObjects
@@ -0,0 +1,28 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ require "service_objects/version"
3
+
4
+ Gem::Specification.new do |s|
5
+ s.name = "service_objects"
6
+ s.version = ServiceObjects::VERSION.dup
7
+ s.author = "Andrew Kozin"
8
+ s.email = "andrew.kozin@gmail.com"
9
+ s.homepage = "https://github.com/nepalez/service_objects"
10
+ s.summary = "Service objects (interactors)."
11
+ s.description = "Base class for objects, that implements" \
12
+ " both the Interactor and Observer design patterns."
13
+ s.license = "MIT"
14
+ s.platform = Gem::Platform::RUBY
15
+ s.required_ruby_version = "~> 2.1"
16
+
17
+ s.require_paths = ["lib"]
18
+ s.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
19
+ s.test_files = Dir["spec/**/*.rb"]
20
+ s.extra_rdoc_files = Dir["README.md", "LICENSE", "config/metrics/STYLEGUIDE"]
21
+
22
+ s.add_runtime_dependency "activemodel", "~> 4.1"
23
+ s.add_runtime_dependency "extlib", "~> 0.9"
24
+ s.add_runtime_dependency "naught", "~> 1.0"
25
+ s.add_runtime_dependency "wisper", "~> 1.6"
26
+
27
+ s.add_development_dependency "hexx-suit", "~> 0.0"
28
+ end
@@ -0,0 +1,15 @@
1
+ # encoding: utf-8
2
+
3
+ # Loads the RSpec test suit.
4
+ require "hexx-suit"
5
+
6
+ # Loads the RSpec support files.
7
+ Dir.chdir File.expand_path("..", __FILE__) do
8
+ Dir["./support/config/*.rb"].each { |file| require file }
9
+ end
10
+
11
+ # Loads runtime metrics in the current scope
12
+ Hexx::Suit.load_metrics_for(self)
13
+
14
+ # Loads the code of the module.
15
+ require "service_objects"
@@ -0,0 +1,43 @@
1
+ # encoding: utf-8
2
+
3
+ describe ServiceObjects::Base do
4
+
5
+ let(:publisher) { Wisper::Publisher }
6
+ let(:helpers) { ServiceObjects::Helpers }
7
+
8
+ it "is Dependable" do
9
+ expect(described_class).to be_kind_of helpers::Dependable
10
+ end
11
+
12
+ it "is Parameterized" do
13
+ expect(described_class).to be_kind_of helpers::Parameterized
14
+ end
15
+
16
+ it "includes Messages helper" do
17
+ expect(described_class).to include helpers::Messages
18
+ end
19
+
20
+ it "includes Parameters helper" do
21
+ expect(described_class).to include helpers::Parameters
22
+ end
23
+
24
+ it "includes Validations helper" do
25
+ expect(described_class).to include helpers::Validations
26
+ end
27
+
28
+ it "includes Exceptions helper" do
29
+ expect(described_class).to include helpers::Exceptions
30
+ end
31
+
32
+ it "includes Wisper::Publisher" do
33
+ expect(described_class).to include publisher
34
+ end
35
+
36
+ describe "#run" do
37
+
38
+ it "is defined" do
39
+ expect(subject).to respond_to(:run).with(0).arguments
40
+ end
41
+ end
42
+
43
+ end # describe ServiceObjects::Base