hexx 2.2.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d010b19ba275b8486d80e503db0486446a407e4e
4
- data.tar.gz: e0f93d5c6656747d41f70dea4d7f5f71f11ed5f8
3
+ metadata.gz: fb67d554082720fe92957b12996f0a313b3b3ae8
4
+ data.tar.gz: 197971ca631d755c770543cf8393d173bff65ff3
5
5
  SHA512:
6
- metadata.gz: d76d30cf7bf4d130790c9cae1cf3affee4dcd4d09d2643c098c31b1eb167a8ad86127b50460d1bacdeb8bfbd368a962d649eab3f50beb67ee142b0381bc798fb
7
- data.tar.gz: 6b4cff429dd38d0188849e697c89aa25d66365aabbe33ad0026d299906f0335d0172d2f7f2ba82b2927504d72e846efc9c9af3ea4b825b59581f7e1aebf3c087
6
+ metadata.gz: 2616e0e816e014aba36151690e5d5546ace6adcd2e1ec08bef86191526bf594576359ed0b3eace7ce4380eb629c00d675cd0bb1aef3e1ab5eb6918a6b8e018ef
7
+ data.tar.gz: b811fd9b51612ea91d71c75b0c87fedfe2146a10f78b4327953390fc7cd430dec07f9475e5d72b7aa5f00660b1309b81a859805fd37c7b8ead676f666e11c3f8
data/.rubocop.yml CHANGED
@@ -14,6 +14,9 @@ Style/AccessorMethodName:
14
14
  Exclude:
15
15
  - 'spec/**/*'
16
16
 
17
+ Style/AsciiComments:
18
+ Enabled: false
19
+
17
20
  Style/ClassAndModuleChildren:
18
21
  Exclude:
19
22
  - 'spec/**/*'
data/README.rdoc CHANGED
@@ -63,7 +63,7 @@ A typical service object is shown below:
63
63
  # runs a service
64
64
  def run
65
65
  # a wrapper sends :error notification to listeners in case of any exception raised.
66
- transaction
66
+ escape
67
67
  MyModel.save! name: name # name is defined by allow_params helper.
68
68
  publish :success # notifies its listeners.
69
69
  end
data/Rakefile CHANGED
@@ -19,5 +19,6 @@ task :check do
19
19
  system "coveralls report"
20
20
  system "rubocop"
21
21
  system "inch --pedantic"
22
- system "metric_fu"
22
+ system "metric_fu --no-cane --no-churn --no-rails-best-practices " \
23
+ "--no-hotspots --no-reek"
23
24
  end
data/lib/hexx.rb CHANGED
@@ -1,9 +1,16 @@
1
+ # encoding: utf-8
1
2
  require "active_model"
2
3
  require "wisper"
3
4
 
4
- # Application files to be loaded.
5
- def files
6
- @files ||= Dir[File.expand_path("../hexx/**/*.rb", __FILE__)]
7
- end
5
+ # Loading gem code (the order is essential).
6
+ root = File.expand_path "../hexx", __FILE__
7
+ Dir[
8
+ "#{ root }/models/**/*.rb",
9
+ "#{ root }/service/**/*.rb",
10
+ "#{ root }/**/*.rb"
11
+ ].each { |f| require f }
8
12
 
9
- files.each { |file| require file }
13
+ # @api show
14
+ # Namespace for the gem.
15
+ module Hexx
16
+ end
data/lib/hexx/models.rb CHANGED
@@ -1,8 +1,8 @@
1
- require_relative "models/base_coercer"
1
+ # encoding: utf-8
2
2
 
3
3
  module Hexx
4
4
 
5
- # Declares the +.attr_coerced+ private class method.
5
+ # Declares the +attr_coerced+ private class method.
6
6
  #
7
7
  # @example
8
8
  #
@@ -14,14 +14,28 @@ module Hexx
14
14
  # attr_coerced :name, type: ActiveSupport::Multibyte::Chars
15
15
  # end
16
16
  #
17
+ # # This will coerce user name with mb chars:
18
+ # user = User.new name: "Иоанн"
19
+ # user.name
20
+ # # => #<ActiveSupport::Multibyte::Chars @wrapped_string = "Иоанн">
17
21
  module Models
18
22
 
19
23
  private
20
24
 
25
+ # @!method attr_coerced(*names, options)
26
+ # Coerced the attribute(s) with given type.
27
+ # @example (see Hexx::Models)
28
+ # @param [Array<Symbol, String>] names The list of attributes to be coerced.
29
+ # @param [Hash] options The coersion options.
30
+ # @option options [Class] :type The class for coersion.
21
31
  def attr_coerced(*names, type:)
22
32
  names.each { |name| coercer.new(self, name, type).coerce }
23
33
  end
24
34
 
35
+ # @api hide
36
+ # Applied coercer for attributes.
37
+ # @note The method is reloaded in the 'hexx-active_record' gem to allow
38
+ # coersion of ActiveRecord models' attributes.
25
39
  def coercer
26
40
  @coercer ||= BaseCoercer
27
41
  end
@@ -1,10 +1,18 @@
1
1
  module Hexx
2
2
  module Models
