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.
@@ -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