mimi-messaging 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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