3
3
 
4
- # Coerces PORO attribute's getter and setter with given type.
4
+ # @api hide
5
+ # Coerces class attribute getter and setter with given type.
5
6
  class BaseCoercer < Struct.new(:klass, :name, :type)
6
7
 
7
8
  # Coerces class attribute's getter and setter.
9
+ #
10
+ # @example
11
+ # BaseCoercer.new SomeModel, :name, StrippedString
12
+ # BaseCoercer.coerce
13
+ #
14
+ # model = SomeModel.new name: "string"
15
+ # model.name.class # => StrippedString
8
16
  def coerce
9
17
  coerce_setter
10
18
  coerce_getter
data/lib/hexx/service.rb CHANGED
@@ -1,23 +1,271 @@
1
- # Service modules the +Service+ class depends on.
2
- def service_modules
3
- Dir[File.expand_path("../service/*.rb", __FILE__)]
4
- end
5
-
6
- service_modules.each { |file| require file }
1
+ # encoding: utf-8
7
2
 
8
3
  module Hexx
9
4
 
10
- # Base class for service objects.
5
+ # @abstract
6
+ # The base class for service objects.
7
+ #
8
+ # @example
9
+ # require "hexx"
10
+ # class GetItem < Hexx::Service
11
+ # allow_params :name
12
+ # def run
13
+ # publish :found, item = Item.where(name: name).first
14
+ # end
15
+ # end
16
+ #
17
+ # service = GetItem.new name: name
18
+ # service.subscribe listener, prefix: :on
19
+ # service.run
20
+ # # => This will call the listener's method #on_found(item).
11
21
  class Service
12
- include Messages, Parameters, Transactions, Validations, Callbacks
22
+ include Wisper::Publisher
23
+ include ActiveModel::Validations
24
+ include Parameters
13
25
 
14
- # Runs the service object.
26
+ # @!scope class
27
+ # @!method new(options = {})
28
+ # Constructs the service object.
29
+ #
30
+ # @example
31
+ # service = Hexx::Service.new name: name
32
+ #
33
+ # @param [Hash] options The options to be assigned to the {#params}.
34
+ # @return [Hexx::Service] The service object.
35
+
36
+ # @!scope class
37
+ # @!visibility private
38
+ # @!method allow_params(*params)
39
+ # Whitelists {#params} and declares a parameter for corresponding keys.
40
+ #
41
+ # @example (see Hexx::Service#params)
42
+ #
43
+ # @example Defines corresponding readonly attributes.
44
+ # class GetItem < Hexx::Service
45
+ # allow_params :name
46
+ # end
15
47
  #
16
- # The method must be defined in a specific service class,
17
- # inherited from <tt>Hexx::Service</tt>.
48
+ # service = GetItem.new name: "Олег", family: "Рюрикович"
49
+ # service.name # => "Олег"
50
+ #
51
+ # @param [Array<Symbol, String>] params The list of allowed keys.
52
+
53
+ # @!scope class
54
+ # @!method validates(attribute, options)
55
+ # Adds a standard validation for the attribute.
56
+ # @note The method is defined in the {ActiveModel::Validations} module.
57
+
58
+ # @!scope class
59
+ # @!method validate(method, options)
60
+ # Adds a custom validation (calls given method).
61
+ # @note The method is defined in the {ActiveModel::Validations} module.
62
+
63
+ # @!attribute params [r] The list of service object parameters.
64
+ # The attribute is assigned via the {.new} method options.
65
+ # The keys should be explicitly declared by the {.allow_params} helper.
66
+ #
67
+ # @example Only whitelisted params are being assigned.
68
+ # class GetItem < Hexx::Service
69
+ # allow_params :name
70
+ # end
71
+ #
72
+ # service = GetItem.new name: "Олег", family: "Рюрикович"
73
+ # service.params # => { "name" => "Олег" }
74
+
75
+ # @!method subscribe(listener, options = {})
76
+ # Subscribes the listener to service object's notifications.
77
+ # The <tt>:prefix</tt> sets the prefix to be added to a notification name
78
+ # to provide a corresponding listener method, that should be called by
79
+ # the publisher.
18
80
  #
81
+ # @example (see Hexx::Service)
82
+ # @param [Object] listener The object that should receive notifications from
83
+ # the service object.
84
+ # @param [Hash] options The list of the subscription options.
85
+ # @option options [Symbol] :prefix The prefix for the listener's callbacks.
86
+
87
+ # @abstract
88
+ # Runs the service object.
89
+ # @note The method must be reloaded by a specific service class,
90
+ # inherited from <tt>Hexx::Service</tt>.
91
+ # @raise [NotImplementedError] if a child class hasn't redefined the method.
19
92
  def run
20
93
  fail NotImplementedError.new "#{ self.class.name }#run not implemented."
21
94
  end
