// eslint-disable-next-line @typescript-eslint/triple-slash-reference
///
import * as path from 'path';
import * as fs from 'fs';
import * as util from 'util';
import * as mkdirp from 'mkdirp';
import axios from 'axios';
import type {
Action,
ComplexType,
CSDL,
Function,
EnumType,
SchemaElement,
SchemaWrapper,
Term,
TypeDefinition
} from '@sap-ux/vocabularies/CSDL';
const writeFile = util.promisify(fs.writeFile);
const vocabularyConfig = JSON.parse(fs.readFileSync(path.join(__dirname, './config.json')).toString('utf-8')) || {};
const DEFAULT_KEYS = ['$Version', '$Reference'];
const ANNOTATION_TARGETS = [
'EntityContainer',
'Schema',
'Reference',
'EntityType',
'EnumType',
'ComplexType',
'Property',
'NavigationProperty',
'TypeDefinition',
'Term',
'Parameter',
'ReturnType',
'EntitySet',
'Singleton',
'ActionImport',
'FunctionImport',
'Action',
'Function',
'Include',
'Annotation',
'Collection',
'Record',
'PropertyValue'
];
export const getVocabularyFile = async (url: string): Promise => {
try {
const response = await axios.get(url);
return response.data;
} catch (error) {
throw error;
}
};
/**
* Prepare the vocabularies by including the references and some known used types.
*
* @param vocabularies the vocabulary json data
* @returns a map of vocabularies
*/
function prepareVocabularies(vocabularies: VocabularyObject[]): EnrichedVocabularyMap {
const allVocabularies: EnrichedVocabularyMap = {};
vocabularies.forEach((vocabularyInfo) => {
allVocabularies[vocabularyInfo.name] = {
content: vocabularyInfo.content,
alias: vocabularyInfo.name,
references: [],
namespace: ''
};
if (vocabularyInfo.content.$Reference !== undefined) {
const vocabularyReferences = vocabularyInfo.content.$Reference;
const references: VocabularyReference[] = [];
Object.keys(vocabularyReferences).forEach((referenceName: string) => {
const referenceInfo = vocabularyReferences[referenceName];
if (referenceInfo.$Include) {
referenceInfo.$Include.forEach((includeDefinition) => {
const { $Alias: alias = '', $Namespace: namespace = '' } = { ...includeDefinition };
references.push({ alias: alias, namespace: namespace });
});
}
});
if (vocabularyInfo.name === 'Common') {
references.push({ alias: 'Analytics', namespace: 'com.sap.vocabularies.Analytics.v1' });
}
if (vocabularyInfo.name === 'UI') {
references.push({ alias: 'Analytics', namespace: 'com.sap.vocabularies.Analytics.v1' });
references.push({ alias: 'Aggregation', namespace: 'Org.OData.Aggregation.V1' });
}
allVocabularies[vocabularyInfo.name].references = references;
}
Object.keys(vocabularyInfo.content).forEach((vocabularyKey) => {
if (!DEFAULT_KEYS.includes(vocabularyKey)) {
allVocabularies[vocabularyInfo.name].namespace = vocabularyKey;
}
});
});
return allVocabularies;
}
type VocabularyObject = {
name: string;
content: CSDL;
};
type VocabularyReference = {
alias: string;
namespace: string;
};
type EnrichedVocabularyObject = {
alias: string;
content: CSDL;
namespace: string;
references: VocabularyReference[];
};
type EnrichedVocabularyMap = { [key: string]: EnrichedVocabularyObject };
const schemaElementRegexp = /^[^$@]*$/;
/**
* @param schemaElementKey the key of the schema element
* @param schemaElement the schema element to test
* @returns true if the element is a schema element
*/
function isSchemaElement(
schemaElementKey: string,
schemaElement: SchemaElement | Action[] | Function[]
): schemaElement is SchemaElement {
return schemaElementRegexp.test(schemaElementKey);
}
/**
* @param schemaElement the schema element to evaluate
* @returns true if the element is an Enum
*/
function isEnumType(schemaElement: SchemaElement): schemaElement is EnumType {
return schemaElement && (schemaElement as EnumType).$Kind === 'EnumType';
}
/**
* @param schemaElement the schema element to evaluate
* @returns true if the element is a TypeDefinition
*/
function isSimpleType(schemaElement: SchemaElement): schemaElement is TypeDefinition {
return schemaElement && (schemaElement as TypeDefinition).$Kind === 'TypeDefinition';
}
/**
* Retrieve and output the @Core.Description from the element.
*
* @param schemaElement the schema element to evaluate
* @returns the current element description
*/
function getDescription(schemaElement: ComplexType | TypeDefinition | Term | EnumType | Action | Function) {
if (schemaElement['@Core.Description']) {
return `/**\n ${schemaElement['@Core.Description']}\n*/\n`;
} else {
return '';
}
}
/**
* Unalias a string from all its vocabulary reference (from aliased name to full name).
*
* @param referenceMap a map of all existing vocabularies
* @param aliasedValue the string containing aliased values
* @returns the unaliased value of the vocabulary name
*/
function unalias(referenceMap: any, aliasedValue: string | undefined): string | undefined {
if (!aliasedValue) {
return aliasedValue;
}
const [alias, ...value] = aliasedValue.split('.');
const reference = referenceMap[alias];
if (reference) {
return `${reference}.${value.join('.')}`;
} else if (aliasedValue.indexOf('@') !== -1) {
const [preAlias, ...postAlias] = aliasedValue.split('@');
return `${preAlias}@${unalias(referenceMap, postAlias.join('@'))}`;
} else {
return aliasedValue;
}
}
/**
* Return only the type name in case the type is declared in this file with this alias (ie: Core.Description -> Description in the Core file).
*
* @param typeValue the type to format
* @param currentAlias the alias from the current vocabulary
* @returns the formatted type
*/
function formatType(typeValue: string, currentAlias: string) {
const typeSplit = typeValue.split('.');
if (typeSplit.length > 1 && typeSplit[0] === currentAlias) {
return typeSplit[1];
} else {
return typeValue;
}
}
/**
* Generates the TS types for the vocabularies into the given path.
*
* @param targetFolder the target folder for the types
*/
async function generateTypes(targetFolder: string) {
mkdirp.sync(targetFolder);
const vocabularyPromises = Object.keys(vocabularyConfig).map(async (vocabularyName) => {
const vocabularyContent = await getVocabularyFile(vocabularyConfig[vocabularyName]);
return { name: vocabularyName, content: vocabularyContent };
});
const vocabularies: VocabularyObject[] = await Promise.all(vocabularyPromises);
const allVocabularies: EnrichedVocabularyMap = prepareVocabularies(vocabularies);
const references: any = {};
const allTermsToTypes: any = {};
Object.keys(allVocabularies).forEach((vocabularyAlias) => {
references[vocabularyAlias] = allVocabularies[vocabularyAlias].namespace;
});
for (const vocabularyAlias in allVocabularies) {
const vocabularyData = allVocabularies[vocabularyAlias];
const vocabularyNamespace = vocabularyData.namespace;
const vocabularyNamespaceTrans = vocabularyData.namespace.replace(/\./g, '_');
const compositesAnnotations: any = ANNOTATION_TARGETS.reduce(
(compositesAnnotations: any, annotationTargetName) => {
compositesAnnotations[annotationTargetName] = [];
return compositesAnnotations;
},
{}
);
let vocabularyEdmDef: string = '';
let vocabularyDef: string = '';
vocabularyData.references.forEach((reference) => {
vocabularyDef += `import * as ${reference.alias} from "./${reference.alias}";\n`;
});
// vocabularyDef += `import { AnnotationTerm, PropertyValue, EnumValue } from "@sap/ux/vocabularies/Edm";\n`;
vocabularyDef += 'import * as Edm from "../Edm";\n';
vocabularyDef += 'import AnnotationTerm = Edm.AnnotationTerm;\n';
vocabularyDef += 'import PropertyAnnotationValue = Edm.PropertyAnnotationValue;\n';
vocabularyDef += 'import EnumValue = Edm.EnumValue;\n';
vocabularyDef += 'import ComplexType = Edm.RecordComplexType;\n';
// vocabularyDef += " import AnnotationRecord = Edm.AnnotationRecord;\n";
vocabularyDef += '\n';
const vocabularyDefinition: SchemaWrapper = vocabularyData.content[vocabularyData.namespace];
const typeInheritances: any = {};
const vocabularyTerms: any = {};
const vocabularyTypes: any = {};
Object.keys(vocabularyDefinition).forEach((vocabularyTerm) => {
const vocabularyTermInfo: SchemaElement | Action[] | Function[] = vocabularyDefinition[vocabularyTerm];
if (isSchemaElement(vocabularyTerm, vocabularyTermInfo)) {
vocabularyDef += '\n';
switch (vocabularyTermInfo.$Kind) {
case 'TypeDefinition':
vocabularyDef += `\n// Typedefinition \n`;
vocabularyDef += getDescription(vocabularyTermInfo);
vocabularyDef += `export type ${vocabularyTerm} = ${formatType(
vocabularyTermInfo.$UnderlyingType,
vocabularyAlias
)}`;
vocabularyDef += `;`;
break;
case 'Term':
vocabularyDef += `\n// Term \n`;
vocabularyDef += getDescription(vocabularyTermInfo);
const termType = formatType(vocabularyTermInfo.$Type || 'Edm.String', vocabularyAlias);
(vocabularyTermInfo.$AppliesTo || Object.keys(compositesAnnotations)).forEach(
(target: string) => {
compositesAnnotations[target].push(
// Switch from FQDN to not FQDN
// `\n '@${vocabularyNamespace}.${vocabularyTerm}'?: ${vocabularyTerm};`
`\n '${vocabularyTerm}'?: ${vocabularyNamespaceTrans}.${vocabularyTerm};`
);
}
);
let [termAlias, termName] = termType.split('.');
if (termName === undefined) {
termName = termAlias;
termAlias = vocabularyAlias;
}
let renamedTermType = `${termType}`;
const targetTerm = vocabularyDefinition[termName];
if (
termAlias === vocabularyAlias &&
(!isSchemaElement(termName, targetTerm) ||
(targetTerm.$Kind !== 'TypeDefinition' && !isEnumType(targetTerm)))
) {
renamedTermType += `Types`;
}
if (renamedTermType === 'Edm.AnnotationPath') {
renamedTermType = 'Edm.AnnotationPath';
}
if (vocabularyTermInfo.$Collection) {
renamedTermType += `[]`;
}
//let needNewLine = false;
if (isSchemaElement(termName, targetTerm) && isEnumType(targetTerm)) {
vocabularyDef += `export type ${vocabularyTerm} = EnumValue<${renamedTermType}>`;
//needNewLine = true;
} else if (
renamedTermType === 'Core.Tag' ||
renamedTermType.startsWith('Edm.String') ||
(isSchemaElement(termName, targetTerm) && isSimpleType(targetTerm))
) {
vocabularyDef += `export type ${vocabularyTerm} = { term : ${vocabularyAlias}AnnotationTerms.${vocabularyTerm}} & AnnotationTerm>`;
} else {
vocabularyDef += `export type ${vocabularyTerm} = { term : ${vocabularyAlias}AnnotationTerms.${vocabularyTerm} } & AnnotationTerm<${renamedTermType}>`;
}
// if (vocabularyTermInfo.$Nullable) {
// if (needNewLine) {
// vocabularyDef += `\n `;
// }
// vocabularyDef += ` | null`;
// }
vocabularyTerms[vocabularyTerm] = true;
vocabularyDef += `;`;
break;
case 'ComplexType':
vocabularyDef += `\n// ComplexType \n`;
vocabularyDef += getDescription(vocabularyTermInfo);
if (!typeInheritances[vocabularyTerm]) {
typeInheritances[vocabularyTerm] = [];
}
vocabularyDef += `export type ${vocabularyTerm} = ComplexType & `;
if (vocabularyTermInfo.$BaseType) {
const baseType = formatType(vocabularyTermInfo.$BaseType, vocabularyAlias);
if (!typeInheritances[baseType]) {
typeInheritances[baseType] = [];
}
typeInheritances[baseType].push(vocabularyTerm);
vocabularyDef += `Omit<${baseType}, '$Type'> & `;
}
vocabularyDef += `{\n`;
vocabularyDef += ` $Type: ${vocabularyAlias}AnnotationTypes.${vocabularyTerm};\n`;
vocabularyTypes[vocabularyTerm] = `${vocabularyNamespace}.${vocabularyTerm}`;
Object.keys(vocabularyTermInfo).forEach((vocabularyTermKey) => {
if (vocabularyTermInfo[vocabularyTermKey]['@Core.Description']) {
vocabularyDef += ` /**\n ${vocabularyTermInfo[vocabularyTermKey]['@Core.Description']}\n */\n`;
}
if (vocabularyTermKey[0] != '@' && vocabularyTermKey[0] != '$') {
let keyType = formatType(
vocabularyTermInfo[vocabularyTermKey].$Type || 'Edm.String',
vocabularyAlias
);
vocabularyDef += ` ${vocabularyTermKey}`;
if (vocabularyTermInfo[vocabularyTermKey].$Nullable) {
vocabularyDef += `?`;
}
const targetTerm = vocabularyDefinition[keyType];
if (
keyType.startsWith('Edm.') &&
keyType !== 'Edm.AnnotationPath' &&
keyType !== 'Edm.PropertyPath' &&
keyType !== 'Edm.NavigationPropertyPath'
) {
if (vocabularyTermInfo[vocabularyTermKey].$Collection) {
keyType += `[]`;
}
keyType = `PropertyAnnotationValue<${keyType}>`;
} else if (
targetTerm &&
isSchemaElement(keyType, targetTerm) &&
targetTerm.$Kind === 'ComplexType'
) {
keyType += `Types`;
if (vocabularyTermInfo[vocabularyTermKey].$Collection) {
keyType += `[]`;
}
keyType = `${keyType}`;
} else {
if (keyType === 'Edm.AnnotationPath') {
if (vocabularyTermInfo[vocabularyTermKey]['@Validation.AllowedTerms']) {
keyType = `Edm.AnnotationPath<${(
vocabularyTermInfo[vocabularyTermKey]['@Validation.AllowedTerms'] as any
)
.map((type: string) => {
return formatType(type, vocabularyAlias);
})
.join('|')}>`;
} else {
keyType = 'Edm.AnnotationPath';
}
}
if (vocabularyTermInfo[vocabularyTermKey].$Collection) {
keyType += `[]`;
}
}
vocabularyDef += `: ${keyType}`;
// if (vocabularyTermInfo[vocabularyTermKey].$Nullable) {
// vocabularyDef += ` | null`;
// }
vocabularyDef += ';\n';
if (
vocabularyTermKey === 'Action' &&
(keyType === 'Common.QualifiedName' || keyType === 'Common.ActionOverload')
) {
vocabularyDef += `\n ActionTarget?: Edm.Action;\n`;
}
}
});
vocabularyDef += `}\n`;
break;
case 'EnumType':
vocabularyDef += `\n// EnumType \n`;
vocabularyDef += getDescription(vocabularyTermInfo);
vocabularyDef += `export const enum ${vocabularyTerm} {\n`;
let hasPreviousEnum = false;
Object.keys(vocabularyTermInfo).forEach((vocabularyTermKey) => {
if (
vocabularyTermKey[0] != '@' &&
vocabularyTermKey[0] != '$' &&
vocabularyTermKey.indexOf('@') === -1
) {
if (hasPreviousEnum) {
vocabularyDef += ',';
vocabularyDef += '\n';
}
if (vocabularyTermInfo[vocabularyTermKey + '@Core.Description']) {
vocabularyDef += ` /**\n ${
vocabularyTermInfo[vocabularyTermKey + '@Core.Description']
}\n */\n`;
}
if (vocabularyTermInfo[vocabularyTermKey + '@Core.Description']) {
vocabularyDef += ` /**\n ${
vocabularyTermInfo[vocabularyTermKey + '@Core.LongDescription']
}\n */\n`;
}
vocabularyDef += ` ${vocabularyTermKey} = "${vocabularyAlias}.${vocabularyTerm}/${vocabularyTermKey}"`;
hasPreviousEnum = true;
}
});
vocabularyDef += '\n';
vocabularyDef += `}\n`;
break;
default:
if (Array.isArray(vocabularyTermInfo)) {
vocabularyTermInfo.forEach((vocabularyTermDetail: Action | Function) => {
vocabularyDef += getDescription(vocabularyTermDetail);
vocabularyDef += `export type ${vocabularyTerm} = (`;
if (vocabularyTermDetail.$Parameter) {
vocabularyTermDetail.$Parameter.forEach((parameterDetail, index: number) => {
if (index > 0) {
vocabularyDef += ', ';
}
vocabularyDef += `${parameterDetail.$Name}`;
if (parameterDetail.$Type) {
vocabularyDef += `: ${parameterDetail.$Type}`;
} else {
vocabularyDef += `: any`;
}
});
}
vocabularyDef += `) => `;
if (vocabularyTermDetail.$ReturnType) {
vocabularyDef += `${vocabularyTermDetail.$ReturnType.$Type}`;
} else {
vocabularyDef += `void`;
}
});
} else {
console.log('Unsupported Type ' + JSON.stringify(vocabularyTermInfo));
}
break;
}
}
});
vocabularyDef += '\n';
vocabularyDef += `export const enum ${vocabularyAlias}AnnotationTerms {\n`;
Object.keys(vocabularyTerms).forEach((termName) => {
vocabularyDef += ` ${termName} = "${vocabularyNamespace}.${termName}",\n`;
});
vocabularyDef += '}';
vocabularyDef += '\n';
vocabularyDef += `export const enum ${vocabularyAlias}AnnotationTypes {\n`;
Object.keys(vocabularyTypes).forEach((typeName) => {
vocabularyDef += ` ${typeName} = "${vocabularyTypes[typeName]}",\n`;
});
Object.keys(vocabularyTerms).forEach((termName) => {
const targetType = unalias(references, (vocabularyDefinition[termName] as Term).$Type);
if (targetType) {
allTermsToTypes[`${vocabularyNamespace}.${termName}`] = targetType;
}
});
vocabularyDef += '}\n';
vocabularyDef += '\n';
vocabularyDef += '\n';
vocabularyDef += '\n';
Object.keys(typeInheritances).forEach((baseType) => {
let localVocDefinition = vocabularyDefinition;
const originalBaseType = baseType;
if (baseType.indexOf('.') !== -1) {
const [baseVoc, basebaseType] = baseType.split('.');
localVocDefinition = allVocabularies[baseVoc].content[allVocabularies[baseVoc].namespace];
baseType = basebaseType;
}
if (!(localVocDefinition[baseType] as ComplexType).$Abstract) {
vocabularyDef += `export type ${baseType}Types = ${originalBaseType}`;
if (typeInheritances[originalBaseType].length > 0) {
vocabularyDef += ` | ${typeInheritances[originalBaseType]
.map((type: string) => type + 'Types')
.join(' | ')}`;
}
vocabularyDef += ';\n';
} else if (typeInheritances[originalBaseType].length > 0) {
vocabularyDef += `export type ${originalBaseType}Types = ${typeInheritances[originalBaseType]
.map((type: string) => type + 'Types')
.join(' | ')}`;
vocabularyDef += ';\n';
}
});
vocabularyEdmDef = `import * as ${vocabularyNamespaceTrans} from "./${vocabularyAlias}";\n\n`;
Object.keys(compositesAnnotations).forEach((targetKey) => {
let compositeTarget = `\n// Type containing all possible annotations to use for ${targetKey}\nexport interface ${targetKey}Annotations_${vocabularyAlias} {`;
compositeTarget += compositesAnnotations[targetKey].join('');
compositeTarget += `\n}`;
compositeTarget += '\n';
// compositeTarget += `\n// Type containing all possible annotations to use for ${targetKey}\n export interface ${targetKey}Annotations {`;
// compositeTarget += `\n ${vocabularyAlias}?: ${targetKey}Annotations_${vocabularyAlias}`;
// compositeTarget += `\n}`;
vocabularyEdmDef += '\n';
vocabularyEdmDef += compositeTarget;
});
await writeFile(path.join(targetFolder, `${vocabularyAlias}.ts`), vocabularyDef);
await writeFile(path.join(targetFolder, `${vocabularyAlias}_Edm.ts`), vocabularyEdmDef);
}
let edmTypesValue = ``;
Object.keys(allVocabularies).map((vocabularyAlias) => {
edmTypesValue += `import * as ${vocabularyAlias} from "./${vocabularyAlias}_Edm";\n`;
});
edmTypesValue += '\n';
ANNOTATION_TARGETS.forEach((annotationTarget) => {
edmTypesValue += `export type ${annotationTarget}Annotations = {\n`;
Object.keys(allVocabularies).map((vocabularyAlias) => {
edmTypesValue += ` ${vocabularyAlias}?: ${vocabularyAlias}.${annotationTarget}Annotations_${vocabularyAlias};\n`;
});
edmTypesValue += `}\n;`;
});
let edmTermToTypes = '';
edmTermToTypes += `export enum TermToTypes {\n`;
Object.keys(allTermsToTypes).forEach((termName) => {
edmTermToTypes += ` "${termName}" = "${allTermsToTypes[termName]}",\n`;
});
edmTermToTypes += '}';
await writeFile(path.join(targetFolder, `Edm_Types.ts`), edmTypesValue);
await writeFile(path.join(targetFolder, `TermToTypes.ts`), edmTermToTypes);
}
generateTypes(path.join(__dirname, '../src/vocabularies'))
.then(() => {
console.log('File generated successfully');
})
.catch((e) => {
console.error('Error while generating vocabularies', e);
});