# encoding: utf-8 # encoding: binary # THIS IS AN AUTOGENERATED FILE, DO NOT MODIFY # IT DIRECTLY ! FOR CHANGES, PLEASE UPDATE FILES # IN THE ./codegen DIRECTORY OF THE AMQ-PROTOCOL REPOSITORY.<% import codegen_helpers as helpers %><% import re, os, codegen %> require "amq/protocol/table" require "amq/protocol/frame" require "amq/hacks" module AMQ module Protocol PROTOCOL_VERSION = "${spec.major}.${spec.minor}.${spec.revision}".freeze PREAMBLE = "${'AMQP\\x00\\x%02x\\x%02x\\x%02x' % (spec.major, spec.minor, spec.revision)}".freeze DEFAULT_PORT = ${spec.port} TLS_PORT = 5671 SSL_PORT = 5671 # caching EMPTY_STRING = "".freeze PACK_CHAR = 'C'.freeze PACK_UINT16 = 'n'.freeze PACK_UINT16_X2 = 'n2'.freeze PACK_UINT32 = 'N'.freeze PACK_UINT32_X2 = 'N2'.freeze PACK_INT64 = 'q'.freeze PACK_UCHAR_UINT32 = 'CN'.freeze PACK_CHAR_UINT16_UINT32 = 'cnN'.freeze PACK_32BIT_FLOAT = 'f'.freeze PACK_64BIT_FLOAT = 'd'.freeze # @return [Array] Collection of subclasses of AMQ::Protocol::Class. def self.classes Protocol::Class.classes end # @return [Array] Collection of subclasses of AMQ::Protocol::Method. def self.methods Protocol::Method.methods end class Error < StandardError DEFAULT_MESSAGE = "AMQP error".freeze def self.inherited(subclass) @_subclasses ||= [] @_subclasses << subclass end # self.inherited(subclazz) def self.subclasses_with_values @_subclasses.select{ |k| defined?(k::VALUE) } end # self.subclasses_with_values def self.[](code) if result = subclasses_with_values.detect { |klass| klass::VALUE == code } result else raise "No such exception class for code #{code}" unless result end # if end # self.[] def initialize(message = self.class::DEFAULT_MESSAGE) super(message) end end class FrameTypeError < Protocol::Error def initialize(types) super("Must be one of #{types.inspect}") end end class EmptyResponseError < Protocol::Error DEFAULT_MESSAGE = "Empty response received from the server." def initialize(message = self.class::DEFAULT_MESSAGE) super(message) end end class BadResponseError < Protocol::Error def initialize(argument, expected, actual) super("Argument #{argument} has to be #{expected.inspect}, was #{data.inspect}") end end class SoftError < Protocol::Error def self.inherited(subclass) Error.inherited(subclass) end # self.inherited(subclass) end class HardError < Protocol::Error def self.inherited(subclass) Error.inherited(subclass) end # self.inherited(subclass) end % for tuple in spec.constants: % if tuple[2] == "soft-error" or tuple[2] == "hard-error": class ${codegen.to_ruby_class_name(tuple[0])} < ${codegen.to_ruby_class_name(tuple[2])} VALUE = ${tuple[1]} end % endif % endfor # We don't instantiate the following classes, # as we don't actually need any per-instance state. # Also, this is pretty low-level functionality, # hence it should have a reasonable performance. # As everyone knows, garbage collector in MRI performs # really badly, which is another good reason for # not creating any objects, but only use class as # a struct. Creating classes is quite expensive though, # but here the inheritance comes handy and mainly # as we can't simply make a reference to a function, # we can't use a hash or an object. I've been also # considering to have just a bunch of methods, but # here's the problem, that after we'd require this file, # all these methods would become global which would # be a bad, bad thing to do. class Class @classes = Array.new def self.method_id @method_id end def self.name @name end def self.inherited(base) if self == Protocol::Class @classes << base end end def self.classes @classes end end class Method @methods = Array.new def self.method_id @method_id end def self.name @name end def self.index @index end def self.inherited(base) if self == Protocol::Method @methods << base end end def self.methods @methods end def self.split_headers(user_headers) properties, headers = {}, {} user_headers.each do |key, value| # key MUST be a symbol since symbols are not garbage-collected if Basic::PROPERTIES.include?(key) properties[key] = value else headers[key] = value end end return [properties, headers] end def self.encode_body(body, channel, frame_size) return [] if body.empty? # See https://dev.rabbitmq.com/wiki/Amqp091Errata#section_11 limit = frame_size - 8 array = Array.new while body payload, body = body[0, limit], body[limit, body.length - limit] # array << [0x03, payload] array << BodyFrame.new(payload, channel) end array end # We can return different: # - instantiate given subclass of Method # - create an OpenStruct object # - create a hash # - yield params into the block rather than just return # @api plugin def self.instantiate(*args, &block) self.new(*args, &block) # or OpenStruct.new(args.first) # or args.first # or block.call(*args) end end % for klass in spec.classes : class ${klass.constant_name} < Protocol::Class @name = "${klass.name}" @method_id = ${klass.index} % if klass.fields: ## only the Basic class has fields (refered as properties in the JSON) PROPERTIES = [ % for field in klass.fields: :${field.ruby_name}, # ${spec.resolveDomain(field.domain)} % endfor ] % for f in klass.fields: # <% i = klass.fields.index(f) %>1 << ${15 - i} def self.encode_${f.ruby_name}(value) buffer = '' % for line in helpers.genSingleEncode(spec, "value", f.domain): ${line} % endfor [${i}, ${"0x%04x" % ( 1 << (15-i),)}, buffer] end % endfor % endif ## TODO: not only basic, resp. in fact it's only this class, but not necessarily in the future, rather check if properties are empty #} % if klass.name == "basic" : def self.encode_properties(body_size, properties) pieces, flags = [], 0 properties.reject {|key, value| value.nil?}.each do |key, value| i, f, result = self.__send__(:"encode_#{key}", value) flags |= f pieces[i] = result end # result = [${klass.index}, 0, body_size, flags].pack('n2Qn') result = [${klass.index}, 0].pack(PACK_UINT16_X2) result += AMQ::Hacks.pack_64_big_endian(body_size) result += [flags].pack(PACK_UINT16) result + pieces.join(EMPTY_STRING) end # THIS DECODES ONLY FLAGS DECODE_PROPERTIES = { % for f in klass.fields: ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)} => :${f.ruby_name}, % endfor } DECODE_PROPERTIES_TYPE = { % for f in klass.fields: ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)} => :${spec.resolveDomain(f.domain)}, % endfor } # Hash doesn't give any guarantees on keys order, we will do it in a # straightforward way DECODE_PROPERTIES_KEYS = [ % for f in klass.fields: ${"0x%04x" % ( 1 << (15 - klass.fields.index(f)),)}, % endfor ] def self.decode_properties(data) offset, data_length, properties = 0, data.bytesize, {} compressed_index = data[offset, 2].unpack(PACK_UINT16)[0] offset += 2 while data_length > offset DECODE_PROPERTIES_KEYS.each do |key| next unless compressed_index >= key compressed_index -= key name = DECODE_PROPERTIES[key] || raise(RuntimeError.new("No property found for index #{index.inspect}!")) case DECODE_PROPERTIES_TYPE[key] when :shortstr size = data[offset, 1].unpack(PACK_CHAR)[0] offset += 1 result = data[offset, size] when :octet size = 1 result = data[offset, size].unpack(PACK_CHAR).first when :timestamp size = 8 result = Time.at(data[offset, size].unpack(PACK_UINT32_X2).last) when :table size = 4 + data[offset, 4].unpack(PACK_UINT32)[0] result = Table.decode(data[offset, size]) end properties[name] = result offset += size end end properties end % endif % for method in klass.methods: class ${method.constant_name} < Protocol::Method @name = "${klass.name}.${method.name}" @method_id = ${method.index} @index = ${method.binary()} @packed_indexes = [${klass.index}, ${method.index}].pack(PACK_UINT16_X2).freeze % if (spec.type == "client" and method.accepted_by("client")) or (spec.type == "server" and method.accepted_by("server") or spec.type == "all"): # @return def self.decode(data) offset = 0 % for line in helpers.genDecodeMethodDefinition(spec, method): ${line} % endfor % if (method.klass.name == "connection" or method.klass.name == "channel") and method.name == "close": self.new(${', '.join([f.ruby_name for f in method.arguments])}) % else: self.new(${', '.join([f.ruby_name for f in method.arguments])}) % endif end % if len(method.arguments) > 0: attr_reader ${', '.join([":" + f.ruby_name for f in method.arguments])} % endif def initialize(${', '.join([f.ruby_name for f in method.arguments])}) % for f in method.arguments: @${f.ruby_name} = ${f.ruby_name} % endfor end % endif def self.has_content? % if method.hasContent: true % else: false % endif end % if (spec.type == "client" and method.accepted_by("server")) or (spec.type == "server" and method.accepted_by("client")) or spec.type == "all": # @return # ${method.params()} % if klass.name == "connection": def self.encode(${(", ").join(method.not_ignored_args())}) % else: def self.encode(${(", ").join(["channel"] + method.not_ignored_args())}) % endif % for argument in method.ignored_args(): ${codegen.convert_to_ruby(argument)} % endfor % if klass.name == "connection": channel = 0 % endif buffer = '' buffer << @packed_indexes % for line in helpers.genEncodeMethodDefinition(spec, method): ${line} % endfor % if "payload" in method.args() or "user_headers" in method.args(): frames = [MethodFrame.new(buffer, channel)] % if "user_headers" in method.args(): properties, headers = self.split_headers(user_headers) # TODO: what shall I do with the headers? if properties.nil? or properties.empty? raise RuntimeError.new("Properties can not be empty!") # TODO: or can they? end properties_payload = Basic.encode_properties(payload.bytesize, properties) frames << HeaderFrame.new(properties_payload, channel) % endif % if "payload" in method.args(): frames + self.encode_body(payload, channel, frame_size) % endif % else: MethodFrame.new(buffer, channel) % endif end % endif end % endfor end % endfor METHODS = begin Method.methods.inject(Hash.new) do |hash, klass| hash.merge!(klass.index => klass) end end end end