@sap/cds-dk
Version:
Command line client and development toolkit for the SAP Cloud Application Programming Model
373 lines (322 loc) • 13.4 kB
JavaScript
const { getRegExpGroups } = require('./utils');
const messages = require("../message").getMessages();
module.exports = class ParserContext {
/**
* @constructor
*/
constructor(content) {
this.services = this.getServicesFromContent(content);
}
getTopicName(eventName) {
return eventName.startsWith('ce/') ? eventName.substring(3) : eventName;
}
getEventName(messageRefString) {
const lastSlashIndex = messageRefString.lastIndexOf('/');
return lastSlashIndex == -1 ? '' : messageRefString.substring(lastSlashIndex + 1);
}
/**
* Returns services from the asyncapi json object along with the events and annotations.
* @returns an object with all the services.
*/
getServicesFromContent(content) {
const services = {};
const serviceAnnotations = this.getServiceAnnotations(content);
Object.entries(content.components.messages).forEach(([, value]) => {
let eventName;
// Message name should be used to capture the event name
if (value.name) {
eventName = value.name;
} else {
throw new Error(messages.INVALID_ASYNCAPI_FILE);
}
// uses regex to break event name in different components
const regExpGroups = getRegExpGroups(eventName);
const service = regExpGroups[1] + "." + regExpGroups[2];
const event = regExpGroups[0];
// add service to services entry if not present
if (!(service in services)) {
services[service] = { 'kind': 'service', '@cds.external': 'true', ...serviceAnnotations, 'events': {}, 'types': {} };
}
// add event entry to service
const eventAnnotations = this.getEventAnnotations(value);
// add topic annotation
eventAnnotations['@topic'] = this.getTopicName(eventName);
let properties = {};
let types = {};
if (value.payload) {
// gets the schema being referred to in the event
const eventSchema = this.resolveRef(content, value.payload);
[properties, types] = this.getPropertiesAndTypes(event, eventSchema);
}
services[service].types = types;
services[service].events[event] = { 'kind': 'event', '@cds.external': 'true', ...eventAnnotations, elements: properties };
});
return services;
}
/**
* Returns annotations of a particular service.
* @returns an object with all the annotations as key value pairs.
*/
getServiceAnnotations(content) {
const serviceAnnotationsMapping = {
'x-sap-stateInfo': 'StateInfo', // expected: object
'x-sap-shortText': 'ShortText' // expected: string
};
const infoAnnotationsMapping = {
'title': 'Title', // expected: string
'version': 'SchemaVersion', // expected: string
'description': 'Description' // expected: string
};
const annotations = this.getAnnotations(content, serviceAnnotationsMapping);
if (content.info) {
for (const [key, value] of Object.entries(infoAnnotationsMapping)) {
if (content.info[key] && !(content.info[key] instanceof Object)) {
// info values will always be in string format
annotations['@AsyncAPI.' + value] = content.info[key];
}
}
}
return annotations;
}
/**
* Returns annotations of a particular event. Here the parameter content is event content.
* @returns an object with all the annotations as key value pairs.
*/
getEventAnnotations(content) {
const eventLevelAnnotationsMapping = {
'x-sap-event-spec-version': 'EventSpecVersion', // expected: string
'x-sap-event-source': 'EventSource', // expected: string
'x-sap-event-version': 'EventSchemaVersion', // expected: string
'x-sap-event-source-parameters': 'EventSourceParams', // expected: object
'x-sap-event-characteristics': 'EventCharacteristics', // expected: object
'x-sap-stateInfo': 'EventStateInfo' // expected: object
};
const annotations = this.getAnnotations(content, eventLevelAnnotationsMapping);
return annotations;
}
/**
* Returns validity of the annotation value. Here the parameter
* content is the annotation name and value of the annotation.
* @returns true/false depending on the annotation value.
*/
isValidAnnotationValue(annotation, value) {
const objectTypeAnnotations = ['StateInfo', 'EventSourceParams', 'EventCharacteristics', 'EventStateInfo'];
// if the value is an array or null
if (Array.isArray(value) || value === null) return false;
if (objectTypeAnnotations.includes(annotation) && (value instanceof Object)) {
return true;
} else if ((!objectTypeAnnotations.includes(annotation)) && (typeof value === 'string')) {
return true;
} else {
console.warn(`WARNING: Annotations which are not following the expected type are omitted.`);
return false;
}
}
/**
* Returns annotations for an event/service. Here the parameter content
* is event/service content along with the corresponding mapping.
* @returns an object with the annotations as key value pairs.
*/
getAnnotations(content, annotationsMapping) {
const annotations = {};
for (const [key, value] of Object.entries(content)) {
// case: known annotation
if (annotationsMapping[key] && this.isValidAnnotationValue(annotationsMapping[key], value)) {
annotations['@AsyncAPI.' + annotationsMapping[key]] = value;
}
// case: unkown annotation
else if (key.startsWith('x-')) {
if (!annotations['@AsyncAPI.Extensions']) {
annotations['@AsyncAPI.Extensions'] = {};
}
// removes x- from the element
annotations['@AsyncAPI.Extensions'][key.substring(2)] = value;
}
}
return annotations;
}
/**
* Returns the referred schema. Here the content is the asyncapi
* document and payloadObject is the reference path
* @returns an object with the referred schema.
*/
resolveRef(content, payloadObject) {
let schema = {};
if (payloadObject['$ref']) {
const pathArray = payloadObject['$ref'].split('/');
pathArray.forEach((value) => {
if (value === '#') schema = content;
else if (schema[value]) schema = schema[value];
// in case path doesn't exist in the file
else schema = {};
});
if (!Object.keys(schema).length) {
console.warn(`WARNING: Unable to resolve ref path: ` + payloadObject['$ref']);
}
}
else if (payloadObject instanceof Object) {
// in case ref is not there and schema object is there
schema = payloadObject;
}
return schema;
}
/**
* Returns the properties and types generated, related to an event.
* @returns a properties object and a types object.
*/
getPropertiesAndTypes(eventName, content) {
const properties = {};
let types = {};
if (content.properties) {
for (const [key, value] of Object.entries(content.properties)) {
// will contain type objects generated for a property
let referredTypes = {};
let isRequired = false;
if (content['required']?.includes(key)) isRequired = true;
[properties[key], referredTypes] = this.parseProperty(eventName, key, value, isRequired);
// collecting all the types
types = { ...types, ...referredTypes };
}
}
return [properties, types];
}
/**
* Returns the processed objects for property and type.
* @returns property object and type object.
*/
parseProperty(eventName, propertyName, propertyContent, isRequired) {
let property = {};
let propertyAttributes = {};
let types = {};
if (isRequired === true) {
property['@mandatory'] = true;
}
let type = this.getTypeFromArray(propertyContent.type);
// case where type is defined
if (type && (type === 'object' || type === 'array')) {
// for generation of type: event name + property name
propertyName = propertyName[0].toUpperCase() + propertyName.substring(1);
let typeName = eventName + "." + propertyName;
// generate object for the introduced type
const elements = this.getTypeElements(propertyContent);
types[typeName] = { 'kind': 'type', '@cds.external': 'true', ...elements };
// referring to the generated type from the property
propertyAttributes = { 'type': typeName };
} else {
propertyAttributes = this.getPropertyObject(propertyContent);
}
property = { ...property, ...propertyAttributes };
return [property, types];
}
/**
* Returns generated type for the property.
* @returns an object for a type.
*/
getTypeElements(propertyContent) {
let properties = {};
let type = this.getTypeFromArray(propertyContent.type);
// for nested object
if (type === 'object') {
for (const [key, value] of Object.entries(propertyContent.properties)) {
properties[key] = this.getTypeElements(value);
}
}
// for nested array
else if (type === 'array') {
properties.items = this.getTypeElements(propertyContent.items);
return properties;
}
// base data type mapping
else {
return this.getPropertyObject(propertyContent);
}
return { elements: properties };
}
/**
* Returns type for the property from the AsyncAPI Document.
* @returns the value of type.
*/
getTypeFromArray(typeValue) {
let type;
// if type is an array
if (Array.isArray(typeValue)) {
// one of the value will be null
type = typeValue[0] === 'null' ? typeValue[1] : typeValue[0];
} else {
type = typeValue;
}
return type;
}
/**
* Returns generated object for a single property.
* @returns an object for a property.
*/
getPropertyObject(propertyContent) {
const property = {};
if ('$ref' in propertyContent) {
const refPathParts = propertyContent['$ref'].split('/');
const propertyName = refPathParts[refPathParts.length - 1];
property['type'] = { 'ref': [refPathParts[3], propertyName] };
return property;
}
const inputType = this.getTypeFromArray(propertyContent.type);
const cdsType = this.getDataType(inputType, propertyContent.format);
property['type'] = cdsType;
// case: cds type is decimal
if (cdsType === 'cds.Decimal') {
if (propertyContent['x-sap-precision']) property.precision = propertyContent['x-sap-precision'];
if (propertyContent['x-sap-scale']) property.scale = propertyContent['x-sap-scale'];
}
if (propertyContent['default']) {
property['default'] = { 'val': propertyContent['default'] };
}
if (propertyContent['enum']) {
property['enum'] = this.getEnumObject(propertyContent['enum']);
}
return property;
}
/**
* Takes the enums from the AsyncAPI Document.
* @returns the property object of enums.
*/
getEnumObject(enumArray) {
const enumProperty = {};
enumArray.forEach(val => {
enumProperty[val] = {};
});
return enumProperty;
}
/**
* Takes the type and format from the document and returns the corresponding cds type.
* @returns the corresponding cds type.
*/
getDataType(type, format = null) {
const asyncapiToCdsMapping = {
'number': 'cds.Double',
'boolean': 'cds.Boolean',
'integer': 'cds.Integer'
};
if (type === 'string') {
switch (format) {
case 'int64':
return 'cds.Integer64';
case 'decimal':
return 'cds.Decimal';
case 'uuid':
return 'cds.UUID';
case 'date':
return 'cds.Date';
case 'partial-time':
return 'cds.Time';
case 'date-time':
return 'cds.Timestamp';
default:
return 'cds.LargeString';
}
} else if (asyncapiToCdsMapping[type]) {
return asyncapiToCdsMapping[type];
} else {
throw new Error(messages.UNRESOLVED_TYPE + `'${type}'`);
}
}
}