95
+
96
+ # Makes private methods with given prefix public.
97
+ #
98
+ # @example Opens private methods.
99
+ # def GetItem < Hexx::Service
100
+ # private
101
+ # def on_success
102
+ # publish :success
103
+ # end
104
+ # end
105
+ #
106
+ # service = GetItem.new
107
+ # service.respond_to? :on_success
108
+ # # => false
109
+ #
110
+ # service_with_callbacks = service.with_callbacks
111
+ # service_with_callbacks.respond_to? :on_success
112
+ # # => true
113
+ #
114
+ # @return [Hexx::Service::WithCallbacks<Hexx::Service>]
115
+ # The decorator that allows access to the service's private methods.
116
+ def with_callbacks(prefix: nil)
117
+ WithCallbacks.new(self, prefix: prefix)
118
+ end
119
+
120
+ private
121
+
122
+ # The helper runs another service object and subscribes +self+ for the
123
+ # service object's notifications.
124
+ #
125
+ # @example
126
+ # class AddItem < Hexx::Service
127
+ # allow_params :id
128
+ #
129
+ # # Runs a service for finding an item.
130
+ # # Service notifications to be received with a prefix :on_item
131
+ # def find_item
132
+ # run_service GetItem, :on_item, id: params["id"]
133
+ # end
134
+ #
135
+ # private
136
+ #
137
+ # attr_reader :item
138
+ #
139
+ # # Receives GetItem's :found notification
140
+ # def on_item_found(item)
141
+ # @item = item
142
+ # publish :found, item
143
+ # end
144
+ #
145
+ # # Receives GetItem's :not_found notification
146
+ # def on_item_not_found(*)
147
+ # # ... do some stuff here
148
+ # end
149
+ # end
150
+ #
151
+ # @param [Class] service_class The service class to instantiate and run
152
+ # a service object.
153
+ # @param [Symbol] prefix The prefix for callbacks to receive the service
154
+ # object's notifications.
155
+ # @param [Hash] options ({}) The options for the service object initializer.
156
+ # @raise [TypeError] when the service_class is not a <tt>Hexx::Service</tt>.
157
+ def run_service(service_class, prefix, options = {})
158
+ fail TypeError unless service_class.ancestors.include? Hexx::Service
159
+ service = service_class.new(options)
160
+ service.subscribe with_callbacks, prefix: prefix
161
+ service.run
162
+ end
163
+
164
+ # @!method escape
165
+ #
166
+ # The method rescues block runtime errors and publishes the :error
167
+ # notification.
168
+ #
169
+ # @example
170
+ # class GetItem < Hexx::Service
171
+ # def run
172
+ # escape do
173
+ # errors.add :base, :error
174
+ # fail Invalid.new self
175
+ # end
176
+ # end
177
+ # end
178
+ #
179
+ # service = GetItem.new
180
+ # service.subscribe listener
181
+ # service.run
182
+ # # => the listener will be sent the error(messages).
183
+ #
184
+ # @yield the block.
185
+ # @return the value returned by the block.
186
+ def escape
187
+ yield
188
+ rescue Service::Invalid => err
189
+ publish :error, Message.from(err.service)
190
+ rescue => err
191
+ publish :error, [Message.new(type: "error", text: err.message)]
192
+ end
193
+
194
+ # Translates given key in current service's scope.
195
+ #
196
+ # @note The method uses I18n.t library method.
197
+ #
198
+ # @example Returns a translation if the first argument is a symbol.
199
+ # class Test < Hexx::Service
200
+ # end
201
+ # service = Test.new
202
+ # service.t :name
203
+ # # => "translation not found: en.activemodel.messages.models.test.name"
204
+ #
205
+ # @example Returns the string argument.
206
+ # service = Hexx::Service.new
207
+ # service.t "name"
208
+ # # => "name"
209
+ #
210
+ # @param [Symbol, String] text The text to be translated.
211
+ # @param [Hash] options The translation options.
212
+ # @return [String] The translation.
213
+ def t(text, options = {})
214
+ return text unless text.is_a? Symbol
215
+ scope = %w(activemodel messages models) << self.class.name.underscore
216
+ I18n.t text, options.merge(scope: scope)
217
+ end
218
+
219
+ # The array of service messages, added by the {#add_message} helper.
220
+ #
221
+ # @example
222
+ # class Test < Hexx::Service
223
+ # def run
224
+ # add_message "info", :ok
225
+ # messages
226
+ # end
227
+ # end
228
+ # result = Test.new.run.first
229
+ # result.type
230
+ # # => "info"
231
+ # result.text
232
+ # # => "translation not found: en.activemodel.messages.models.test.ok"
233
+ #
234
+ # @return [Array<Hexx::Service::Message>] The array of message objects.
235
+ def messages
236
+ @messages ||= []
237
+ end
238
+
239
+ # Adds the translated message to the {#messages} array.
240
+ # @example (see Service#messages)
241
+ # @param [String] type The type of the message ("error", "info", "success")
242
+ # @param [String, Symbol] text The text of the message. The symbol will
243
+ # be translated using the {#t} method.
244
+ # @param [Hash] options The translation options.
245
+ # @return The updated {#messages} array.
246
+ def add_message(type, text, options = {})
247
+ messages << Message.new(type: type, text: t(text, options))
248
+ end
249
+
250
+ # Runs validations and fails if the service is invalid.
251
+ #
252
+ # @example (see Hexx::Service::Invalid)
253
+ #
254
+ # @example Safe usage (recommended) with the {#escape} wrapper.
255
+ # service GetItem < Hexx::Service
256
+ # allow_params :uuid
257
+ # validates :uuid, presence: true
258
+ # def run
259
+ # escape { validate! }
260
+ # end
261
+ # end
262
+ #
263
+ # service = GetItem.new
264
+ # service.run # => publishes :error notification
265
+ #
266
+ # @raise [Hexx::Service::Invalid] when the service object isn't valid.
267
+ def validate!
268
+ fail Invalid.new(self) unless valid?
269
+ end
22
270
  end
