service_objects 0.1.0 → 1.0.0

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/.coveralls.yml +1 -0
  3. data/.metrics +1 -0
  4. data/.travis.yml +9 -1
  5. data/.yardopts +1 -1
  6. data/Gemfile +1 -1
  7. data/Guardfile +29 -8
  8. data/LICENSE +1 -1
  9. data/README.md +179 -342
  10. data/Rakefile +3 -3
  11. data/config/metrics/churn.yml +1 -1
  12. data/config/metrics/flay.yml +1 -1
  13. data/config/metrics/metric_fu.yml +1 -0
  14. data/config/metrics/rubocop.yml +4 -4
  15. data/config/metrics/simplecov.yml +1 -1
  16. data/lib/service_objects.rb +6 -9
  17. data/lib/service_objects/base.rb +190 -17
  18. data/lib/service_objects/listener.rb +21 -75
  19. data/lib/service_objects/message.rb +15 -96
  20. data/lib/service_objects/version.rb +1 -1
  21. data/service_objects.gemspec +11 -9
  22. data/spec/lib/base_spec.rb +247 -0
  23. data/spec/lib/listener_spec.rb +96 -0
  24. data/spec/lib/message_spec.rb +48 -0
  25. data/spec/spec_helper.rb +8 -6
  26. metadata +56 -93
  27. data/bin/service +0 -17
  28. data/config/metrics/pippi.yml +0 -3
  29. data/lib/service_objects/cli.rb +0 -117
  30. data/lib/service_objects/cli/locale.erb +0 -20
  31. data/lib/service_objects/cli/service.erb +0 -125
  32. data/lib/service_objects/cli/spec.erb +0 -87
  33. data/lib/service_objects/helpers.rb +0 -17
  34. data/lib/service_objects/helpers/dependable.rb +0 -63
  35. data/lib/service_objects/helpers/exceptions.rb +0 -64
  36. data/lib/service_objects/helpers/messages.rb +0 -95
  37. data/lib/service_objects/helpers/parameterized.rb +0 -85
  38. data/lib/service_objects/helpers/parameters.rb +0 -71
  39. data/lib/service_objects/helpers/validations.rb +0 -54
  40. data/lib/service_objects/invalid.rb +0 -55
  41. data/lib/service_objects/null.rb +0 -26
  42. data/lib/service_objects/parsers.rb +0 -13
  43. data/lib/service_objects/parsers/dependency.rb +0 -69
  44. data/lib/service_objects/parsers/notification.rb +0 -85
  45. data/lib/service_objects/rspec.rb +0 -75
  46. data/lib/service_objects/utils/normal_hash.rb +0 -34
  47. data/spec/tests/base_spec.rb +0 -43
  48. data/spec/tests/bin/service_spec.rb +0 -18
  49. data/spec/tests/cli_spec.rb +0 -179
  50. data/spec/tests/helpers/dependable_spec.rb +0 -77
  51. data/spec/tests/helpers/exceptions_spec.rb +0 -112
  52. data/spec/tests/helpers/messages_spec.rb +0 -64
  53. data/spec/tests/helpers/parameterized_spec.rb +0 -136
  54. data/spec/tests/helpers/parameters_spec.rb +0 -71
  55. data/spec/tests/helpers/validations_spec.rb +0 -60
  56. data/spec/tests/invalid_spec.rb +0 -69
  57. data/spec/tests/listener_spec.rb +0 -73
  58. data/spec/tests/message_spec.rb +0 -191
  59. data/spec/tests/null_spec.rb +0 -17
  60. data/spec/tests/parsers/dependency_spec.rb +0 -29
  61. data/spec/tests/parsers/notification_spec.rb +0 -84
  62. data/spec/tests/rspec_spec.rb +0 -86
  63. data/spec/tests/utils/normal_hash_spec.rb +0 -16
data/Rakefile CHANGED
@@ -9,7 +9,7 @@ end
9
9
  # Loads bundler tasks
