mimi-messaging 0.1.1
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 +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.travis.yml +4 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +21 -0
- data/README.md +11 -0
- data/Rakefile +6 -0
- data/bin/console +7 -0
- data/bin/setup +8 -0
- data/lib/mimi/messaging.rb +121 -0
- data/lib/mimi/messaging/connection.rb +181 -0
- data/lib/mimi/messaging/errors.rb +19 -0
- data/lib/mimi/messaging/listener.rb +72 -0
- data/lib/mimi/messaging/message.rb +66 -0
- data/lib/mimi/messaging/model.rb +27 -0
- data/lib/mimi/messaging/model_provider.rb +96 -0
- data/lib/mimi/messaging/notification.rb +31 -0
- data/lib/mimi/messaging/packer.rb +92 -0
- data/lib/mimi/messaging/provider.rb +48 -0
- data/lib/mimi/messaging/request.rb +56 -0
- data/lib/mimi/messaging/request_processor.rb +195 -0
- data/lib/mimi/messaging/request_processor/context.rb +39 -0
- data/lib/mimi/messaging/request_processor/dsl.rb +121 -0
- data/lib/mimi/messaging/version.rb +5 -0
- data/mimi-messaging.gemspec +38 -0
- metadata +169 -0
@@ -0,0 +1,19 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class ConnectionError < StandardError
|
4
|
+
end # class ConnectionError
|
5
|
+
|
6
|
+
class RequestError < StandardError
|
7
|
+
attr_accessor :params
|
8
|
+
|
9
|
+
def initialize(message = 'failed to process request', params = {})
|
10
|
+
@message = message
|
11
|
+
@params = params.dup
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_s
|
15
|
+
@message
|
16
|
+
end
|
17
|
+
end # class RequestError
|
18
|
+
end # module Messaging
|
19
|
+
end # module Mimi
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class Listener < RequestProcessor
|
4
|
+
DEFAULT_DURABLE_QUEUE_OPTIONS = {
|
5
|
+
exclusive: false,
|
6
|
+
durable: true,
|
7
|
+
auto_delete: false
|
8
|
+
}
|
9
|
+
|
10
|
+
DEFAULT_TEMPORARY_QUEUE_OPTIONS = {
|
11
|
+
exclusive: true,
|
12
|
+
durable: false,
|
13
|
+
auto_delete: true
|
14
|
+
}
|
15
|
+
|
16
|
+
abstract!
|
17
|
+
|
18
|
+
queue_options DEFAULT_TEMPORARY_QUEUE_OPTIONS
|
19
|
+
|
20
|
+
def self.queue(name = nil, opts = {})
|
21
|
+
return super unless name
|
22
|
+
queue_name(name)
|
23
|
+
if name && name != ''
|
24
|
+
queue_options(DEFAULT_DURABLE_QUEUE_OPTIONS.merge(opts))
|
25
|
+
else
|
26
|
+
queue_options(DEFAULT_TEMPORARY_QUEUE_OPTIONS.merge(opts))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.default_queue_name
|
31
|
+
''
|
32
|
+
end
|
33
|
+
|
34
|
+
# Sets or gets notification resource name
|
35
|
+
#
|
36
|
+
def self.notification(name = nil, _opts = {})
|
37
|
+
notification_name name
|
38
|
+
true
|
39
|
+
end
|
40
|
+
|
41
|
+
# Sets or gets queue name
|
42
|
+
#
|
43
|
+
def self.notification_name(name = nil)
|
44
|
+
if name && @notification_name
|
45
|
+
raise "#{self} has already registered '#{@notification_name}' as notification name"
|
46
|
+
end
|
47
|
+
(@notification_name ||= name) || default_notification_name
|
48
|
+
end
|
49
|
+
|
50
|
+
# Default (inferred) notification name
|
51
|
+
#
|
52
|
+
def self.default_notification_name
|
53
|
+
class_name_to_resource_name(name, 'listener')
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.resource_name
|
57
|
+
notification_name
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.request_type(_d, _m, _p)
|
61
|
+
:broadcast
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.construct_queue
|
65
|
+
exchange = channel.fanout(resource_name)
|
66
|
+
q = channel.create_queue(queue_name, queue_options)
|
67
|
+
q.bind(exchange)
|
68
|
+
q
|
69
|
+
end
|
70
|
+
end # class Listener
|
71
|
+
end # module Messaging
|
72
|
+
end # module Mimi
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'msgpack'
|
3
|
+
|
4
|
+
module Mimi
|
5
|
+
module Messaging
|
6
|
+
class Message < Hashie::Mash
|
7
|
+
def self.queue(name, _opts = {})
|
8
|
+
@queue_name = name
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.queue_name
|
12
|
+
@queue_name || default_queue_name
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.default_queue_name
|
16
|
+
Mimi::Messaging::RequestProcessor.class_name_to_resource_name(self, 'message')
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.get(name, data = {}, opts = {})
|
20
|
+
d, m, response = Mimi::Messaging.get(
|
21
|
+
queue_name, encode(data), opts.deep_merge(headers: { method_name: name.to_s })
|
22
|
+
)
|
23
|
+
raise Timeout::Error unless response
|
24
|
+
message = new(decode(response))
|
25
|
+
raise RequestError.new(message.error, Message.new(message.params)) if message.error?
|
26
|
+
message
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.post(name, data = {}, opts = {})
|
30
|
+
Mimi::Messaging.post(
|
31
|
+
queue_name, encode(data), opts.deep_merge(headers: { method_name: name.to_s })
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.add_method(name, &block)
|
36
|
+
self.class.instance_eval do
|
37
|
+
define_method(name, &block)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.methods(*names)
|
42
|
+
names.each do |method_name|
|
43
|
+
add_method(method_name) do |*params|
|
44
|
+
get(method_name, *params)
|
45
|
+
end
|
46
|
+
add_method("#{method_name}!") do |*params|
|
47
|
+
post(method_name, *params)
|
48
|
+
nil
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.encode(data)
|
54
|
+
MessagePack.pack(data) # data.to_json
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.decode(raw_message)
|
58
|
+
MessagePack.unpack(raw_message) # JSON.parse(raw_message)
|
59
|
+
end
|
60
|
+
|
61
|
+
def to_s
|
62
|
+
to_hash.to_s
|
63
|
+
end
|
64
|
+
end # class Message
|
65
|
+
end # module Messaging
|
66
|
+
end # module Mimi
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class Model < Mimi::Messaging::Message
|
4
|
+
def self.default_queue_name
|
5
|
+
Mimi::Messaging::RequestProcessor.class_name_to_resource_name(self, 'model')
|
6
|
+
end
|
7
|
+
|
8
|
+
methods :create, :update, :show, :destroy, :list
|
9
|
+
|
10
|
+
def update(params = {})
|
11
|
+
self.replace(self.class.update(params.merge(id: id)))
|
12
|
+
end
|
13
|
+
|
14
|
+
def save
|
15
|
+
update(self)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.find(id)
|
19
|
+
show(id: id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.all
|
23
|
+
list.list
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end # module Messaging
|
27
|
+
end # module Mimi
|
@@ -0,0 +1,96 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class ModelProvider < Mimi::Messaging::Provider
|
4
|
+
abstract!
|
5
|
+
|
6
|
+
def self.model(model_class, options = {})
|
7
|
+
raise "#{self} already serves model #{@model_class}" if @model_class
|
8
|
+
@model_class = model_class
|
9
|
+
@model_options = options
|
10
|
+
@model_methods_only = options[:only] ? [*(options[:only])] : nil
|
11
|
+
@model_methods_except = options[:except] ? [*(options[:except])] : nil
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.exposed_methods
|
15
|
+
model_methods = Mimi::Messaging::ModelProvider.public_instance_methods(false).dup
|
16
|
+
methods_hidden = []
|
17
|
+
methods_hidden = model_methods - model_methods.only(*@model_methods_only) if @model_methods_only
|
18
|
+
methods_hidden = model_methods.only(*@model_methods_except) if @model_methods_except
|
19
|
+
m = super
|
20
|
+
m - methods_hidden
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.model_class
|
24
|
+
@model_class || raise("#{self} has no defined model")
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.serialize(method_name = nil, &block)
|
28
|
+
if method_name && block_given?
|
29
|
+
raise "Only one of method_name or block is accepted by #{self}.serialize"
|
30
|
+
end
|
31
|
+
@serialize = method_name.to_sym if method_name
|
32
|
+
@serialize = block if block_given?
|
33
|
+
@serialize || parent_property(:serialize) || :as_json
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.scope(&block)
|
37
|
+
@scope = block if block_given?
|
38
|
+
@scope || -> (r) { r }
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.permitted_params(*names)
|
42
|
+
@permitted_params = names.map(&:to_sym) if names.size > 0
|
43
|
+
@permitted_params || model_class.attribute_names.map(&:to_sym)
|
44
|
+
end
|
45
|
+
|
46
|
+
def create
|
47
|
+
serialize model_class_scoped.create!(permitted_params)
|
48
|
+
end
|
49
|
+
|
50
|
+
def update(id:)
|
51
|
+
model = model_class_scoped.find(id)
|
52
|
+
model.update!(permitted_params)
|
53
|
+
serialize model
|
54
|
+
end
|
55
|
+
|
56
|
+
def show(id:)
|
57
|
+
serialize model_class_scoped.find(id)
|
58
|
+
end
|
59
|
+
|
60
|
+
def destroy(id:)
|
61
|
+
model_class_scoped.find(id)
|
62
|
+
raise "#{self.class}#destroy is not implemented"
|
63
|
+
end
|
64
|
+
|
65
|
+
def list
|
66
|
+
reply list: model_class_scoped.all.map { |v| serialize v }
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def permitted_params
|
72
|
+
params.except('id').only(*self.class.permitted_params.map(&:to_s)).to_hash
|
73
|
+
end
|
74
|
+
|
75
|
+
def model_class
|
76
|
+
self.class.model_class
|
77
|
+
end
|
78
|
+
|
79
|
+
def model_class_scoped
|
80
|
+
scope_block = self.class.scope
|
81
|
+
__execute(model_class, &scope_block)
|
82
|
+
end
|
83
|
+
|
84
|
+
def serialize(model_instance, opts = nil)
|
85
|
+
opts ||= params
|
86
|
+
if self.class.serialize.is_a?(Symbol)
|
87
|
+
model_instance.send(self.class.serialize, opts)
|
88
|
+
elsif self.class.serialize.is_a?(Proc)
|
89
|
+
self.class.serialize.call(model_instance, opts)
|
90
|
+
else
|
91
|
+
raise "#{self.class}#serialize is neither a Symbol or Proc"
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end # class ModelProvider
|
95
|
+
end # module Messaging
|
96
|
+
end # module Mimi
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class Notification < Hashie::Mash
|
4
|
+
def self.notification(name, _opts = {})
|
5
|
+
@notification_name = name
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.notification_name
|
9
|
+
@notification_name || default_notification_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.default_notification_name
|
13
|
+
Mimi::Messaging::RequestProcessor.class_name_to_resource_name(self, 'notification')
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.broadcast(name, data = {}, opts = {})
|
17
|
+
Mimi::Messaging.broadcast(
|
18
|
+
notification_name, Message.encode(data), opts.merge(headers: { method_name: name.to_s })
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
def broadcast(name, opts = {})
|
23
|
+
self.class.broadcast(name, self, opts)
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_s
|
27
|
+
to_hash.to_s
|
28
|
+
end
|
29
|
+
end # class Notification
|
30
|
+
end # module Messaging
|
31
|
+
end # module Mimi
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require 'msgpack'
|
2
|
+
|
3
|
+
class TypePacker
|
4
|
+
APPLICATION_TYPE_EXT = 0x00
|
5
|
+
|
6
|
+
def self.register(type, opts = {})
|
7
|
+
raise ArgumentError, 'Invalid :from_bytes, proc expected' unless opts[:from_bytes].is_a?(Proc)
|
8
|
+
raise ArgumentError, 'Invalid :to_bytes, proc expected' unless opts[:to_bytes].is_a?(Proc)
|
9
|
+
type_name = type.to_s
|
10
|
+
params = opts.dup.merge(type: type, type_name: type_name)
|
11
|
+
type_packers[type] = params
|
12
|
+
type.send(:define_method, :to_msgpack_ext) { TypePacker.pack(self) }
|
13
|
+
MessagePack::DefaultFactory.register_type(
|
14
|
+
APPLICATION_TYPE_EXT,
|
15
|
+
type,
|
16
|
+
packer: :to_msgpack_ext,
|
17
|
+
# unpacker: :from_msgpack_ext
|
18
|
+
)
|
19
|
+
|
20
|
+
MessagePack::DefaultFactory.register_type(
|
21
|
+
APPLICATION_TYPE_EXT,
|
22
|
+
self,
|
23
|
+
unpacker: :unpack,
|
24
|
+
# unpacker: :from_msgpack_ext
|
25
|
+
)
|
26
|
+
|
27
|
+
# pk = MessagePack::Packer.new
|
28
|
+
# pk.register_type(APPLICATION_TYPE_EXT, type, :to_msgpack_ext) # { |v| TypePacker.pack(v) }
|
29
|
+
|
30
|
+
# register_type_packer!
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.type_packers
|
34
|
+
@type_packers ||= {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.pack(value)
|
38
|
+
type_packer = type_packers.values.find { |p| value.is_a?(p[:type]) }
|
39
|
+
raise "No packer registered for type #{value.class}" unless type_packer
|
40
|
+
bytes = type_packer[:to_bytes].call(value)
|
41
|
+
"#{type_packer[:type_name]}=#{bytes}"
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.unpack(value)
|
45
|
+
vp = value.partition('=') # splits "Type=bytes" into ['Type', '=', 'bytes']
|
46
|
+
type_name = vp.first
|
47
|
+
bytes = vp.last
|
48
|
+
type_packer = type_packers.values.find { |p| p[:type_name] == type_name }
|
49
|
+
raise "No unpacker registered for type #{type_name}" unless type_packer
|
50
|
+
type_packer[:from_bytes].call(bytes)
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.register_type_packer!
|
54
|
+
return if @type_packer_registered
|
55
|
+
# pk = MessagePack::Packer.new
|
56
|
+
# pk.register_type(APPLICATION_TYPE_EXT) { |v| TypePacker.pack(v) }
|
57
|
+
uk = MessagePack::Unpacker.new
|
58
|
+
uk.register_type(APPLICATION_TYPE_EXT) { |b| TypePacker.unpack(b) }
|
59
|
+
@type_packer_registered = true
|
60
|
+
end
|
61
|
+
end # class TypePacker
|
62
|
+
|
63
|
+
|
64
|
+
class A
|
65
|
+
class B
|
66
|
+
attr_reader :value
|
67
|
+
def initialize(value)
|
68
|
+
@value = value
|
69
|
+
end
|
70
|
+
end # class B
|
71
|
+
end # class A
|
72
|
+
|
73
|
+
TypePacker.register(
|
74
|
+
Time,
|
75
|
+
from_bytes: -> (b) { Time.at(b.unpack('D').first) },
|
76
|
+
to_bytes: -> (v) { [v.to_f].pack('D') }
|
77
|
+
)
|
78
|
+
|
79
|
+
TypePacker.register(
|
80
|
+
A::B,
|
81
|
+
from_bytes: -> (b) { A::B.new(b.unpack('D').first) },
|
82
|
+
to_bytes: -> (v) { [v.value.to_f].pack('D') }
|
83
|
+
)
|
84
|
+
|
85
|
+
# TypePacker.register(
|
86
|
+
# DateTime,
|
87
|
+
# from_bytes: -> (b) { Time.at(b.unpack('D').first).to_datetime },
|
88
|
+
# to_bytes: -> (v) { [v.to_time.to_f].pack('D') }
|
89
|
+
# )
|
90
|
+
|
91
|
+
# m = "\x83\xA1a\xC7\r\x00Time=\x1D@\x95\xC7\x80\xCA\xD5A\xA1b\x02\xA1c\xC7\x11\x00DateTime=X@\x95\xC7\x80\xCA\xD5A"
|
92
|
+
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module Mimi
|
2
|
+
module Messaging
|
3
|
+
class Provider < RequestProcessor
|
4
|
+
abstract!
|
5
|
+
queue_options exclusive: false, auto_delete: true
|
6
|
+
|
7
|
+
def self.default_queue_name
|
8
|
+
class_name_to_resource_name(name, 'provider')
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(d, m, p)
|
12
|
+
super
|
13
|
+
begin
|
14
|
+
catch(:halt) do
|
15
|
+
reply(@result) unless request.replied?
|
16
|
+
end
|
17
|
+
rescue StandardError => e
|
18
|
+
__execute_error_handlers(e)
|
19
|
+
end
|
20
|
+
if request.get? && !request.replied?
|
21
|
+
logger.error "No response sent to #{request.canonical_name}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Default error handler for RequestError and its descendants
|
26
|
+
#
|
27
|
+
error RequestError do |e|
|
28
|
+
logger.warn "#{request.canonical_name}: #{e} (#{e.params})"
|
29
|
+
reply error: e.message, params: e.params
|
30
|
+
end
|
31
|
+
|
32
|
+
# Default error handler for StandardError and its descendants
|
33
|
+
#
|
34
|
+
error StandardError do |e|
|
35
|
+
logger.error "#{request.canonical_name}: #{e} (#{e.class})"
|
36
|
+
logger.debug((e.backtrace || ['<no backtrace>']).join("\n"))
|
37
|
+
reply error: e.message
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def reply(data = {})
|
43
|
+
request.send_response(data)
|
44
|
+
halt
|
45
|
+
end
|
46
|
+
end # class Provider
|
47
|
+
end # module Messaging
|
48
|
+
end # module Mimi
|