23
271
  end
@@ -3,43 +3,63 @@ require_relative "message"
3
3
  module Hexx
4
4
  class Service
5
5
 
6
- # Exception to be raised by invalid services.
6
+ # The exception to be raised by invalid services.
7
+ #
8
+ # @example
9
+ # service GetItem < Hexx::Service
10
+ # allow_params :uuid
11
+ # validates :uuid, presence: true
12
+ # def run
13
+ # validate!
14
+ # end
15
+ # end
16
+ #
17
+ # service = GetItem.new
18
+ # service.run # => fails with the {Hexx::Service::Invalid}.
7
19
  class Invalid < ::RuntimeError
8
20
 
9
- # An invalid service object with error messages.
21
+ # @!attribute [r] service
22
+ # @return [Hexx::Service] The invalid service object.
10
23
  attr_reader :service
11
24
 
25
+ # @!scope class
26
+ # @!method new(service)
12
27
  # Initializes the exception.
13
28
  #
14
29
  # @example
15
- # fail Hexx::Service::Invalid.new(service)
16
- #
17
- # Params:
18
- # +service+:: a Hexx::Service object containing error messages.
30
+ # fail Hexx::Service::Invalid.new service
19
31
  #
32
+ # @param [Hexx::Service] service The invalid service.
33
+ # @raise [ArgumentError] if the argument is not a service object.
34
+ # @return [Hexx::Service::Invalid] The exception.
35
+
36
+ # @api hide
20
37
  def initialize(service)
21
38
  @service = service
22
39
  fail ArgumentError unless self.service.is_a? Hexx::Service
23
40
  end
24
41
 
42
+ # @!attribute [r] message
25
43
  # Returns a default text message for the exception.
26
44
  #
27
45
  # @example
28
- # error = Hexx::Service::Invalid.new service
29
- # error.message # => "Service invalid: #<Hexx::Service... >"
46
+ # error = Hexx::Service::Invalid.new service
47
+ # error.message # => "Service invalid: #<Hexx::Service... >"
30
48
  #
49
+ # @return [String] The message.
31
50
  def message
32
51
  "Service invalid: #{ service.inspect }"
33
52
  end
34
53
 
35
- # Returns a list of <tt>Hexx::Service::Message</tt> messages of
36
- # +error+ type.
54
+ # @!attribute [r] messages
55
+ # Returns a list of error messages from the service.
37
56
  #
38
57
  # @example
39
- # error = Hexx::Service::Invalid.new service
40
- # error.messages # => [#<Hexx::Message... >, ...]
41
- # error.messages.first.type # => "error"
58
+ # error = Hexx::Service::Invalid.new service
59
+ # error.messages # => [#<Hexx::Message... >]
60
+ # error.messages.first.type # => "error"
42
61
  #
62
+ # @return [Array<Hexx::Service::Message>] The list of messages.
43
63
  def messages
44
64
  service.errors.values.flatten.map do |text|
45
65
  Message.new type: "error", text: text
@@ -5,18 +5,39 @@ module Hexx
5
5
  class Message
6
6
  include Comparable
7
7
 
8
- # A stringified type and a text of the message.
9
- attr_reader :type, :text
8
+ # @!attribute [r] type
9
+ # The type of the message
10
+ #
11
+ # @example
12
+ # message = Message.new type: :error, text: "message"
13
+ # message.type = "error"
14
+ #
15
+ # @return [String] The type of the message.
16
+ attr_reader :type
10
17
 
11
- # Initializes the message.
18
+ # @!attribute [r] text
19
+ # The text of the message
12
20
  #
13
21
  # @example
14
- # Message.new type: "success", text: "Object created!"
22
+ # message = Message.new type: :error, text: "message"
23
+ # message.text = "message"
24
+ #
25
+ # @return [String] The text of the message.
26
+ attr_reader :text
27
+
28
+ # @!scope class
29
+ # @!method new(options)
30
+ # Constructs the message with type and text.
15
31
  #
16
- # Params:
17
- # <tt>:type</tt>:: string/symbolic message type (:error, :info, :success).
18
- # <tt>:text</tt>:: message text.
32
+ # @example
33
+ # Message.new type: "success", text: "Object created."
19
34
  #
35
+ # @param [Hash] options The list of the message attributes.
36
+ # @option options [String, Symbol] :type The type of the message.
37
+ # @option options [String, Symbol] :text The text of the message.
38
+ # @return [Hexx::Service::Message] The message.
39
+
40
+ # @api hide
20
41
  def initialize(type:, text:)
