backendless-coderunner
Version:
Backendless CodeRunner for Node.js
229 lines (178 loc) • 6.41 kB
JavaScript
const xml = require('xml')
const NAMESPACE = 'services'
const typesMapping = {
STRING : { javaType: 'java.lang.String' },
BOOLEAN: { javaType: 'boolean', nativeType: 'bool' },
NUMBER : { javaType: 'float', nativeType: 'float' },
ARRAY : { javaType: generic('javaType', 'java.util.List'), nativeType: generic('nativeType', 'List') },
OBJECT : { javaType: 'java.lang.Object' },
DATE : { javaType: 'java.util.Date', nativeType: 'DateTime' }
}
function packaged(name) {
return `${ NAMESPACE }.${ name }`
}
function generic(nature, type) {
return function(elementType) {
return elementType ? `${ type }<${ elementType[nature] }>` : type
}
}
function unwrapPromise(type) {
return type && type.name === 'Promise' ? unwrapPromise(type.elementType) : type
}
function getTypeMapping(type, definitions) {
type = unwrapPromise(type) || { name: 'void' }
function resolve(nature, withinNamespace) {
let result = mapped && mapped[nature]
if (typeof result === 'function') {
result = result(mappedSubType)
}
result = result || type.name
if (definitions[result] && withinNamespace) {
result = packaged(result)
}
return result
}
const mapped = typesMapping[type.name.toUpperCase()]
const mappedSubType = type.elementType && getTypeMapping(type.elementType, definitions)
return {
type : resolve('type'),
javaType : resolve('javaType', true),
nativeType : resolve('nativeType', true),
fullType : resolve('fullType', true),
elementType: mappedSubType && mappedSubType.type
}
}
const HTTP_METHODS = ['GET', 'PUT', 'POST', 'DELETE']
const isValidHttpMethod = value => HTTP_METHODS.indexOf(value) !== -1
/**
* Enriches service method node by adding 'method', 'route', 'description', 'operationName',
* and 'systemNonBillableRequest' attributes.
* @param {Object} methodNode
* @param {Object} method
* @returns {Object} methodNode
*/
function enrichMethodNode(methodNode, method) {
methodNode.description = method.description || ''
methodNode.operationName = method.tags.operationname || methodNode.name
methodNode.registerAs = method.tags.registeras || ''
methodNode.category = method.tags.category || ''
if (method.tags.executiontimeoutinseconds) {
const executionTimeoutInSeconds = Number(method.tags.executiontimeoutinseconds)
if (!isNaN(executionTimeoutInSeconds)) {
methodNode.executionTimeoutInSeconds = executionTimeoutInSeconds
}
}
if (method.tags.systemnonbillablerequest) {
methodNode.systemNonBillableRequest = true
}
if (methodNode.registerAs !== 'SYSTEM') {
methodNode.appearanceIcon = method.tags.appearanceicon || ''
methodNode.appearanceColor = method.tags.appearancecolor || ''
method.metaInfo.sampleResult = method.tags.sampleresult ? JSON.parse(method.tags.sampleresult) : null
method.metaInfo.sampleResultLoader = method.tags.sampleresultloader
? JSON.parse(method.tags.sampleresultloader)
: null
}
methodNode.metaInfo = JSON.stringify(method.metaInfo)
if (method.tags.route) {
let httpPath = method.tags.route
let httpMethod = 'GET'
const sepPos = httpPath.indexOf(' ')
if (sepPos !== -1) {
httpMethod = httpPath.substr(0, sepPos).toUpperCase()
httpPath = httpPath.substr(sepPos + 1)
if (!isValidHttpMethod(httpMethod)) {
throw new Error(`Unsupported HTTP method [${ httpMethod }] for ${ method.name } method`)
}
}
methodNode.method = httpMethod
methodNode.path = httpPath
}
return methodNode
}
/**
* @param {Array.<String>} services
* @param {Object.<String, Object>} definitions
* @returns {*}
*/
function buildServicesXml(services, definitions) {
const nsNode = [{ _attr: { name: NAMESPACE, fullname: NAMESPACE } }]
const runtimeNode = [{ _attr: { generationMode: 'FULL' } }]
const types = new Set()
function registerType(type) {
if (type && definitions[type] && !types.has(type)) {
types.add(type)
nsNode.push(dataTypeNode(definitions[type]))
}
}
function typedNode(name, type) {
const mapping = getTypeMapping(type, definitions)
const result = {
name : name,
type : mapping.type,
nativetype: mapping.nativeType,
fulltype : mapping.fullType,
javatype : mapping.javaType
}
mapping.elementType && (result.elementType = mapping.elementType)
registerType(result.type)
registerType(result.elementType)
return result
}
function dataTypeNode(definition) {
const attributes = {
name : definition.name,
description : definition.description || definition.name,
fullname : packaged(definition.name),
typeNamespace: NAMESPACE
}
const datatype = []
datatype.push({ _attr: attributes })
Object.keys(definition.properties).forEach(propName => {
const node = typedNode(propName, definition.properties[propName].type)
datatype.push({ field: { _attr: node } })
})
return { datatype: datatype }
}
services.forEach(function(serviceName) {
const serviceDef = definitions[serviceName]
const serviceNode = [{
_attr: {
name : serviceName,
description: (serviceDef && serviceDef.description) || serviceName,
fullname : packaged(serviceName),
namespace : NAMESPACE
}
}]
nsNode.push({ service: serviceNode })
if (serviceDef) {
Object.keys(serviceDef.methods).forEach(name => {
const method = serviceDef.methods[name]
if (method.access !== 'private') {
const methodNode = [{
_attr: enrichMethodNode(typedNode(name, method.returnType), method)
}]
method.params.forEach(param => {
const argNode = typedNode(param.name, param.type)
argNode.required = !param.optional
if (param.description) {
argNode.description = param.description
}
methodNode.push({
arg: {
_attr: argNode
}
})
})
serviceNode.push({ method: methodNode })
}
})
}
})
return xml({ namespaces: [{ namespace: nsNode }, { runtime: runtimeNode }] }, {
declaration: { encoding: 'UTF-8' },
indent : ' '
})
}
exports.buildXML = buildServicesXml