service_objects 0.1.0 → 1.0.0

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