21
42
  @type, @text = type.to_s, text.to_s
22
43
  end
@@ -24,11 +45,11 @@ module Hexx
24
45
  # Extracts error messages from ActiveRecord or ActiveModel objects.
25
46
  #
26
47
  # @example
27
- # Message.from(object) # => [#<Hexx::Service::Message ...>]
28
- #
29
- # Params:
30
- # +object+:: an object to extract messages from.
48
+ # Message.from(object)
49
+ # # => [#<Hexx::Service::Message ...>]
31
50
  #
51
+ # @param [ActiveRecord::Base] object The object to extract messages from.
52
+ # @return [Array<Hexx::Service::Message>] The array of extracted messages.
32
53
  def self.from(object)
33
54
  object.errors.values.flatten.map do |text|
34
55
  new type: "error", text: text
@@ -38,18 +59,17 @@ module Hexx
38
59
  # Distinguishes two messages by type and text.
39
60
  #
40
61
  # @example
41
- # a = Message.new(type: "a", text: "a")
42
- # b = Message.new(type: "a", text: "a")
43
- # c = Message.new(type: "b", text: "a")
44
- # d = Message.new(type: "a", text: "b")
62
+ # a = Message.new(type: "a", text: "a")
63
+ # b = Message.new(type: "a", text: "a")
64
+ # c = Message.new(type: "b", text: "a")
65
+ # d = Message.new(type: "a", text: "b")
45
66
  #
46
- # a == b # => true
47
- # a == c # => false
48
- # a == d # => false
49
- #
50
- # Params:
51
- # +other+:: other message to distinguish from.
67
+ # a == b # => true
68
+ # a == c # => false
69
+ # a == d # => false
52
70
  #
71
+ # @param [Object] other The object for the comparison.
72
+ # @return [Boolean] The result of the comparison.
53
73
  def ==(other)
54
74
  return false unless other.is_a? self.class
55
75
  [type, text] == [other.type, other.text]
@@ -58,13 +78,14 @@ module Hexx
58
78
  # Compares messages by type and text.
59
79
  #
60
80
  # @example
61
- # ab = Message.new(type: "a", text: "b")
62
- # ba = Message.new(type: "b", text: "a")
63
- # ab < ba # => true
64
- #
65
- # Params:
66
- # +other+:: other message to compare with.
81
+ # ab = Message.new(type: "a", text: "b")
82
+ # ba = Message.new(type: "b", text: "a")
83
+ # ab < ba # => true
67
84
  #
85
+ # @param [Object] other The object for the comparison.
86
+ # @return [-1, 0, 1] The result of the comparison if the argument is
87
+ # comparable with the message.
88
+ # @return [nil] if the result is incomparable with the message.
68
89
  def <=>(other)
69
90
  fail ArgumentError unless other.is_a? self.class
70
91
  [type, text] <=> [other.type, other.text]
@@ -1,6 +1,7 @@
1
1
  module Hexx
2
2
  class Service
3
3
 
4
+ # @api hide
4
5
  # Contains methods to declare parameters and set their values.
5
6
  module Parameters
6
7
  extend ActiveSupport::Concern
@@ -8,7 +9,19 @@ module Hexx
8
9
  # Methods to declare and allow services params.
9
10
  module ClassMethods
10
11
 
11
- # A list of allowed params for the service objects.
12
+ # @!attribute [r] params
13
+ # The list of allowed instance parameters. The parameters are added
14
+ # to the list by the {.allow_params} method.
15
+ #
16
+ # @example
17
+ # class Service
18
+ # include Parameters
19
+ # allow_params :name
20
+ # end
21
+ #
22
+ # Service.params # => "name"
23
+ #
24
+ # @return [Array<String>] The list of allowed instance parameters.
12
25
  def params
13
26
  @params ||= []
14
27
  end
@@ -16,26 +29,32 @@ module Hexx
16
29
  private
17
30
 
18
31
  # Sets a list of allowed parameters for the class constructor and
19
- # adds corresponding attributes to instance methods.
32
+ # defines the corresponding instance attributes.
33
+ #
34
+ # @example (see Hexx::Service::Parameters.params)
35
+ # @param [Array<Symbol, String>] keys The list of allowed parameters.
20
36
  def allow_params(*keys)
21
37
  @params = keys.map(&:to_s)
22
38
  attr_accessor(*params)
23
39
  end
24
40
  end
25
41
 
26
- # Initializes a service object with a hash of parameters.
42
+ # @!method new(params)
43
+ # Constructs a service object with a hash of parameters.
27
44
  #
28
45
  # @example
29
- # Service.new(hash = {})
46
+ # Service.new name: "name"
30
47
  #
31
- # Params:
32
- # +hash+:: a hash of parameters for the service.
33
- #
34
- def initialize(hash = {})
35
- extract_params_from hash
36
- params.each { |key, value| public_send "#{ key }=", value }
48
+ # @param [Hash] params ({}) The parameters of the service object.
49
+ def initialize(params = {})
50
+ extract_params_from params
51
+ @params.each { |key, value| public_send "#{ key }=", value }
37
52
  end
38
53
 
