service_objects 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 +1 -0
- data/.metrics +5 -0
- data/.rspec +2 -0
- data/.rubocop.yml +2 -0
- data/.travis.yml +4 -0
- data/.yardopts +3 -0
- data/Gemfile +3 -0
- data/Guardfile +16 -0
- data/LICENSE +21 -0
- data/README.md +461 -0
- data/Rakefile +17 -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 +14 -0
- data/config/metrics/pippi.yml +3 -0
- data/config/metrics/reek.yml +1 -0
- data/config/metrics/roodi.yml +24 -0
- data/config/metrics/rubocop.yml +75 -0
- data/config/metrics/saikuro.yml +3 -0
- data/config/metrics/simplecov.yml +6 -0
- data/config/metrics/yardstick.yml +37 -0
- data/lib/service_objects/base.rb +43 -0
- data/lib/service_objects/helpers/dependable.rb +63 -0
- data/lib/service_objects/helpers/exceptions.rb +64 -0
- data/lib/service_objects/helpers/messages.rb +96 -0
- data/lib/service_objects/helpers/parameterized.rb +85 -0
- data/lib/service_objects/helpers/parameters.rb +71 -0
- data/lib/service_objects/helpers/validations.rb +54 -0
- data/lib/service_objects/invalid.rb +55 -0
- data/lib/service_objects/listener.rb +97 -0
- data/lib/service_objects/message.rb +117 -0
- data/lib/service_objects/null.rb +26 -0
- data/lib/service_objects/utils/normal_hash.rb +34 -0
- data/lib/service_objects/version.rb +9 -0
- data/lib/service_objects.rb +12 -0
- data/service_objects.gemspec +28 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/tests/base_spec.rb +43 -0
- data/spec/tests/helpers/dependable_spec.rb +77 -0
- data/spec/tests/helpers/exceptions_spec.rb +112 -0
- data/spec/tests/helpers/messages_spec.rb +64 -0
- data/spec/tests/helpers/parameterized_spec.rb +136 -0
- data/spec/tests/helpers/parameters_spec.rb +71 -0
- data/spec/tests/helpers/validations_spec.rb +60 -0
- data/spec/tests/invalid_spec.rb +69 -0
- data/spec/tests/listener_spec.rb +50 -0
- data/spec/tests/message_spec.rb +191 -0
- data/spec/tests/null_spec.rb +17 -0
- data/spec/tests/utils/normal_hash_spec.rb +16 -0
- 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,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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|