amqplib
Version:
An AMQP 0-9-1 (e.g., RabbitMQ) library and client.
724 lines (633 loc) • 21.2 kB
JavaScript
var FS = require('fs');
var format = require('util').format;
var defs = require('./amqp-rabbitmq-0.9.1.json');
var FRAME_OVERHEAD = 8; // type + channel + size + frame-end
var METHOD_OVERHEAD = FRAME_OVERHEAD + 4;
// F_O + classId + methodId
var PROPERTIES_OVERHEAD = FRAME_OVERHEAD + 4 + 8 + 2;
// F_O + classId + weight + content size + flags
var out = process.stdout;
function printf() {
out.write(format.apply(format, arguments), 'utf8');
}
function nl() { out.write('\n'); }
function println() { printf.apply(printf, arguments); nl(); }
function isEmptyObject(val) {
return (val != null && typeof val === 'object' &&
Object.keys(val).length === 0);
}
function stringifyValue(val) {
return (isEmptyObject(val)) ? 'EMPTY_OBJECT' :
JSON.stringify(val);
}
var constants = {};
var constant_strs = {};
for (var i = 0, len = defs.constants.length; i < len; i++) {
var cdef = defs.constants[i];
constants[constantName(cdef)] = cdef.value;
constant_strs[cdef.value] = cdef.name;
}
function constantName(def) {
return def.name.replace(/-/g, '_');
}
function methodName(clazz, method) {
return initial(clazz.name) + method.name.split('-').map(initial).join('');
}
function propertyName(dashed) {
var parts = dashed.split('-');
return parts[0] + parts.slice(1).map(initial).join('');
}
function initial(part) {
return part.charAt(0).toUpperCase() + part.substr(1);
}
function argument(a) {
var type = a.type || domains[a.domain];
var friendlyName = propertyName(a.name);
return {type: type, name: friendlyName, default: a['default-value']};
}
var domains = {};
for (var i=0, len = defs.domains.length; i < len; i++) {
var dom = defs.domains[i];
domains[dom[0]] = dom[1];
}
var methods = {};
var propertieses = {};
for (var i = 0, len = defs.classes.length; i < len; i++) {
var clazz = defs.classes[i];
for (var j = 0, num = clazz.methods.length; j < num; j++) {
var method = clazz.methods[j];
var name = methodName(clazz, method);
var info = 'methodInfo' + name;
methods[name] = {
id: methodId(clazz, method),
name: name,
methodId: method.id,
clazzId: clazz.id,
clazz: clazz.name,
args: method['arguments'].map(argument),
isReply: method.answer,
encoder: 'encode' + name,
decoder: 'decode' + name,
info: info
};
}
if (clazz.properties && clazz.properties.length > 0) {
var name = propertiesName(clazz);
var props = clazz.properties;
propertieses[name] = {
id: clazz.id,
name: name,
encoder: 'encode' + name,
decoder: 'decode' + name,
info: 'propertiesInfo' + name,
args: props.map(argument),
};
}
}
// OK let's get emitting
println(
'/** @preserve This file is generated by the script\n',
'* ../bin/generate-defs.js, which is not in general included in a\n',
'* distribution, but is available in the source repository e.g. at\n',
'* https://github.com/squaremo/amqp.node/\n',
'*/');
println("'use strict';"); nl();
println('var Buffer = require("safe-buffer").Buffer;');
nl()
println('var codec = require("./codec");');
println('var ints = require("buffer-more-ints");');
println('var encodeTable = codec.encodeTable;');
println('var decodeFields = codec.decodeFields;');
nl();
println('var SCRATCH = Buffer.alloc(65536);');
println('var EMPTY_OBJECT = Object.freeze({});');
println('module.exports.constants = %s',
JSON.stringify(constants));
nl();
println('module.exports.constant_strs = %s',
JSON.stringify(constant_strs));
nl();
println('module.exports.FRAME_OVERHEAD = %d;', FRAME_OVERHEAD);
nl();
println('module.exports.decode = function(id, buf) {');
println('switch (id) {');
for (var m in methods) {
var method = methods[m];
println('case %d: return %s(buf);', method.id, method.decoder);
}
for (var p in propertieses) {
var props = propertieses[p];
println('case %d: return %s(buf);', props.id, props.decoder);
}
println('default: throw new Error("Unknown class/method ID");');
println('}}'); nl();
println('module.exports.encodeMethod =',
'function(id, channel, fields) {');
println('switch (id) {');
for (var m in methods) {
var method = methods[m];
println('case %d: return %s(channel, fields);',
method.id, method.encoder);
}
println('default: throw new Error("Unknown class/method ID");');
println('}}'); nl();
println('module.exports.encodeProperties ='
, 'function(id, channel, size, fields) {');
println('switch (id) {');
for (var p in propertieses) {
var props = propertieses[p];
println('case %d: return %s(channel, size, fields);',
props.id, props.encoder);
}
println('default: throw new Error("Unknown class/properties ID");');
println('}}'); nl();
println('module.exports.info = function(id) {');
println('switch(id) {');
for (var m in methods) {
var method = methods[m];
println('case %d: return %s; ', method.id, method.info);
}
for (var p in propertieses) {
var properties = propertieses[p];
println('case %d: return %s', properties.id, properties.info);
}
println('default: throw new Error("Unknown class/method ID");');
println('}}'); nl();
for (var m in methods) {
var method = methods[m];
println('module.exports.%s = %d;', m, method.id);
decoderFn(method); nl();
encoderFn(method); nl();
infoObj(method); nl();
}
for (var p in propertieses) {
var properties = propertieses[p];
println('module.exports.%s = %d;', p, properties.id);
encodePropsFn(properties); nl();
decodePropsFn(properties); nl();
infoObj(properties); nl();
}
function methodId(clazz, method) {
return (clazz.id << 16) + method.id;
}
function propertiesName(clazz) {
return initial(clazz.name) + 'Properties';
}
function valTypeTest(arg) {
switch (arg.type) {
// everything is booleany
case 'bit': return 'true'
case 'octet':
case 'short':
case 'long':
case 'longlong':
case 'timestamp': return "typeof val === 'number' && !isNaN(val)";
case 'shortstr': return "typeof val === 'string' &&" +
" Buffer.byteLength(val) < 256";
case 'longstr': return "Buffer.isBuffer(val)";
case 'table': return "typeof val === 'object'";
}
}
function typeDesc(t) {
switch (t) {
case 'bit': return 'booleany';
case 'octet':
case 'short':
case 'long':
case 'longlong':
case 'timestamp': return "a number (but not NaN)";
case 'shortstr': return "a string (up to 255 chars)";
case 'longstr': return "a Buffer";
case 'table': return "an object";
}
}
function defaultValueRepr(arg) {
switch (arg.type) {
case 'longstr':
return format("Buffer.from(%s)", JSON.stringify(arg.default));
default:
// assumes no tables as defaults
return JSON.stringify(arg.default);
}
}
// Emit code to assign the arg value to `val`.
function assignArg(a) {
println("val = fields['%s'];", a.name);
}
function assignOrDefault(a) {
println("val = fields['%s'];", a.name);
println("if (val === undefined) val = %s;", defaultValueRepr(a));
}
// Emit code for assigning an argument value to `val`, checking that
// it exists (if it does not have a default) and is the correct
// type.
function checkAssignArg(a) {
assignArg(a);
println('if (val === undefined) {');
if (a.default !== undefined) {
println('val = %s;', defaultValueRepr(a));
}
else {
println('throw new Error("Missing value for mandatory field \'%s\'");', a.name);
}
println('}'); // undefined test
println('else if (!(%s)) {', valTypeTest(a));
println('throw new TypeError(');
println('"Field \'%s\' is the wrong type; must be %s");',
a.name, typeDesc(a.type));
println('}'); // type test
}
// Emit code for encoding `val` as a table and assign to a fresh
// variable (based on the arg name). I use a scratch buffer to compose
// the encoded table, otherwise I'd have to do a size calculation pass
// first. I can get away with this only because 1. the encoding
// procedures are not re-entrant; and, 2. I copy the result into
// another buffer before returning. `scratchOffset`, `val`, `len` are
// expected to have been declared.
function assignTable(a) {
var varname = tableVar(a);
println(
"len = encodeTable(SCRATCH, val, scratchOffset);");
println('var %s = SCRATCH.slice(scratchOffset, scratchOffset + len);', varname);
println('scratchOffset += len;');
}
function tableVar(a) {
return a.name + '_encoded';
}
function stringLenVar(a) {
return a.name + '_len';
}
function assignStringLen(a) {
var v = stringLenVar(a);
// Assumes the value or default is in val
println("var %s = Buffer.byteLength(val, 'utf8');", v);
}
function encoderFn(method) {
var args = method['args'];
println('function %s(channel, fields) {', method.encoder);
println('var offset = 0, val = null, bits = 0, varyingSize = 0;');
println('var len, scratchOffset = 0;');
// Encoding is split into two parts. Some fields have a fixed size
// (e.g., integers of a specific width), while some have a size that
// depends on the datum (e.g., strings). Each field will therefore
// either 1. contribute to the fixed size; or 2. emit code to
// calculate the size (and possibly the encoded value, in the case
// of tables).
var fixedSize = METHOD_OVERHEAD;
var bitsInARow = 0;
for (var i=0, len = args.length; i < len; i++) {
var arg = args[i];
if (arg.type != 'bit') bitsInARow = 0;
switch (arg.type) {
// varying size
case 'shortstr':
checkAssignArg(arg);
assignStringLen(arg);
println("varyingSize += %s;", stringLenVar(arg));
fixedSize += 1;
break;
case 'longstr':
checkAssignArg(arg);
println("varyingSize += val.length;");
fixedSize += 4;
break;
case 'table':
// For a table we have to encode the table before we can see its
// length.
checkAssignArg(arg);
assignTable(arg);
println('varyingSize += %s.length;', tableVar(arg));
break;
// fixed size
case 'octet': fixedSize += 1; break;
case 'short': fixedSize += 2; break;
case 'long': fixedSize += 4; break;
case 'longlong': //fall through
case 'timestamp':
fixedSize += 8; break;
case 'bit':
bitsInARow ++;
// open a fresh pack o' bits
if (bitsInARow === 1) fixedSize += 1;
// just used a pack; reset
else if (bitsInARow === 8) bitsInARow = 0;
break;
}
}
println('var buffer = Buffer.alloc(%d + varyingSize);', fixedSize);
println('buffer[0] = %d;', constants.FRAME_METHOD);
println('buffer.writeUInt16BE(channel, 1);');
// skip size for now, we'll write it in when we know
println('buffer.writeUInt32BE(%d, 7);', method.id);
println('offset = 11;');
bitsInARow = 0;
for (var i = 0, len = args.length; i < len; i++) {
var a = args[i];
// Flush any collected bits before doing a new field
if (a.type != 'bit' && bitsInARow > 0) {
bitsInARow = 0;
println('buffer[offset] = bits; offset++; bits = 0;');
}
switch (a.type) {
case 'octet':
checkAssignArg(a);
println('buffer.writeUInt8(val, offset); offset++;');
break;
case 'short':
checkAssignArg(a);
println('buffer.writeUInt16BE(val, offset); offset += 2;');
break;
case 'long':
checkAssignArg(a);
println('buffer.writeUInt32BE(val, offset); offset += 4;');
break;
case 'longlong':
case 'timestamp':
checkAssignArg(a);
println('ints.writeUInt64BE(buffer, val, offset); offset += 8;');
break;
case 'bit':
checkAssignArg(a);
println('if (val) bits += %d;', 1 << bitsInARow);
if (bitsInARow === 7) { // I don't think this ever happens, but whatever
println('buffer[offset] = bits; offset++; bits = 0;');
bitsInARow = 0;
}
else bitsInARow++;
break;
case 'shortstr':
assignOrDefault(a);
println('buffer[offset] = %s; offset++;', stringLenVar(a));
println('buffer.write(val, offset, "utf8"); offset += %s;',
stringLenVar(a));
break;
case 'longstr':
assignOrDefault(a);
println('len = val.length;');
println('buffer.writeUInt32BE(len, offset); offset += 4;');
println('val.copy(buffer, offset); offset += len;');
break;
case 'table':
println('offset += %s.copy(buffer, offset);', tableVar(a));
break;
default: throw new Error("Unexpected argument type: " + a.type);
}
}
// Flush any collected bits at the end
if (bitsInARow > 0) {
println('buffer[offset] = bits; offset++;');
}
println('buffer[offset] = %d;', constants.FRAME_END);
// size does not include the frame header or frame end byte
println('buffer.writeUInt32BE(offset - 7, 3);');
println('return buffer;');
println('}');
}
function fieldsDecl(args) {
println('var fields = {');
for (var i=0, num=args.length; i < num; i++) {
println('%s: undefined,', args[i].name);
}
println('};');
}
function decoderFn(method) {
var args = method.args;
println('function %s(buffer) {', method.decoder);
println('var offset = 0, val, len;');
fieldsDecl(args);
var bitsInARow = 0;
for (var i=0, num=args.length; i < num; i++) {
var a = args[i];
var field = "fields['" + a.name + "']";
// Flush any collected bits before doing a new field
if (a.type != 'bit' && bitsInARow > 0) {
bitsInARow = 0;
println('offset++;');
}
switch (a.type) {
case 'octet':
println('val = buffer[offset]; offset++;');
break;
case 'short':
println('val = buffer.readUInt16BE(offset); offset += 2;');
break;
case 'long':
println('val = buffer.readUInt32BE(offset); offset += 4;');
break;
case 'longlong':
case 'timestamp':
println('val = ints.readUInt64BE(buffer, offset); offset += 8;');
break;
case 'bit':
var bit = 1 << bitsInARow;
println('val = !!(buffer[offset] & %d);', bit);
if (bitsInARow === 7) {
println('offset++;');
bitsInARow = 0;
}
else bitsInARow++;
break;
case 'longstr':
println('len = buffer.readUInt32BE(offset); offset += 4;');
println('val = buffer.slice(offset, offset + len);');
println('offset += len;');
break;
case 'shortstr':
println('len = buffer.readUInt8(offset); offset++;');
println('val = buffer.toString("utf8", offset, offset + len);');
println('offset += len;');
break;
case 'table':
println('len = buffer.readUInt32BE(offset); offset += 4;');
println('val = decodeFields(buffer.slice(offset, offset + len));');
println('offset += len;');
break;
default:
throw new TypeError("Unexpected type in argument list: " + a.type);
}
println('%s = val;', field);
}
println('return fields;');
println('}');
}
function infoObj(thing) {
var info = JSON.stringify({id: thing.id,
classId: thing.clazzId,
methodId: thing.methodId,
name: thing.name,
args: thing.args});
println('var %s = module.exports.%s = %s;',
thing.info, thing.info, info);
}
// The flags are laid out in groups of fifteen in a short (high to
// low bits), with a continuation bit (at 0) and another group
// following if there's more than fifteen. Presence and absence
// are conflated with true and false, for bit fields (i.e., if the
// flag for the field is set, it's true, otherwise false).
//
// However, none of that is actually used in AMQP 0-9-1. The only
// instance of properties -- basic properties -- has 14 fields, none
// of them bits.
function flagAt(index) {
return 1 << (15 - index);
}
function encodePropsFn(props) {
println('function %s(channel, size, fields) {', props.encoder);
println('var offset = 0, flags = 0, val, len;');
println('var scratchOffset = 0, varyingSize = 0;');
var fixedSize = PROPERTIES_OVERHEAD;
var args = props.args;
function incVarying(by) {
println("varyingSize += %d;", by);
}
for (var i=0, num=args.length; i < num; i++) {
var p = args[i];
assignArg(p);
println("if (val != undefined) {");
println("if (%s) {", valTypeTest(p));
switch (p.type) {
case 'shortstr':
assignStringLen(p);
incVarying(1);
println('varyingSize += %s;', stringLenVar(p));
break;
case 'longstr':
incVarying(4);
println('varyingSize += val.length;');
break;
case 'table':
assignTable(p);
println('varyingSize += %s.length;', tableVar(p));
break;
case 'octet': incVarying(1); break;
case 'short': incVarying(2); break;
case 'long': incVarying(4); break;
case 'longlong': // fall through
case 'timestamp':
incVarying(8); break;
// no case for bit, as they are accounted for in the flags
}
println('} else {');
println('throw new TypeError(');
println('"Field \'%s\' is the wrong type; must be %s");',
p.name, typeDesc(p.type));
println('}');
println('}');
}
println('var buffer = Buffer.alloc(%d + varyingSize);', fixedSize);
println('buffer[0] = %d', constants.FRAME_HEADER);
println('buffer.writeUInt16BE(channel, 1);');
// content class ID and 'weight' (== 0)
println('buffer.writeUInt32BE(%d, 7);', props.id << 16);
// skip frame size for now, we'll write it in when we know.
// body size
println('ints.writeUInt64BE(buffer, size, 11);');
println('flags = 0;');
// we'll write the flags later too
println('offset = 21;');
for (var i=0, num=args.length; i < num; i++) {
var p = args[i];
var flag = flagAt(i);
assignArg(p);
println("if (val != undefined) {");
if (p.type === 'bit') { // which none of them are ..
println('if (val) flags += %d;', flag);
}
else {
println('flags += %d;', flag);
// %%% FIXME only slightly different to the method args encoding
switch (p.type) {
case 'octet':
println('buffer.writeUInt8(val, offset); offset++;');
break;
case 'short':
println('buffer.writeUInt16BE(val, offset); offset += 2;');
break;
case 'long':
println('buffer.writeUInt32BE(val, offset); offset += 4;');
break;
case 'longlong':
case 'timestamp':
println('ints.writeUInt64BE(buffer, val, offset);');
println('offset += 8;');
break;
case 'shortstr':
var v = stringLenVar(p);
println('buffer[offset] = %s; offset++;', v);
println("buffer.write(val, offset, 'utf8');");
println("offset += %s;", v);
break;
case 'longstr':
println('buffer.writeUInt32BE(val.length, offset);');
println('offset += 4;');
println('offset += val.copy(buffer, offset);');
break;
case 'table':
println('offset += %s.copy(buffer, offset);', tableVar(p));
break;
default: throw new Error("Unexpected argument type: " + p.type);
}
}
println('}'); // != undefined
}
println('buffer[offset] = %d;', constants.FRAME_END);
// size does not include the frame header or frame end byte
println('buffer.writeUInt32BE(offset - 7, 3);');
println('buffer.writeUInt16BE(flags, 19);');
println('return buffer.slice(0, offset + 1);');
println('}');
}
function decodePropsFn(props) {
var args = props.args;
println('function %s(buffer) {', props.decoder);
println('var flags, offset = 2, val, len;');
println('flags = buffer.readUInt16BE(0);');
println('if (flags === 0) return {};');
fieldsDecl(args);
for (var i=0, num=args.length; i < num; i++) {
var p = argument(args[i]);
var field = "fields['" + p.name + "']";
println('if (flags & %d) {', flagAt(i));
if (p.type === 'bit') {
println('%d = true;', field);
}
else {
switch (p.type) {
case 'octet':
println('val = buffer[offset]; offset++;');
break;
case 'short':
println('val = buffer.readUInt16BE(offset); offset += 2;');
break;
case 'long':
println('val = buffer.readUInt32BE(offset); offset += 4;');
break;
case 'longlong':
case 'timestamp':
println('val = ints.readUInt64BE(buffer, offset); offset += 8;');
break;
case 'longstr':
println('len = buffer.readUInt32BE(offset); offset += 4;');
println('val = buffer.slice(offset, offset + len);');
println('offset += len;');
break;
case 'shortstr':
println('len = buffer.readUInt8(offset); offset++;');
println('val = buffer.toString("utf8", offset, offset + len);');
println('offset += len;');
break;
case 'table':
println('len = buffer.readUInt32BE(offset); offset += 4;');
println('val = decodeFields(buffer.slice(offset, offset + len));');
println('offset += len;');
break;
default:
throw new TypeError("Unexpected type in argument list: " + p.type);
}
println('%s = val;', field);
}
println('}');
}
println('return fields;');
println('}');
}