10
10
  Bundler::GemHelper.install_tasks
11
11
 
12
- # Loads the Hexx::Suit and its tasks
12
+ # Loads the Hexx::RSpec and its tasks
13
13
  begin
14
14
  require "hexx-suit"
15
15
  Hexx::Suit.install_tasks
@@ -18,5 +18,5 @@ rescue LoadError
18
18
  Hexx::RSpec.install_tasks
19
19
  end
20
20
 
21
- # Sets the Hexx::RSpec task by default
22
- task default: :test
21
+ # Sets the Hexx::RSpec :test task to default
22
+ task default: "test:coverage:run"
@@ -2,5 +2,5 @@
2
2
  ignore_files:
3
3
  - spec
4
4
  - config
5
- minimum_churn_count: 2
5
+ minimum_churn_count: 0
6
6
  start_date: "1 year ago"
@@ -1,2 +1,2 @@
1
1
  ---
2
- minimum_score: 8
2
+ minimum_score: 5
@@ -1,6 +1,7 @@
1
1
  ---
2
2
  folders: # The list of folders to be used by any metric.
3
3
  - lib
4
+ - app
4
5
  metrics: # The list of allowed metrics. The other metrics are disabled.
5
6
  - cane
6
7
  - churn
@@ -3,6 +3,10 @@
3
3
  # output: "tmp/rubocop"
4
4
  # format: "html"
5
5
 
6
+ AllCops:
7
+ Exclude:
8
+ - '**/db/schema.rb'
9
+
6
10
  Lint/HandleExceptions:
7
11
  Exclude:
8
12
  - '**/*_spec.rb'
@@ -46,10 +50,6 @@ Style/FileName:
46
50
  Style/RaiseArgs:
47
51
  EnforcedStyle: compact
48
52
 
49
- Style/RescueModifier:
50
- Exclude:
51
- - '**/*_spec.rb'
52
-
53
53
  Style/SingleLineMethods:
54
54
  Exclude:
55
55
  - '**/*_spec.rb'
@@ -3,4 +3,4 @@ output: tmp/coverage
3
3
  filters: # The list of paths to be excluded from coverage checkup
4
4
  - "spec/"
5
5
  - "config/"
6
- groups: []
6
+ groups: []
@@ -1,14 +1,11 @@
1
1
  # encoding: utf-8
2
2
 
3
- # The namespace for the 'service_objects' gem code
4
- module ServiceObjects
3
+ require_relative "service_objects/version"
4
+ require_relative "service_objects/message"
5
+ require_relative "service_objects/listener"
6
+ require_relative "service_objects/base"
5
7
 
6
- require_relative "service_objects/version"
7
- require_relative "service_objects/null"
8
- require_relative "service_objects/utils/normal_hash"
9
- require_relative "service_objects/listener"
10
- require_relative "service_objects/message"
11
- require_relative "service_objects/invalid"
12
- require_relative "service_objects/base"
8
+ # Namespace for the code of the 'service_objects' gem
9
+ module ServiceObjects
13
10
 
14
11
  end # module ServiceObjects
@@ -1,36 +1,209 @@
1
1
  # encoding: utf-8
2
+
3
+ require "attestor"
4
+ require "attr_coerced"
5
+ require "virtus"
2
6
  require "wisper"
3
7
 
4
8
  module ServiceObjects
5
9
 
6
- require_relative "helpers"
7
-
8
10
  # Base class for service objects
9
11
  #
10
12
  # @example
