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 +4 -4
- data/.rubocop.yml +3 -0
- data/README.rdoc +1 -1
- data/Rakefile +2 -1
- data/lib/hexx.rb +12 -5
- data/lib/hexx/models.rb +16 -2
- data/lib/hexx/models/base_coercer.rb +9 -1
- data/lib/hexx/service.rb +259 -11
- data/lib/hexx/service/invalid.rb +33 -13
- data/lib/hexx/service/message.rb +48 -27
- data/lib/hexx/service/parameters.rb +29 -10
- data/lib/hexx/service/with_callbacks.rb +107 -0
- data/lib/hexx/version.rb +1 -1
- data/spec/hexx/service_spec.rb +51 -37
- metadata +17 -6
- data/lib/hexx/service/callbacks.rb +0 -77
- data/lib/hexx/service/messages.rb +0 -34
- data/lib/hexx/service/transactions.rb +0 -30
- data/lib/hexx/service/validations.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fb67d554082720fe92957b12996f0a313b3b3ae8
|
4
|
+
data.tar.gz: 197971ca631d755c770543cf8393d173bff65ff3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2616e0e816e014aba36151690e5d5546ace6adcd2e1ec08bef86191526bf594576359ed0b3eace7ce4380eb629c00d675cd0bb1aef3e1ab5eb6918a6b8e018ef
|
7
|
+
data.tar.gz: b811fd9b51612ea91d71c75b0c87fedfe2146a10f78b4327953390fc7cd430dec07f9475e5d72b7aa5f00660b1309b81a859805fd37c7b8ead676f666e11c3f8
|
data/.rubocop.yml
CHANGED
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
|
-
|
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
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
|
-
#
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
|
13
|
+
# @api show
|
14
|
+
# Namespace for the gem.
|
15
|
+
module Hexx
|
16
|
+
end
|
data/lib/hexx/models.rb
CHANGED
@@ -1,8 +1,8 @@
|
|
1
|
-
|
1
|
+
# encoding: utf-8
|
2
2
|
|
3
3
|
module Hexx
|
4
4
|
|
5
|
-
# Declares the
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
22
|
+
include Wisper::Publisher
|
23
|
+
include ActiveModel::Validations
|
24
|
+
include Parameters
|
13
25
|
|
14
|
-
#
|
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
|
-
#
|
17
|
-
#
|
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
|
data/lib/hexx/service/invalid.rb
CHANGED
@@ -3,43 +3,63 @@ require_relative "message"
|
|
3
3
|
module Hexx
|
4
4
|
class Service
|
5
5
|
|
6
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
29
|
-
#
|
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
|
-
#
|
36
|
-
#
|
54
|
+
# @!attribute [r] messages
|
55
|
+
# Returns a list of error messages from the service.
|
37
56
|
#
|
38
57
|
# @example
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
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
|
data/lib/hexx/service/message.rb
CHANGED
@@ -5,18 +5,39 @@ module Hexx
|
|
5
5
|
class Message
|
6
6
|
include Comparable
|
7
7
|
|
8
|
-
#
|
9
|
-
|
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
|
-
#
|
18
|
+
# @!attribute [r] text
|
19
|
+
# The text of the message
|
12
20
|
#
|
13
21
|
# @example
|
14
|
-
#
|
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
|
-
#
|
17
|
-
#
|
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
|
-
#
|
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
|
-
#
|
42
|
-
#
|
43
|
-
#
|
44
|
-
#
|
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
|
-
#
|
47
|
-
#
|
48
|
-
#
|
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
|
-
#
|
62
|
-
#
|
63
|
-
#
|
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
|
-
#
|
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
|
-
#
|
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
|
-
#
|
42
|
+
# @!method new(params)
|
43
|
+
# Constructs a service object with a hash of parameters.
|
27
44
|
#
|
28
45
|
# @example
|
29
|
-
# Service.new
|
46
|
+
# Service.new name: "name"
|
30
47
|
#
|
31
|
-
#
|
32
|
-
|
33
|
-
|
34
|
-
|
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
data/spec/hexx/service_spec.rb
CHANGED
@@ -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
|
-
|
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 "#
|
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.
|
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.
|
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.
|
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:
|
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
|
+
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/
|
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
|