54
+ # @!attribute [r] params
55
+ # The parameters of the service objects. The parameters are set on
56
+ # the object initialization, then are stringified and whitelisted.
57
+ # @return [Hash] the service object parameters.
39
58
  attr_reader :params
40
59
 
41
60
  private
@@ -0,0 +1,107 @@
1
+ module Hexx
2
+ class Service
3
+
4
+ # Wraps the service object and grants access to its private methods.
5
+ #
6
+ # @example
7
+ # class GetItem < Hexx::Service
8
+ # private
9
+ # attr_reader :on_something, :something
10
+ # end
11
+ #
12
+ # service = GetItem.new
13
+ # service.respond_to? :on_something # => false
14
+ # service.respond_to? :something # => false
15
+ #
16
+ # wrapper = Hexx::Service::WithCallbacks.new service, prefix: :on
17
+ # wrapper.respond_to? :on_something # => true
18
+ # wrapper.respond_to? :something # => false
19
+ class WithCallbacks
20
+
21
+ # @!scope class
22
+ # @!method new(object, options)
23
+ # Initializes the decorator of given object.
24
+ #
25
+ # @example (see Hexx::Service::WithCallbacks)
26
+ #
27
+ # @param [Hexx::Service] object The service object to be decorated.
28
+ # @param [Hash] options The list of wrapper options.
29
+ # @option options [Symbol] :prefix (nil) The prefix for private methods
30
+ # to be accessible.
31
+
32
+ # @api hide
33
+ def initialize(object, prefix: nil)
34
+ @object = object
35
+ @prefix = Regexp.new(prefix ? "^#{ prefix }_" : "")
36
+ end
37
+
38
+ # @api hide
39
+ # Redefines the +respond_to?+ to allow access to object's methods.
40
+ #
41
+ # @example
42
+ # service = WithCallbacks(some_service, prefix: :on)
43
+ # service.respond_to? :on_something # => true
44
+ #
45
+ # @param [Symbol] method The name of the method to check access to.
46
+ def respond_to?(method, *)
47
+ object.respond_to?(method) || valid_callback?(method) || super
48
+ end
49
+
50
+ # Compares the object with another service object.
51
+ # The wrapper is equal to another wrapper for the same object and prefix.
52
+ #
53
+ # @example Wrappers are equal if they have the same objects and prefixes
54
+ # service = Hexx::Service.new
55
+ # a = Hexx::Service::WithCallbacks.new service, prefix: :on
56
+ # b = Hexx::Service::WithCallbacks.new service, prefix: :on
57
+ # a == b # => true
58
+ #
59
+ # @example Wrappers are different if they have different prefixes
60
+ # service = Hexx::Service.new
61
+ # a = Hexx::Service::WithCallbacks.new service, prefix: :on
62
+ # b = Hexx::Service::WithCallbacks.new service, prefix: :when
63
+ # a == b # => false
64
+ #
65
+ # @example Wrappers are different if they have different objects
66
+ # a = Hexx::Service::WithCallbacks.new Hexx::Service.new, prefix: :on
67
+ # b = Hexx::Service::WithCallbacks.new Hexx::Service.new, prefix: :on
68
+ # a == b # => false
69
+ #
70
+ # @example A wrapper differs from any non-wrapper
71
+ # service = Hexx::Service.new
72
+ # a = Hexx::Service::WithCallbacks.new service, prefix: :on
73
+ # a == service # => false
74
+ #
75
+ # @param [Object] other The other object to compare the wrapper to.
76
+ # @return [Boolean].
77
+ def ==(other)
78
+ return false unless other.is_a?(Service) || other.is_a?(self.class)
79
+ value == other.value
80
+ end
81
+
82
+ protected
83
+
84
+ # @api hide
85
+ # The value to compare wrappers by.
86
+ # @return [String] value.
87
+ def value
88
+ object.inspect + prefix.inspect
89
+ end
90
+
91
+ private
92
+
93
+ # @api hide
94
+ attr_reader :object, :prefix
95
+
96
+ # @api hide
97
+ def method_missing(method, *args, &block)
98
+ valid_callback?(method) ? object.send(method, *args, &block) : super
99
+ end
100
+
101
+ # @api hide
102
+ def valid_callback?(method)
103
+ method.to_s[prefix] && object.respond_to?(method, include_all: true)
104
+ end
105
+ end
106
+ end
107
+ end
data/lib/hexx/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  module Hexx
3
3
 
4
4
  # Current release.
5
- VERSION = "2.2.0"
5
+ VERSION = "3.0.0"
6
6
  end
@@ -5,8 +5,8 @@ module Hexx
5
5
 
6
6
  around :each do |example|
7
7
  class Test < Service
8
- private
9
8
  attr_reader :on_something, :something
9
+ private :on_something, :something
10
10
  end
11
11
  example.run
12
12
  Hexx.send :remove_const, :Test
@@ -77,19 +77,6 @@ module Hexx
77
77
  end
78
78
  end
79
79
 