11
- # AddItem = Class.new(ServiceObjects::Base)
13
+ # class ChangeFoo < ServiceObjects::Base
14
+ #
15
+ # # Attributes definition
16
+ # attribute :id, Integer
17
+ # attribute :name
18
+ # attr_coerced :name, String
19
+ #
20
+ # # Object validation rules
21
+ # validate { invalid :blank_id unless id }
22
+ # validate { invalid :blank_name unless name }
23
+ #
24
+ # # Injectable dependencies
25
+ # dependency :get_foo, default: GetFoo
26
+ #
27
+ # private
28
+ #
29
+ # # The main algorithm
30
+ # def run!
31
+ # validate
32
+ # get_foo
33
+ # change_foo
34
+ # end
35
+ #
36
+ # attr_accessor :foo
12
37
  #
13
- # @see http://www.rubydoc.info/github/krisleech/wisper
14
- # 'wisper' gem by Kris Leech
15
- # @see http://apidock.com/rails/v4.1.8/ActiveModel/Validations
16
- # ActiveModel::Validations
38
+ # def get_foo
39
+ # run_service get_foo.new(id: id), Listener.new(self)
40
+ # end
41
+ #
42
+ # def change_foo
43
+ # foo.save! name: name
44
+ # publish :changed, foo, message(:success, :changed, name: name)
45
+ # rescue => error
46
+ # publish :error, message(:error, error.message)
47
+ # end
48
+ #
49
+ # class Listener < ServiceObjects::Listener
50
+ # def on_found(foo, *)
51
+ # self.foo = foo
52
+ # end
53
+ #
54
+ # def otherwise
55
+ # publish :not_found, message(:error, :not_found, id: id)
56
+ # end
57
+ # end
58
+ # end
59
+ #
60
+ # @see https://github.com/solnic/virtus
61
+ # Virtus API
62
+ # @see http://www.rubydoc.info/gems/wisper
63
+ # Wisper::Publisher API
64
+ # @see http://www.rubydoc.info/gems/attestor
65
+ # Attestor API
66
+ # @see http://www.rubydoc.info/gems/attr_coerced
67
+ # AttrCorerced API
17
68
  class Base
18
69
 
19
- extend Helpers::Dependable
20
- extend Helpers::Parameterized
21
-
22
- include Helpers::Exceptions
23
- include Helpers::Validations
70
+ # @!parse include Virtus::Model::Core
71
+ __send__ :include, Virtus.model
72
+ include AttrCoerced
73
+ include Attestor::Validations
24
74
  include Wisper::Publisher
25
- # @!parse include ServiceObjects::Helpers::Parameters
26
- # @!parse include ServiceObjects::Helpers::Messages
27
- # @!parse include ActiveModel::Validations
28
75
 
29
- # @abstract
30
- # Runs service object
76
+ # Declares the dependency from another class
77
+ #
78
+ # @param [#to_sym] name
79
+ # @option [Class] :default
80
+ #
81
+ # @return [undefined]
82
+ def self.dependency(name, default: nil)
83
+ attr_accessor(name)
84
+ return unless default
85
+ define_method(name) { instance_eval "@#{ name } ||= #{ default }" }
86
+ end
87
+
88
+ # @!method subscribe(listener, prefix: nil)
89
+ # Subscribes a listener for the service object's notifications
90
+ #
91
+ # @param [Object] listener
92
+ # @option [#to_sym, nil] :prefix
93
+ #
94
+ # @return [undefined]
95
+ #
96
+ # @see https://github.com/krisleech/wisper 'wisper' gem API
97
+
98
+ # Runs the service by calling {#run!} and catching its :published throws
99
+ #
100
+ # @raise [NotImplementedError] if {#run!} method not implemented yet
31
101
  #
32
102
  # @return [undefined]
33
103
  def run
