mockttp
Version:
Mock HTTP server for testing HTTP clients and stubbing webservices
264 lines • 9.92 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.ClientServerChannel = exports.Serializable = void 0;
exports.serialize = serialize;
exports.deserialize = deserialize;
exports.serializeBuffer = serializeBuffer;
exports.deserializeBuffer = deserializeBuffer;
exports.ensureParamsDeferenced = ensureParamsDeferenced;
exports.serializeProxyConfig = serializeProxyConfig;
exports.deserializeProxyConfig = deserializeProxyConfig;
const buffer_1 = require("buffer");
const stream_1 = require("stream");
const _ = require("lodash");
const rule_parameters_1 = require("../rules/rule-parameters");
function serialize(obj, stream) {
const channel = new ClientServerChannel(stream);
const data = obj.serialize(channel);
data.topicId = channel.topicId;
return data;
}
function deserialize(data, stream, options, lookup) {
const type = data.type;
const channel = new ClientServerChannel(stream, data.topicId);
const deserialized = lookup[type].deserialize(data, channel, options);
// Wrap .dispose and ensure the channel is always disposed too.
const builtinDispose = deserialized.dispose;
deserialized.dispose = () => {
builtinDispose();
channel.dispose();
};
return deserialized;
}
class Serializable {
/**
* @internal
*/
serialize(_channel) {
// By default, we assume data is transferrable as-is
return this;
}
/**
* @internal
*/
static deserialize(data, _channel, _options // Varies, e.g. in plugins.
) {
// By default, we assume we just need to assign the right prototype
return _.create(this.prototype, data);
}
// This rule is being unregistered. Any handlers who need to cleanup when they know
// they're no longer in use should implement this and dispose accordingly.
// Only deserialized rules are disposed - if the originating rule needs
// disposing too, ping the channel and let it know.
dispose() { }
}
exports.Serializable = Serializable;
const DISPOSE_MESSAGE = { disposeChannel: true };
// Wraps another stream, ensuring that messages go only to the paired channel on the
// other client/server. In practice, each handler gets one end of these streams in
// their serialize/deserialize methods, and can use them to sync live data reliably.
class ClientServerChannel extends stream_1.Duplex {
constructor(rawStream, topicId) {
super({ objectMode: true });
this.rawStream = rawStream;
this._onRawStreamError = (error) => {
this.destroy(error);
};
this._onRawStreamFinish = () => {
this.end();
};
this._readFromRawStream = (rawData) => {
const stringData = rawData.toString();
stringData.split('\n').filter(d => !!d).forEach((rawDataLine) => {
let data;
try {
data = JSON.parse(rawDataLine);
}
catch (e) {
console.log(e);
console.log('Received unparseable message, dropping.', rawDataLine.toString());
return;
}
if (data.topicId === this.topicId) {
if (_.isEqual(_.omit(data, 'topicId'), DISPOSE_MESSAGE))
this.dispose(true);
else
this.push(data);
}
});
};
this.reading = false;
this.topicId = topicId || crypto.randomUUID();
this.rawStream.on('error', this._onRawStreamError);
this.rawStream.on('finish', this._onRawStreamFinish);
}
/**
* @internal @hidden
*/
_write(message, encoding, callback) {
message.topicId = this.topicId;
const chunk = JSON.stringify(message) + '\n';
if (!this.rawStream.write(chunk, encoding)) {
this.rawStream.once('drain', callback);
}
else {
callback();
}
}
_read() {
if (!this.reading) {
this.rawStream.on('data', this._readFromRawStream);
this.reading = true;
}
}
request(actionOrData, dataOrNothing) {
let action;
let data;
if (_.isString(actionOrData)) {
action = actionOrData;
data = dataOrNothing;
}
else {
data = actionOrData;
}
const requestId = crypto.randomUUID();
return new Promise((resolve, reject) => {
const responseListener = (response) => {
if (response.requestId === requestId) {
if (response.error) {
// Derialize error from plain object
reject(Object.assign(new Error(), { stack: undefined }, response.error));
}
else {
resolve(response.data);
}
this.removeListener('data', responseListener);
}
};
const request = { data, requestId };
if (action)
request.action = action;
this.write(request, (e) => {
if (e)
reject(e);
else
this.on('data', responseListener);
});
});
}
onRequest(cbOrAction, cbOrNothing) {
let actionName;
let cb;
if (_.isString(cbOrAction)) {
actionName = cbOrAction;
cb = cbOrNothing;
}
else {
cb = cbOrAction;
}
this.on('data', async (request) => {
const { requestId, action } = request;
// Filter by actionName, if set
if (actionName !== undefined && action !== actionName)
return;
try {
const response = {
requestId,
data: await cb(request.data)
};
if (!this.writable)
return; // Response too slow - drop it
this.write(response);
}
catch (error) {
// Make the error serializable:
error = _.pick(error, Object.getOwnPropertyNames(error));
if (!this.writable)
return; // Response too slow - drop it
this.write({ requestId, error });
}
});
}
// Shuts down the channel. Only needs to be called on one side, the other side
// will be shut down automatically when it receives DISPOSE_MESSAGE.
dispose(disposeReceived = false) {
this.on('error', () => { }); // Dispose is best effort - we don't care about errors
// Only one side needs to send a dispose - we send first if we haven't seen one.
if (!disposeReceived)
this.end(DISPOSE_MESSAGE);
else
this.end();
// Detach any remaining onRequest handlers:
this.removeAllListeners('data');
// Stop receiving upstream messages from the global stream:
this.rawStream.removeListener('data', this._readFromRawStream);
this.rawStream.removeListener('error', this._onRawStreamError);
this.rawStream.removeListener('finish', this._onRawStreamFinish);
}
}
exports.ClientServerChannel = ClientServerChannel;
function serializeBuffer(buffer) {
return buffer.toString('base64');
}
function deserializeBuffer(buffer) {
return buffer_1.Buffer.from(buffer, 'base64');
}
const SERIALIZED_PARAM_REFERENCE = "__mockttp__param__reference__";
function serializeParam(value) {
// Swap the symbol for a string, since we can't serialize symbols in JSON:
return { [SERIALIZED_PARAM_REFERENCE]: value[rule_parameters_1.MOCKTTP_PARAM_REF] };
}
function isSerializedRuleParam(value) {
return value && SERIALIZED_PARAM_REFERENCE in value;
}
function ensureParamsDeferenced(value, ruleParams) {
if (isSerializedRuleParam(value)) {
const paramRef = {
[rule_parameters_1.MOCKTTP_PARAM_REF]: value[SERIALIZED_PARAM_REFERENCE]
};
return (0, rule_parameters_1.dereferenceParam)(paramRef, ruleParams);
}
else {
return value;
}
}
function serializeProxyConfig(proxyConfig, channel) {
if (_.isFunction(proxyConfig)) {
const callbackId = `proxyConfig-callback-${crypto.randomUUID()}`;
channel.onRequest(callbackId, proxyConfig);
return callbackId;
}
else if (_.isArray(proxyConfig)) {
return proxyConfig.map((config) => serializeProxyConfig(config, channel));
}
else if ((0, rule_parameters_1.isParamReference)(proxyConfig)) {
return serializeParam(proxyConfig);
}
else if (proxyConfig) {
return {
...proxyConfig,
trustedCAs: proxyConfig.trustedCAs?.map((caDefinition) => 'cert' in caDefinition
? { cert: caDefinition.cert.toString('utf8') } // Stringify in case of buffers
: caDefinition),
additionalTrustedCAs: proxyConfig.additionalTrustedCAs?.map((caDefinition) => 'cert' in caDefinition
? { cert: caDefinition.cert.toString('utf8') } // Stringify in case of buffers
: caDefinition)
};
}
}
function deserializeProxyConfig(proxyConfig, channel, ruleParams) {
if (_.isString(proxyConfig)) {
const callbackId = proxyConfig;
const proxyConfigCallback = async (options) => {
return await channel.request(callbackId, options);
};
return proxyConfigCallback;
}
else if (_.isArray(proxyConfig)) {
return proxyConfig.map((config) => deserializeProxyConfig(config, channel, ruleParams));
}
else {
return ensureParamsDeferenced(proxyConfig, ruleParams);
}
}
//# sourceMappingURL=serialization.js.map