80
- describe "#validate!" do
81
-
82
- it "passes when object is valid" do
83
- expect { subject.validate! }.not_to raise_error
84
- end
85
-
86
- it "fails when object is invalid" do
87
- allow(subject).to receive(:valid?).and_return false
88
- expect { subject.validate! }
89
- .to raise_error { Service::Invalid.new(subject) }
90
- end
91
- end
92
-
93
80
  describe "#run" do
94
81
 
95
82
  it "fails with NotImplementedError" do
@@ -106,7 +93,8 @@ module Hexx
106
93
 
107
94
  describe "private" do
108
95
 
109
- subject { Service.new.with_callbacks }
96
+ let!(:service) { Service.new }
97
+ subject { service.with_callbacks }
110
98
 
111
99
  describe ".allow_params" do
112
100
 
@@ -121,6 +109,19 @@ module Hexx
121
109
  end
122
110
  end
123
111
 
112
+ describe "#validate!" do
113
+
114
+ it "passes when service is valid" do
115
+ expect { subject.validate! }.not_to raise_error
116
+ end
117
+
118
+ it "fails when service is invalid" do
119
+ allow(service).to receive(:valid?).and_return false
120
+ expect { subject.validate! }
121
+ .to raise_error { Service::Invalid.new(service) }
122
+ end
123
+ end
124
+
124
125
  describe "#t" do
125
126
 
126
127
  let(:scope) { %w(activemodel messages models hexx/service) }
@@ -158,40 +159,22 @@ module Hexx
158
159
  end
159
160
  end
160
161
 
161
- describe "#transaction" do
162
+ describe "#escape" do
162
163
 
163
164
  let(:listener) { double "listener" }
164
165
  before { subject.subscribe listener }
165
166
 
166
167
  it "yields a block" do
167
168
  value = "Hello!"
168
- result = subject.transaction { value }
169
+ result = subject.escape { value }
169
170
  expect(result).to eq value
170
171
  end
171
172
 
172
- context "when a service invalid" do
173
-
174
- before do
175
- allow_any_instance_of(Service).to receive(:validate!) { fail }
176
- end
177
-
178
- it "doesn't yield a block" do
179
- value = "Hello!"
180
- result = subject.transaction { value }
181
- expect(result).not_to eq value
182
- end
183
-
184
- it "publishes an :error" do
185
- expect(listener).to receive(:error)
186
- subject.transaction
187
- end
188
- end
189
-
190
173
  context "when a block raises Service::Invalid error" do
191
174
 
192
175
  before { subject.errors.add :base, :text }
193
176
  let(:run) do
194
- subject.transaction { fail Service::Invalid.new(subject) }
177
+ subject.escape { fail Service::Invalid.new(subject) }
195
178
  end
196
179
 
197
180
  it "rescues" do
@@ -206,7 +189,7 @@ module Hexx
206
189
 
207
190
  context "when a block raises StandardError" do
208
191
 
209
- let(:run) { subject.transaction { fail "text" } }
192
+ let(:run) { subject.escape { fail "text" } }
210
193
 
211
194
  it "rescues" do
212
195
  expect { run }.not_to raise_error
@@ -221,6 +204,37 @@ module Hexx
221
204
  end
222
205
  end
223
206
  end
207
+
208
+ describe "#run_service" do
209
+
210
+ let!(:other) { double "other", subscribe: nil, run: nil }
211
+ before { allow(Test).to receive(:new).and_return other }
212
+
213
+ it "if wrong class given it fails with TypeError" do
214
+ expect { subject.run_service String, :on_string }
215
+ .to raise_error { TypeError }
216
+ end
217
+
218
+ it "creates a service object" do
219
+ options = { "name" => "some name" }
220
+ expect(Test).to receive(:new).with options
221
+ subject.run_service Test, :on_service, options
222
+ end
223
+
224
+ it "subscribes self for the service notifications" do
225
+ expect(other).to receive(:subscribe) do |listener, params|
226
+ expect(listener).to eq subject.with_callbacks
227
+ expect(params).to eq(prefix: :on_service)
228
+ end
229
+ subject.run_service Test, :on_service
230
+ end
231
+
232
+ it "runs the service object after subscriptions" do
233
+ expect(other).to receive(:subscribe).ordered
234
+ expect(other).to receive(:run).ordered
235
+ subject.run_service Test, :on_service
236
+ end
237
+ end
224
238
  end
225
239
  end
226
240
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexx
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0
4
+ version: 3.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kozin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-11-26 00:00:00.000000000 Z
11
+ date: 2014-12-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '0.23'
125
+ - !ruby/object:Gem::Dependency
126
+ name: guard-rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '4.3'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '4.3'
125
139
  description: Defines domain service object and model attributes coercion.
126
140
  email: andrew.kozin@gmail.com
127
141
  executables: []
@@ -138,13 +152,10 @@ files:
138
152
  - lib/hexx/models.rb
139
153
  - lib/hexx/models/base_coercer.rb
140
154
  - lib/hexx/service.rb
141
- - lib/hexx/service/callbacks.rb
142
155
  - lib/hexx/service/invalid.rb
143
156
  - lib/hexx/service/message.rb
144
- - lib/hexx/service/messages.rb
145
157
  - lib/hexx/service/parameters.rb