104
+ catch(:published) { run! }
105
+ end
106
+
107
+ # Validates the object in given context
108
+ #
109
+ # Publishes error if validation fails
110
+ #
111
+ # @param [#to_sym] context
112
+ #
113
+ # @return [undefined]
114
+ def validate(context)
115
+ validate!(context)
116
+ rescue Attestor::InvalidError => error
117
+ publish :error, error.messages.map { |text| Message.new :error, text }
118
+ end
119
+
120
+ # Runs the service and receives its notifications to listener
121
+ #
122
+ # @param [ServiceObjects::Base] service
123
+ # @param [ServiceObjects::Listener] listener
124
+ #
125
+ # @return [undefined]
126
+ def run_service(service, listener)
127
+ service.subscribe listener, prefix: :on
128
+ service.run
129
+ listener.finalize
130
+ end
131
+
132
+ # @!method message(type, key, options)
133
+ # Creates the message of given type
134
+ #
135
+ # @overload message(type, key, options)
136
+ # Translates the symbolic key
137
+ #
138
+ # @example
139
+ # # config/locales/en.yml
140
+ # en:
141
+ # service_objects:
142
+ # get_foo:
143
+ # info:
144
+ # found: "Item with id %{id} has been found"
145
+ #
146
+ # @example
147
+ # result = GetFoo.new.message(:info, :found, id: 1)
148
+ #
149
+ # result.class # => ServiceObjects::Message
150
+ # result.type # => :info
151
+ # result.text # => "Item with id 1 has been found"
152
+ #
153
+ # @param [Symbol] type
154
+ # @param [Symbol] key
155
+ # @param [Hash] options
156
+ #
157
+ # @return [ServiceObjects::Message]
158
+ #
159
+ # @overload message(type, key)
160
+ # Stringifies non-symbolic key
161
+ #
162
+ # @example
163
+ # result = GetFoo.new.message(:info, 1)
164
+ #
165
+ # result.class # => ServiceObjects::Message
166
+ # result.type # => :info
167
+ # result.text # => "1"
168
+ #
169
+ # @param [Symbol] type
170
+ # @param [#to_s] key
171
+ #
172
+ # @return [ServiceObjects::Message]
173
+ def message(type, *args)
174
+ Message.new type, translate(type, *args)
175
+ end
176
+
177
+ # @!method publish(notification, *args)
178
+ # Notifies subscribers and then throws :published
179
+ #
180
+ # @param [#to_sym] notification
181
+ # @param [Array] args
182
+ #
183
+ # @raise [UncaughtThrowError] :published
184
+ #
185
+ # @return [undefined]
186
+ #
187
+ # @see https://github.com/krisleech/wisper 'wisper' gem API
188
+ def publish(*)
189
+ super
190
+ throw :published
191
+ end
192
+
193
+ private
194
+
195
+ def run!
196
+ fail NotImplementedError.new "#{ self.class }#run! not implemented"
197
+ end
198
+
199
+ def __class_path__
200
+ @__class_path__ ||= self.class.name.to_const_path.to_sym
201
+ end
202
+
203
+ def translate(type, message, **options)
204
+ return message unless message.instance_of? Symbol
205
+ scope = [:service_objects, __class_path__, type]
206
+ I18n.t message, options.merge(scope: scope)
34
207
  end
35
208
 
36
209
  end # class Base
@@ -1,110 +1,56 @@
1
1
  # encoding: utf-8
2
2
 
3
+ require "chronicles"
4
+
3
5
  module ServiceObjects
4
6
 
5
- # The base class for service listeners
6
- #
7
- # @example (see #new)
8
- #
9
- # @example (see #finalize)
10
- #
11
- # @api public
7
+ # Describes a decorator with callbacks to receive notifications from services
12
8
  class Listener
9
+ include Chronicles
13
10
 
14
11
  # @!scope class
15
12
  # @!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 :object
25
- # # => object
13
+ # Creates the listener decorator for given object
26
14
  #
27
15
  # @param [Object] object
28
16
  #
29
17
  # @return [ServiceObjects::Listener]
30
- #
31
- # @api public
18
+
19
+ # @private
32
20
  def initialize(object)
33
- @object = object
21
+ @__getobj__ = object
22
+ start_chronicles except: %i(finalize __getobj__)
34
23
  end
35
24
 
36
- # The method called by {#finalize} when no other method has been checked
25
+ # @abstract
37
26
  #
