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 +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
|