hexx 2.2.0 → 3.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.
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