38
- # @return [undefined]
27
+ # The method to be called by {#finalize} when no own methods has been called
39
28
  #
40
- # @abstract
29
+ # @return [undefined]
41
30
  def otherwise
42
31
  end
43
32
 
44
- # Calls {#otherwise} in case no existing method has been checked
45
- #
46
- # @example Calls {#otherwise} in case no method has been checked
47
- # class MyListener < ServiceObjects::Listener
48
- # def on_success
49
- # "success"
50
- # end
51
- #
52
- # def otherwise
53
- # "nothing"
54
- # end
55
- # end
33
+ # Calls {#otherwise} when no own methods has been called
56
34
  #
57
- # listener = MyListener.new
58
- # listener.finalize
59
- # # => "nothing"
35
+ # Clears chronicles.
60
36
  #
61
- # @example Calls {#otherwise} in case an undefined method has been checked
62
- #
63
- # listener = MyListener.new
64
- # listener.respond_to? :on_error
65
- # # => false
66
- #
67
- # listener.finalize
68
- # # => "nothing"
69
- #
70
- # @example Skips {#otherwise} in case a defined method has been checked
71
- #
72
- # listener = MyListener.new
73
- # listener.respond_to? :on_success
74
- # # => true
75
- #
76
- # listener.finalize
77
- # # => nil
78
- #
79
- # @return [self]
37
+ # @return [undefined]
80
38
  def finalize
81
- otherwise unless @notified
82
-
83
- self
84
- end
85
-
86
- # Checks whether the method is defined
87
- #
88
- # Remembers the fact that any defined method has been checked
89
- #
90
- # @return [Boolean]
91
- #
92
- # @api private
93
- def respond_to?(*)
94
- super ? (@notified = true) : false
39
+ otherwise unless chronicles.empty?
40
+ chronicles.clear
95
41
  end
96
42
 
97
43
  # @private
98
44
  def respond_to_missing?(*args)
99
- object.respond_to?(*args)
45
+ __getobj__.respond_to?(*args)
100
46
  end
101
47
 
102
48
  private
103
49
 
104
- attr_reader :object
50
+ attr_reader :__getobj__
105
51
 
106
- def method_missing(*args, &block)
107
- object.send(*args, &block)
52
+ def method_missing(*args)
53
+ __getobj__.__send__(*args)
108
54
  end
109
55
 
110
56
  end # class Listener
@@ -1,116 +1,35 @@
1
1
  # encoding: utf-8
2
- require "json"
3
2
 
4
3
  module ServiceObjects
5
4
 
6
- # Describes messages published by service objects
5
+ # Describes messages to be returned by service objects
7
6
  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
7
 
34
8
  # @!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
9
+ # @!method new(type, text)
10
+ # Creates the immutable message with type and text
40
11
  #
41
- # @param [#to_s] type
12
+ # @param [#to_sym] type
42
13
  # @param [#to_s] text
43
- # @param [#to_f] priority
44
- # optional custom priority of the message (the less the higher)
45
14
  #
46
15
  # @return [ServiceObjects::Message]
47
- # the frozen message object
48
- def initialize(type:, text:, priority: nil)
49
- @type = type.to_s.freeze
16
+
17
+ # @private
18
+ def initialize(type, text)
19
+ @type = type.to_sym
50
20
  @text = text.to_s.freeze
51
- @custom_priority = priority
52
21
  freeze
53
22
  end
54
23
 
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
- #
24
+ # @!attribute [r] text
25
+ # The text of the message
98
26
  # @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
27
+ attr_reader :text
109
28
 
110
- def __compare_to__(other, by:)
111
- value = (send(by) <=> other.send(by))
112
- value == 0 ? nil : value
113
- end
29
+ # @!attribute [r] type
30
+ # The type of the message
31
+ # @return [Symbol]
32
+ attr_reader :type
114
33
 
115
34
  end # class Message
116
35