6-mils
Version:
A JS library for sending, receiving, and parsing cXML messages.
175 lines (146 loc) • 5.13 kB
JavaScript
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