146
- - lib/hexx/service/transactions.rb
147
- - lib/hexx/service/validations.rb
158
+ - lib/hexx/service/with_callbacks.rb
148
159
  - lib/hexx/version.rb
149
160
  - spec/hexx/models_spec.rb
150
161
  - spec/hexx/service/invalid_spec.rb
@@ -1,77 +0,0 @@
1
- module Hexx
2
- class Service
3
-
4
- # Allows to expose private callbacks to notifiers.
5
- module Callbacks
6
-
7
- # Grants access to object methods with given prefix.
8
- #
9
- # @example
10
- # WithCallbacks.new(object, prefix: :on)
11
- #
12
- # Params:
13
- # +object+:: (required) ...a service object to access to.
14
- # <tt>prefix:</tt>:: (optional, symbol or string, default: +nil+)
15
- # ...prefix for private methods to be accessible.
16
- #
17
- class WithCallbacks
18
-
19
- def initialize(object, prefix: nil)
20
- @object = object
21
- @prefix = Regexp.new(prefix ? "^#{ prefix }_" : "")
22
- end
23
-
24
- # Redefines the +respond_to?+ to allow access to object's methods.
25
- #
26
- # @example
27
- # service = WithCallbacks(some_service, prefix: :on)
28
- # service.respond_to? :on_something # => true
29
- #
30
- # Params:
31
- # +method+:: a method name to check access to.
32
- #
33
- def respond_to?(method, *)
34
- object.respond_to?(method) || valid_callback?(method) || super
35
- end
36
-
37
- private
38
-
39
- def method_missing(method, *args, &block)
40
- valid_callback?(method) ? object.send(method, *args, &block) : super
41
- end
42
-
43
- def valid_callback?(method)
44
- method.to_s[prefix] && object.respond_to?(method, include_all: true)
45
- end
46
-
47
- attr_reader :object, :prefix
48
- end
49
-
50
- # Makes private methods with given prefix public.
51
- #
52
- # @example
53
- # class MyService < Hexx::Service
54
- # def call
55
- # # ...
56
- # other_service = OtherService.new
57
- # other_service.subscribe(self.with_callbacks(prefix: :on))
58
- # end
59
- #
60
- # private
61
- #
62
- # # private, but is made publicly accessible
63
- # def on_success
64
- # # ...
65
- # end
66
- # end
67
- #
68
- # Params:
69
- # <tt>prefix:</tt>:: (optional, symbol or string, default: +nil+)
70
- # ...prefix of private methods to be accessible.
71
- #
72
- def with_callbacks(prefix: nil)
73
- WithCallbacks.new(self, prefix: prefix)
74
- end
75
- end
76
- end
77
- end
@@ -1,34 +0,0 @@
1
- require_relative "message"
2
-
3
- module Hexx
4
- class Service
5
-
6
- # Declares methods for creation messages by a service:
7
- #
8
- # <tt>t(text, options = {})</tt>:: translates text in the service's scope.
9
- # <tt>messages</tt>:: returns an array of service's messages.
10
- # <tt>add_message(type, text, options = {})</tt>:: adds a message to array.
11
- #
12
- module Messages
13
-
14
- private
15
-
16
- # Translates given key in current service's scope.
17
- def t(key, options = {})
18
- return key unless key.is_a? Symbol
19
- scope = %w(activemodel messages models) << self.class.name.underscore
20
- I18n.t key, options.merge(scope: scope)
21
- end
22
-
23
- # Returns the array of service's messages.
24
- def messages
25
- @messages ||= []
26
- end
27
-
28
- # Adds the translated message to the messages array.
29
- def add_message(type, text, options = {})
30
- messages << Message.new(type: type, text: t(text, options))
31
- end
32
- end
33
- end
34
- end
@@ -1,30 +0,0 @@
1
- require_relative "invalid"
2
-
3
- module Hexx
4
- class Service
5
-
6
- # Declares the <tt>##transaction</tt> private instance method.
7
- module Transactions
8
- include Wisper::Publisher
9
-
10
- private
11
-
12
- # Yields the block and publishes :error notification with
13
- # error messages in case of any standard exception being raised.
14
- def transaction
15
- run_and_rescue do
16
- validate!
17
- yield
18
- end
19
- end
20
-
21
- def run_and_rescue
22
- yield
23
- rescue Service::Invalid => err
24
- publish :error, Message.from(err.service)
25
- rescue => err
26
- publish :error, [Message.new(type: "error", text: err.message)]
27
- end
28
- end
29
- end
30
- end
@@ -1,24 +0,0 @@
1
- require_relative "invalid"
2
-
3
- module Hexx
4
- class Service
5
-
6
- # Defines methods for active record validation.
7
- module Validations
8
-
9
- # Includes the <tt>ActiveModel::Validations</tt> module to the service.
10
- #
11
- # Params
12
- # +klass+:: a class that includes the module.
13
- #
14
- def self.included(klass)
15
- klass.include ActiveModel::Validations
16
- end
17
-
18
- # Runs validations and raises <tt>Service::Error</tt> if validations fail.
19
- def validate!
20
- fail Invalid.new(self) unless valid?
21
- end
22
- end
23
- end
24
- end