service_objects 0.1.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.coveralls.yml +1 -0
- data/.metrics +1 -0
- data/.travis.yml +9 -1
- data/.yardopts +1 -1
- data/Gemfile +1 -1
- data/Guardfile +29 -8
- data/LICENSE +1 -1
- data/README.md +179 -342
- data/Rakefile +3 -3
- data/config/metrics/churn.yml +1 -1
- data/config/metrics/flay.yml +1 -1
- data/config/metrics/metric_fu.yml +1 -0
- data/config/metrics/rubocop.yml +4 -4
- data/config/metrics/simplecov.yml +1 -1
- data/lib/service_objects.rb +6 -9
- data/lib/service_objects/base.rb +190 -17
- data/lib/service_objects/listener.rb +21 -75
- data/lib/service_objects/message.rb +15 -96
- data/lib/service_objects/version.rb +1 -1
- data/service_objects.gemspec +11 -9
- data/spec/lib/base_spec.rb +247 -0
- data/spec/lib/listener_spec.rb +96 -0
- data/spec/lib/message_spec.rb +48 -0
- data/spec/spec_helper.rb +8 -6
- metadata +56 -93
- data/bin/service +0 -17
- data/config/metrics/pippi.yml +0 -3
- data/lib/service_objects/cli.rb +0 -117
- data/lib/service_objects/cli/locale.erb +0 -20
- data/lib/service_objects/cli/service.erb +0 -125
- data/lib/service_objects/cli/spec.erb +0 -87
- data/lib/service_objects/helpers.rb +0 -17
- data/lib/service_objects/helpers/dependable.rb +0 -63
- data/lib/service_objects/helpers/exceptions.rb +0 -64
- data/lib/service_objects/helpers/messages.rb +0 -95
- data/lib/service_objects/helpers/parameterized.rb +0 -85
- data/lib/service_objects/helpers/parameters.rb +0 -71
- data/lib/service_objects/helpers/validations.rb +0 -54
- data/lib/service_objects/invalid.rb +0 -55
- data/lib/service_objects/null.rb +0 -26
- data/lib/service_objects/parsers.rb +0 -13
- data/lib/service_objects/parsers/dependency.rb +0 -69
- data/lib/service_objects/parsers/notification.rb +0 -85
- data/lib/service_objects/rspec.rb +0 -75
- data/lib/service_objects/utils/normal_hash.rb +0 -34
- data/spec/tests/base_spec.rb +0 -43
- data/spec/tests/bin/service_spec.rb +0 -18
- data/spec/tests/cli_spec.rb +0 -179
- data/spec/tests/helpers/dependable_spec.rb +0 -77
- data/spec/tests/helpers/exceptions_spec.rb +0 -112
- data/spec/tests/helpers/messages_spec.rb +0 -64
- data/spec/tests/helpers/parameterized_spec.rb +0 -136
- data/spec/tests/helpers/parameters_spec.rb +0 -71
- data/spec/tests/helpers/validations_spec.rb +0 -60
- data/spec/tests/invalid_spec.rb +0 -69
- data/spec/tests/listener_spec.rb +0 -73
- data/spec/tests/message_spec.rb +0 -191
- data/spec/tests/null_spec.rb +0 -17
- data/spec/tests/parsers/dependency_spec.rb +0 -29
- data/spec/tests/parsers/notification_spec.rb +0 -84
- data/spec/tests/rspec_spec.rb +0 -86
- data/spec/tests/utils/normal_hash_spec.rb +0 -16
data/Rakefile
CHANGED
@@ -9,7 +9,7 @@ end
|
|
9
9
|
# Loads bundler tasks
|
10
10
|
Bundler::GemHelper.install_tasks
|
11
11
|
|
12
|
-
# Loads the Hexx::
|
12
|
+
# Loads the Hexx::RSpec and its tasks
|
13
13
|
begin
|
14
14
|
require "hexx-suit"
|
15
15
|
Hexx::Suit.install_tasks
|
@@ -18,5 +18,5 @@ rescue LoadError
|
|
18
18
|
Hexx::RSpec.install_tasks
|
19
19
|
end
|
20
20
|
|
21
|
-
# Sets the Hexx::RSpec task
|
22
|
-
task default: :
|
21
|
+
# Sets the Hexx::RSpec :test task to default
|
22
|
+
task default: "test:coverage:run"
|
data/config/metrics/churn.yml
CHANGED
data/config/metrics/flay.yml
CHANGED
@@ -1,2 +1,2 @@
|
|
1
1
|
---
|
2
|
-
minimum_score:
|
2
|
+
minimum_score: 5
|
data/config/metrics/rubocop.yml
CHANGED
@@ -3,6 +3,10 @@
|
|
3
3
|
# output: "tmp/rubocop"
|
4
4
|
# format: "html"
|
5
5
|
|
6
|
+
AllCops:
|
7
|
+
Exclude:
|
8
|
+
- '**/db/schema.rb'
|
9
|
+
|
6
10
|
Lint/HandleExceptions:
|
7
11
|
Exclude:
|
8
12
|
- '**/*_spec.rb'
|
@@ -46,10 +50,6 @@ Style/FileName:
|
|
46
50
|
Style/RaiseArgs:
|
47
51
|
EnforcedStyle: compact
|
48
52
|
|
49
|
-
Style/RescueModifier:
|
50
|
-
Exclude:
|
51
|
-
- '**/*_spec.rb'
|
52
|
-
|
53
53
|
Style/SingleLineMethods:
|
54
54
|
Exclude:
|
55
55
|
- '**/*_spec.rb'
|
data/lib/service_objects.rb
CHANGED
@@ -1,14 +1,11 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
-
|
4
|
-
|
3
|
+
require_relative "service_objects/version"
|
4
|
+
require_relative "service_objects/message"
|
5
|
+
require_relative "service_objects/listener"
|
6
|
+
require_relative "service_objects/base"
|
5
7
|
|
6
|
-
|
7
|
-
|
8
|
-
require_relative "service_objects/utils/normal_hash"
|
9
|
-
require_relative "service_objects/listener"
|
10
|
-
require_relative "service_objects/message"
|
11
|
-
require_relative "service_objects/invalid"
|
12
|
-
require_relative "service_objects/base"
|
8
|
+
# Namespace for the code of the 'service_objects' gem
|
9
|
+
module ServiceObjects
|
13
10
|
|
14
11
|
end # module ServiceObjects
|
data/lib/service_objects/base.rb
CHANGED
@@ -1,36 +1,209 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
+
|
3
|
+
require "attestor"
|
4
|
+
require "attr_coerced"
|
5
|
+
require "virtus"
|
2
6
|
require "wisper"
|
3
7
|
|
4
8
|
module ServiceObjects
|
5
9
|
|
6
|
-
require_relative "helpers"
|
7
|
-
|
8
10
|
# Base class for service objects
|
9
11
|
#
|
10
12
|
# @example
|
11
|
-
#
|
13
|
+
# class ChangeFoo < ServiceObjects::Base
|
14
|
+
#
|
15
|
+
# # Attributes definition
|
16
|
+
# attribute :id, Integer
|
17
|
+
# attribute :name
|
18
|
+
# attr_coerced :name, String
|
19
|
+
#
|
20
|
+
# # Object validation rules
|
21
|
+
# validate { invalid :blank_id unless id }
|
22
|
+
# validate { invalid :blank_name unless name }
|
23
|
+
#
|
24
|
+
# # Injectable dependencies
|
25
|
+
# dependency :get_foo, default: GetFoo
|
26
|
+
#
|
27
|
+
# private
|
28
|
+
#
|
29
|
+
# # The main algorithm
|
30
|
+
# def run!
|
31
|
+
# validate
|
32
|
+
# get_foo
|
33
|
+
# change_foo
|
34
|
+
# end
|
35
|
+
#
|
36
|
+
# attr_accessor :foo
|
12
37
|
#
|
13
|
-
#
|
14
|
-
#
|
15
|
-
#
|
16
|
-
#
|
38
|
+
# def get_foo
|
39
|
+
# run_service get_foo.new(id: id), Listener.new(self)
|
40
|
+
# end
|
41
|
+
#
|
42
|
+
# def change_foo
|
43
|
+
# foo.save! name: name
|
44
|
+
# publish :changed, foo, message(:success, :changed, name: name)
|
45
|
+
# rescue => error
|
46
|
+
# publish :error, message(:error, error.message)
|
47
|
+
# end
|
48
|
+
#
|
49
|
+
# class Listener < ServiceObjects::Listener
|
50
|
+
# def on_found(foo, *)
|
51
|
+
# self.foo = foo
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# def otherwise
|
55
|
+
# publish :not_found, message(:error, :not_found, id: id)
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# @see https://github.com/solnic/virtus
|
61
|
+
# Virtus API
|
62
|
+
# @see http://www.rubydoc.info/gems/wisper
|
63
|
+
# Wisper::Publisher API
|
64
|
+
# @see http://www.rubydoc.info/gems/attestor
|
65
|
+
# Attestor API
|
66
|
+
# @see http://www.rubydoc.info/gems/attr_coerced
|
67
|
+
# AttrCorerced API
|
17
68
|
class Base
|
18
69
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
include
|
23
|
-
include Helpers::Validations
|
70
|
+
# @!parse include Virtus::Model::Core
|
71
|
+
__send__ :include, Virtus.model
|
72
|
+
include AttrCoerced
|
73
|
+
include Attestor::Validations
|
24
74
|
include Wisper::Publisher
|
25
|
-
# @!parse include ServiceObjects::Helpers::Parameters
|
26
|
-
# @!parse include ServiceObjects::Helpers::Messages
|
27
|
-
# @!parse include ActiveModel::Validations
|
28
75
|
|
29
|
-
#
|
30
|
-
#
|
76
|
+
# Declares the dependency from another class
|
77
|
+
#
|
78
|
+
# @param [#to_sym] name
|
79
|
+
# @option [Class] :default
|
80
|
+
#
|
81
|
+
# @return [undefined]
|
82
|
+
def self.dependency(name, default: nil)
|
83
|
+
attr_accessor(name)
|
84
|
+
return unless default
|
85
|
+
define_method(name) { instance_eval "@#{ name } ||= #{ default }" }
|
86
|
+
end
|
87
|
+
|
88
|
+
# @!method subscribe(listener, prefix: nil)
|
89
|
+
# Subscribes a listener for the service object's notifications
|
90
|
+
#
|
91
|
+
# @param [Object] listener
|
92
|
+
# @option [#to_sym, nil] :prefix
|
93
|
+
#
|
94
|
+
# @return [undefined]
|
95
|
+
#
|
96
|
+
# @see https://github.com/krisleech/wisper 'wisper' gem API
|
97
|
+
|
98
|
+
# Runs the service by calling {#run!} and catching its :published throws
|
99
|
+
#
|
100
|
+
# @raise [NotImplementedError] if {#run!} method not implemented yet
|
31
101
|
#
|
32
102
|
# @return [undefined]
|
33
103
|
def run
|
104
|
+
catch(:published) { run! }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Validates the object in given context
|
108
|
+
#
|
109
|
+
# Publishes error if validation fails
|
110
|
+
#
|
111
|
+
# @param [#to_sym] context
|
112
|
+
#
|
113
|
+
# @return [undefined]
|
114
|
+
def validate(context)
|
115
|
+
validate!(context)
|
116
|
+
rescue Attestor::InvalidError => error
|
117
|
+
publish :error, error.messages.map { |text| Message.new :error, text }
|
118
|
+
end
|
119
|
+
|
120
|
+
# Runs the service and receives its notifications to listener
|
121
|
+
#
|
122
|
+
# @param [ServiceObjects::Base] service
|
123
|
+
# @param [ServiceObjects::Listener] listener
|
124
|
+
#
|
125
|
+
# @return [undefined]
|
126
|
+
def run_service(service, listener)
|
127
|
+
service.subscribe listener, prefix: :on
|
128
|
+
service.run
|
129
|
+
listener.finalize
|
130
|
+
end
|
131
|
+
|
132
|
+
# @!method message(type, key, options)
|
133
|
+
# Creates the message of given type
|
134
|
+
#
|
135
|
+
# @overload message(type, key, options)
|
136
|
+
# Translates the symbolic key
|
137
|
+
#
|
138
|
+
# @example
|
139
|
+
# # config/locales/en.yml
|
140
|
+
# en:
|
141
|
+
# service_objects:
|
142
|
+
# get_foo:
|
143
|
+
# info:
|
144
|
+
# found: "Item with id %{id} has been found"
|
145
|
+
#
|
146
|
+
# @example
|
147
|
+
# result = GetFoo.new.message(:info, :found, id: 1)
|
148
|
+
#
|
149
|
+
# result.class # => ServiceObjects::Message
|
150
|
+
# result.type # => :info
|
151
|
+
# result.text # => "Item with id 1 has been found"
|
152
|
+
#
|
153
|
+
# @param [Symbol] type
|
154
|
+
# @param [Symbol] key
|
155
|
+
# @param [Hash] options
|
156
|
+
#
|
157
|
+
# @return [ServiceObjects::Message]
|
158
|
+
#
|
159
|
+
# @overload message(type, key)
|
160
|
+
# Stringifies non-symbolic key
|
161
|
+
#
|
162
|
+
# @example
|
163
|
+
# result = GetFoo.new.message(:info, 1)
|
164
|
+
#
|
165
|
+
# result.class # => ServiceObjects::Message
|
166
|
+
# result.type # => :info
|
167
|
+
# result.text # => "1"
|
168
|
+
#
|
169
|
+
# @param [Symbol] type
|
170
|
+
# @param [#to_s] key
|
171
|
+
#
|
172
|
+
# @return [ServiceObjects::Message]
|
173
|
+
def message(type, *args)
|
174
|
+
Message.new type, translate(type, *args)
|
175
|
+
end
|
176
|
+
|
177
|
+
# @!method publish(notification, *args)
|
178
|
+
# Notifies subscribers and then throws :published
|
179
|
+
#
|
180
|
+
# @param [#to_sym] notification
|
181
|
+
# @param [Array] args
|
182
|
+
#
|
183
|
+
# @raise [UncaughtThrowError] :published
|
184
|
+
#
|
185
|
+
# @return [undefined]
|
186
|
+
#
|
187
|
+
# @see https://github.com/krisleech/wisper 'wisper' gem API
|
188
|
+
def publish(*)
|
189
|
+
super
|
190
|
+
throw :published
|
191
|
+
end
|
192
|
+
|
193
|
+
private
|
194
|
+
|
195
|
+
def run!
|
196
|
+
fail NotImplementedError.new "#{ self.class }#run! not implemented"
|
197
|
+
end
|
198
|
+
|
199
|
+
def __class_path__
|
200
|
+
@__class_path__ ||= self.class.name.to_const_path.to_sym
|
201
|
+
end
|
202
|
+
|
203
|
+
def translate(type, message, **options)
|
204
|
+
return message unless message.instance_of? Symbol
|
205
|
+
scope = [:service_objects, __class_path__, type]
|
206
|
+
I18n.t message, options.merge(scope: scope)
|
34
207
|
end
|
35
208
|
|
36
209
|
end # class Base
|
@@ -1,110 +1,56 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
|
3
|
+
require "chronicles"
|
4
|
+
|
3
5
|
module ServiceObjects
|
4
6
|
|
5
|
-
#
|
6
|
-
#
|
7
|
-
# @example (see #new)
|
8
|
-
#
|
9
|
-
# @example (see #finalize)
|
10
|
-
#
|
11
|
-
# @api public
|
7
|
+
# Describes a decorator with callbacks to receive notifications from services
|
12
8
|
class Listener
|
9
|
+
include Chronicles
|
13
10
|
|
14
11
|
# @!scope class
|
15
12
|
# @!method new(object)
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# Decorates given object by adding methods to be called by service objects
|
19
|
-
#
|
20
|
-
# @example Decorates an object
|
21
|
-
# object = Object.new
|
22
|
-
#
|
23
|
-
# listener = ServiceObjects::Listener.new object
|
24
|
-
# listener.send :object
|
25
|
-
# # => object
|
13
|
+
# Creates the listener decorator for given object
|
26
14
|
#
|
27
15
|
# @param [Object] object
|
28
16
|
#
|
29
17
|
# @return [ServiceObjects::Listener]
|
30
|
-
|
31
|
-
# @
|
18
|
+
|
19
|
+
# @private
|
32
20
|
def initialize(object)
|
33
|
-
@
|
21
|
+
@__getobj__ = object
|
22
|
+
start_chronicles except: %i(finalize __getobj__)
|
34
23
|
end
|
35
24
|
|
36
|
-
#
|
25
|
+
# @abstract
|
37
26
|
#
|
38
|
-
#
|
27
|
+
# The method to be called by {#finalize} when no own methods has been called
|
39
28
|
#
|
40
|
-
# @
|
29
|
+
# @return [undefined]
|
41
30
|
def otherwise
|
42
31
|
end
|
43
32
|
|
44
|
-
# Calls {#otherwise}
|
45
|
-
#
|
46
|
-
# @example Calls {#otherwise} in case no method has been checked
|
47
|
-
# class MyListener < ServiceObjects::Listener
|
48
|
-
# def on_success
|
49
|
-
# "success"
|
50
|
-
# end
|
51
|
-
#
|
52
|
-
# def otherwise
|
53
|
-
# "nothing"
|
54
|
-
# end
|
55
|
-
# end
|
33
|
+
# Calls {#otherwise} when no own methods has been called
|
56
34
|
#
|
57
|
-
#
|
58
|
-
# listener.finalize
|
59
|
-
# # => "nothing"
|
35
|
+
# Clears chronicles.
|
60
36
|
#
|
61
|
-
# @
|
62
|
-
#
|
63
|
-
# listener = MyListener.new
|
64
|
-
# listener.respond_to? :on_error
|
65
|
-
# # => false
|
66
|
-
#
|
67
|
-
# listener.finalize
|
68
|
-
# # => "nothing"
|
69
|
-
#
|
70
|
-
# @example Skips {#otherwise} in case a defined method has been checked
|
71
|
-
#
|
72
|
-
# listener = MyListener.new
|
73
|
-
# listener.respond_to? :on_success
|
74
|
-
# # => true
|
75
|
-
#
|
76
|
-
# listener.finalize
|
77
|
-
# # => nil
|
78
|
-
#
|
79
|
-
# @return [self]
|
37
|
+
# @return [undefined]
|
80
38
|
def finalize
|
81
|
-
otherwise unless
|
82
|
-
|
83
|
-
self
|
84
|
-
end
|
85
|
-
|
86
|
-
# Checks whether the method is defined
|
87
|
-
#
|
88
|
-
# Remembers the fact that any defined method has been checked
|
89
|
-
#
|
90
|
-
# @return [Boolean]
|
91
|
-
#
|
92
|
-
# @api private
|
93
|
-
def respond_to?(*)
|
94
|
-
super ? (@notified = true) : false
|
39
|
+
otherwise unless chronicles.empty?
|
40
|
+
chronicles.clear
|
95
41
|
end
|
96
42
|
|
97
43
|
# @private
|
98
44
|
def respond_to_missing?(*args)
|
99
|
-
|
45
|
+
__getobj__.respond_to?(*args)
|
100
46
|
end
|
101
47
|
|
102
48
|
private
|
103
49
|
|
104
|
-
attr_reader :
|
50
|
+
attr_reader :__getobj__
|
105
51
|
|
106
|
-
def method_missing(*args
|
107
|
-
|
52
|
+
def method_missing(*args)
|
53
|
+
__getobj__.__send__(*args)
|
108
54
|
end
|
109
55
|
|
110
56
|
end # class Listener
|
@@ -1,116 +1,35 @@
|
|
1
1
|
# encoding: utf-8
|
2
|
-
require "json"
|
3
2
|
|
4
3
|
module ServiceObjects
|
5
4
|
|
6
|
-
# Describes messages
|
5
|
+
# Describes messages to be returned by service objects
|
7
6
|
class Message
|
8
|
-
include Comparable
|
9
|
-
|
10
|
-
# @!attribute [r] type
|
11
|
-
# The type of the message
|
12
|
-
#
|
13
|
-
# @return [String]
|
14
|
-
attr_reader :type
|
15
|
-
|
16
|
-
# @!attribute [r] text
|
17
|
-
# The text of the message
|
18
|
-
#
|
19
|
-
# @return [String]
|
20
|
-
attr_reader :text
|
21
|
-
|
22
|
-
# @!attribute [r] priority
|
23
|
-
# The priority level for the message
|
24
|
-
#
|
25
|
-
# If priority hasn't been set on initialization, sets it to -1.0 for errors,
|
26
|
-
# or 0.0 otherwise.
|
27
|
-
#
|
28
|
-
# @return [Float]
|
29
|
-
def priority
|
30
|
-
return @custom_priority.to_f if @custom_priority
|
31
|
-
type == "error" ? -1.0 : 0.0
|
32
|
-
end
|
33
7
|
|
34
8
|
# @!scope class
|
35
|
-
# @!method new(type
|
36
|
-
#
|
37
|
-
#
|
38
|
-
# @example
|
39
|
-
# Message.new type: "info", text: "foo", priority: 3
|
9
|
+
# @!method new(type, text)
|
10
|
+
# Creates the immutable message with type and text
|
40
11
|
#
|
41
|
-
# @param [#
|
12
|
+
# @param [#to_sym] type
|
42
13
|
# @param [#to_s] text
|
43
|
-
# @param [#to_f] priority
|
44
|
-
# optional custom priority of the message (the less the higher)
|
45
14
|
#
|
46
15
|
# @return [ServiceObjects::Message]
|
47
|
-
|
48
|
-
|
49
|
-
|
16
|
+
|
17
|
+
# @private
|
18
|
+
def initialize(type, text)
|
19
|
+
@type = type.to_sym
|
50
20
|
@text = text.to_s.freeze
|
51
|
-
@custom_priority = priority
|
52
21
|
freeze
|
53
22
|
end
|
54
23
|
|
55
|
-
#
|
56
|
-
#
|
57
|
-
# @param [Object] other
|
58
|
-
#
|
59
|
-
# @return [true]
|
60
|
-
# if messages has the same attributes
|
61
|
-
# @return [false]
|
62
|
-
# if the other object is not a message or has different argument(s)
|
63
|
-
def ==(other)
|
64
|
-
(self <=> other) == 0
|
65
|
-
end
|
66
|
-
|
67
|
-
# Compares the message with the other object
|
68
|
-
#
|
69
|
-
# @param [Object] other
|
70
|
-
#
|
71
|
-
# @return [-1, 0, 1]
|
72
|
-
# if the argument is a message
|
73
|
-
# @return [nil]
|
74
|
-
# if the other object is not a message
|
75
|
-
def <=>(other)
|
76
|
-
return unless other.is_a? self.class
|
77
|
-
[:priority, :type, :text]
|
78
|
-
.map { |key| __compare_to__ other, by: key }
|
79
|
-
.detect { |value| value } || 0
|
80
|
-
end
|
81
|
-
|
82
|
-
# Converts the message object to hash
|
83
|
-
#
|
84
|
-
# @return [Hash]
|
85
|
-
def to_h
|
86
|
-
{ type: type, text: text }
|
87
|
-
end
|
88
|
-
|
89
|
-
# Converts the message object to json
|
90
|
-
#
|
91
|
-
# @return [String]
|
92
|
-
def to_json
|
93
|
-
to_h.to_json
|
94
|
-
end
|
95
|
-
|
96
|
-
# A human-readable representation of the message
|
97
|
-
#
|
24
|
+
# @!attribute [r] text
|
25
|
+
# The text of the message
|
98
26
|
# @return [String]
|
99
|
-
|
100
|
-
%W(
|
101
|
-
#<ServiceObjects::Message:#{ object_id }
|
102
|
-
type=\"#{ type }\"
|
103
|
-
text=\"#{ text }\"
|
104
|
-
priority=#{ priority }>
|
105
|
-
).join(" ")
|
106
|
-
end
|
107
|
-
|
108
|
-
private
|
27
|
+
attr_reader :text
|
109
28
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
29
|
+
# @!attribute [r] type
|
30
|
+
# The type of the message
|
31
|
+
# @return [Symbol]
|
32
|
+
attr_reader :type
|
114
33
|
|
115
34
|
end # class Message
|
116
35
|
|