UNPKG

6-mils

Version:

A JS library for sending, receiving, and parsing cXML messages.

175 lines (146 loc) 5.13 kB
const debug = require('debug')('6-mils:InboundCxmlMessage') const { DateTime } = require('luxon') const { evaluateXPathToString } = require('fontoxpath') const { sync } = require('slimdom-sax-parser') const prettyPrintXml = require('@6-mils/humanize-xml') /** * A collection of private property values for each instance of this class. * @type {WeakMap} */ const _private = new WeakMap() /** * A wrapper for the XPath query engine. This makes it easier to provide * consistent functionality wherever an XPath expression needs to be evaluated. * * @param {String} xpath A valid XPath expression. * * @param {SlimDom} dom The DOM to execute the expression against. * * @return {String} */ function getXmlValue (xpath, dom) { return evaluateXPathToString(xpath, dom) } /** * This base class is intended to provide the basic functionality for parsing * messages received from a supplier. */ class InboundCxmlMessage { constructor (src) { src = src || '' debug('Constructing new instance from source "%s"', src) if (src === '%%EMPTY%%') { /** * Sometimes, a supplier may send an empty HTTP response if there is no * specific data to respond with (for example, an OrderResponse is nothing * more than an acknowledgement, whereas a PunchOutSetupResponse will * contain a URL). In this case, the properties of the incoming message * are all set to reasonable defaults. */ _private.set(this, { src: '', dom: null, payloadId: require('@6-mils/CxmlPayloadId')(), timestamp: DateTime.local().toString(), version: require('@6-mils/CxmlVersion'), statusCode: '200', statusText: 'success' }) } else { const props = {} try { props.dom = sync(src) props.src = src.replace(/>\s+</g, '><').replace(/\s+$/, '') // remove all whitespace between tags and at the end of the document } catch (e) { throw new Error('The constructor requires a value that must be a valid cXML string, or the special value %%EMPTY%%.') } props.payloadId = getXmlValue('/cXML/data(@payloadID)', props.dom) props.timestamp = getXmlValue('/cXML/data(@timestamp)', props.dom) props.version = /\d\.\d+\.\d+/.exec(props.dom.doctype.publicId)[0] props.statusCode = getXmlValue('/cXML/Response/Status/data(@code)', props.dom) if (props.statusCode === '200') { props.statusText = 'success' } else { props.statusText = getXmlValue('/cXML/Response/Status/text()', props.dom) || getXmlValue('/cXML/Response/Status/data(@text)', props.dom) } _private.set(this, props) } } /** * @readonly * @return {String} The value of the "payloadID" attribute of the "cXML" * element. */ get payloadId () { return _private.get(this).payloadId } /** * @readonly * @return {String} The value of the "timestamp" attribute of the "cXML" * element. */ get timestamp () { return _private.get(this).timestamp } /** * @readonly * @return {String} The value of the DTD version used by this cXML message * (obtained from the DOCTYPE element). */ get version () { return _private.get(this).version } /** * @readonly * @return {String} The value of the "code" attribute of the "Status" * element. */ get statusCode () { return _private.get(this).statusCode } /** * @readonly * @return {String} Either the inner text of the "Status" element, or the * value of the "text" attribute, if the former is not * given. */ get statusText () { return _private.get(this).statusText } /** * Executes the specified XPath expression against the XML represented by * this message. * * @param {String} xpath XPath expression. * * @return {String} */ query (xpath) { xpath = xpath || '' if (xpath.length === 0) { throw new Error('The parameter value for "query" is required and must be a string containing a valid XPath expression.') } try { return getXmlValue(xpath, _private.get(this).dom) } catch (e) { if (e.message.match(/xpathExpression must be a string/)) { throw new Error('The parameter value for "query" is required and must be a string containing a valid XPath expression.') } if (e.message.match(/^XPST0003: Unable to parse/)) { throw new Error('The parameter value for "query" is required and must be a string containing a valid XPath expression.') } if (e.message.match(/^XPDY0002: context is absent/)) { throw new Error('There is nothing to query. Call "parse" with valid cXML first, and then call "query".') } throw e } } toString (options) { options = options || {} if (options.format) { return prettyPrintXml(_private.get(this).src) } return _private.get(this).src } } module.exports = InboundCxmlMessage