Skip to content

Commit

Permalink
OpenAPI Extension Annotations (#51)
Browse files Browse the repository at this point in the history
* adding the support for extension annotation object

* added test case

* Update CHANGELOG.md

* Update csdl2openapi.js

* Update openapi.test.js

* Update openapi.test.js

* adding schema and enum check

* Update openapi.test.js

* Update openapi.test.js

* Update openapi.test.js
  • Loading branch information
RoshniNaveenaS authored Dec 1, 2024
1 parent 8e981f2 commit 2a4934c
Show file tree
Hide file tree
Showing 3 changed files with 181 additions and 1 deletion.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
The format is based on [Keep a Changelog](http://keepachangelog.com/).

## Version 1.1.0 - tbd

## Version 1.1.0 - tbd
### Added

- Now supports `x-sap` extensions using `@OpenAPI.Extensions` annotations in service, entity and function/action level.

### Fixed

Expand Down
125 changes: 125 additions & 0 deletions lib/compile/csdl2openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,107 @@ module.exports.csdl2openapi = function (
if (externalDocs && Object.keys(externalDocs).length > 0) {
openapi.externalDocs = externalDocs;
}
const extensions = getExtensions(csdl, 'root');
if (extensions && Object.keys(extensions).length > 0) {
Object.assign(openapi, extensions);
}

// function to read @OpenAPI.Extensions and get them in the generated openAPI document
function getExtensions(csdl, level) {
let extensionObj = {};
let containerSchema = {};
if (level ==='root'){
const namespace = csdl.$EntityContainer ? nameParts(csdl.$EntityContainer).qualifier : null;
containerSchema = csdl.$EntityContainer ? csdl[namespace] : {};
}
else if(level === 'schema' || level === 'operation'){
containerSchema = csdl;
}

for (const [key, value] of Object.entries(containerSchema)) {
if (key.startsWith('@OpenAPI.Extensions')) {
const annotationProperties = key.split('@OpenAPI.Extensions.')[1];
const keys = annotationProperties.split('.');
if (!keys[0].startsWith("x-sap-")) {
keys[0] = (keys[0].startsWith("sap-") ? "x-" : "x-sap-") + keys[0];
}
if (keys.length === 1) {
extensionObj[keys[0]] = value;
} else {
nestedAnnotation(extensionObj, keys[0], keys, value);
}
}
}
let extensionEnums = {
"x-sap-compliance-level": {allowedValues: ["sap:base:v1", "sap:core:v1", "sap:core:v2" ] } ,
"x-sap-api-type": {allowedValues: [ "ODATA", "ODATAV4", "REST" , "SOAP"] },
"x-sap-direction": {allowedValues: ["inbound", "outbound", "mixed"] , default : "inbound" },
"x-sap-dpp-entity-semantics": {allowedValues: ["sap:DataSubject", "sap:DataSubjectDetails", "sap:Other"] },
"x-sap-dpp-field-semantics": {allowedValues: ["sap:DataSubjectID", "sap:ConsentID", "sap:PurposeID", "sap:ContractRelatedID", "sap:LegalEntityID", "sap:DataControllerID", "sap:UserID", "sap:EndOfBusinessDate", "sap:BlockingDate", "sap:EndOfRetentionDate"] },
};
checkForExtentionEnums(extensionObj, extensionEnums);

let extenstionSchema = {
"x-sap-stateInfo": ['state', 'deprecationDate', 'decomissionedDate', 'link'],
"x-sap-ext-overview": ['name', 'values'],
"x-sap-deprecated-operation" : ['deprecationDate', 'successorOperationRef', "successorOperationId"],
"x-sap-odm-semantic-key" : ['name', 'values'],
};

checkForExtentionSchema(extensionObj, extenstionSchema);
return extensionObj;
}

function checkForExtentionEnums(extensionObj, extensionEnums){
for (const [key, value] of Object.entries(extensionObj)) {
if(extensionEnums[key] && extensionEnums[key].allowedValues && !extensionEnums[key].allowedValues.includes(value)){
if(extensionEnums[key].default){
extensionObj[key] = extensionEnums[key].default;
}
else{
delete extensionObj[key];
}
}
}
}

function checkForExtentionSchema(extensionObj, extenstionSchema) {
for (const [key, value] of Object.entries(extensionObj)) {
if (extenstionSchema[key]) {
if (Array.isArray(value)) {
extensionObj[key] = value.filter((v) => extenstionSchema[key].includes(v));
} else if (typeof value === "object" && value !== null) {
for (const field in value) {
if (!extenstionSchema[key].includes(field)) {
delete extensionObj[key][field];
}
}
}
}
}
}


function nestedAnnotation(resObj, openapiProperty, keys, value) {
if (resObj[openapiProperty] === undefined) {
resObj[openapiProperty] = {};
}

let node = resObj[openapiProperty];

// traverse the annotation property and define the objects if they're not defined
for (let nestedIndex = 1; nestedIndex < keys.length - 1; nestedIndex++) {
const nestedElement = keys[nestedIndex];
if (node[nestedElement] === undefined) {
node[nestedElement] = {};
}
node = node[nestedElement];
}

// set value annotation property
node[keys[keys.length - 1]] = value;
}

if (!csdl.$EntityContainer) {
delete openapi.servers;
delete openapi.tags;
Expand Down Expand Up @@ -1466,6 +1566,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
: response(204, "Success", undefined, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses),
}
};
const actionExtension = getExtensions(overload, 'operation');
if (Object.keys(actionExtension).length > 0) {
Object.assign(pathItem.post, actionExtension);
}
const description = actionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription];
if (description) pathItem.post.description = description;
if (prefixParameters.length > 0) pathItem.post.parameters = [...prefixParameters];
Expand Down Expand Up @@ -1576,6 +1680,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
responses: response(200, "Success", overload.$ReturnType, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses),
}
};
const functionExtension = getExtensions(overload, 'operation');
if (Object.keys(functionExtension).length > 0) {
Object.assign(pathItem.get, functionExtension);
}
const iDescription = functionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription];
if (iDescription) pathItem.get.description = iDescription;
customParameters(pathItem.get, overload[voc.Capabilities.OperationRestrictions] || {});
Expand Down Expand Up @@ -1759,6 +1867,23 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
break;
}
}

// Add @OpenAPI.Extensions at entity level to schema object
Object.keys(csdl).filter(name => isIdentifier(name)).forEach(namespace => {
const schema = csdl[namespace];
Object.keys(schema).filter(name => isIdentifier(name)).forEach(name => {
const type = schema[name];
if (type.$Kind === 'EntityType' || type.$Kind === 'ComplexType') {
const schemaName = namespace + "." + name + SUFFIX.read;
const extensions = getExtensions(type, 'schema');
if (Object.keys(extensions).length > 0) {
unordered[schemaName] = unordered[schemaName] || {};
Object.assign(unordered[schemaName], extensions);
}
}
});
});

const ordered = {};
for (const name of Object.keys(unordered).sort()) {
ordered[name] = unordered[name];
Expand Down
52 changes: 52 additions & 0 deletions test/lib/compile/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -424,4 +424,56 @@ service CatalogService {
expect(openAPI.externalDocs.url).toBe('https://help.sap.com/docs/product/123.html');
}
);

test('OpenAPI annotations: @OpenAPI.Extensions annotation is added to the openapi document', () => {
const csn = cds.compile.to.csn(`
namespace sap.OpenAPI.test;
@OpenAPI.Extensions: {
![compliance-level]: 'sap:base:v1',
![x-sap-ext-overview]: {
name : 'Communication Scenario',
values: {
text : 'Planning Calendar API Integration',
format: 'plain'
}
}
}
service A {
@OpenAPI.Extensions: {
![dpp-is-potentially-sensitive]: 'true'
}
entity E1 {
key id: String(4);
oid: String(128);
}
@OpenAPI.Extensions: {
![x-sap-operation-intent]: 'read-collection for function',
![sap-deprecated-operation] : {
deprecationDate: '2022-12-31',
successorOperationId: 'successorOperation',
notValidKey: 'notValidValue'
}
}
function F1(param: String) returns String;
@OpenAPI.Extensions: {
![x-sap-operation-intent]: 'read-collection for action'
}
action A1(param: String) returns String;
}`);
const openAPI = toOpenApi(csn);
expect(openAPI).toBeDefined();
expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1');
expect(openAPI['x-sap-ext-overview'].name).toBe('Communication Scenario');
expect(openAPI['x-sap-ext-overview'].values.text).toBe('Planning Calendar API Integration');
expect(openAPI['x-sap-ext-overview'].values.format).toBe('plain');
expect(openAPI.components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"]).toBe('true');
expect(openAPI.paths["/F1"].get["x-sap-operation-intent"]).toBe('read-collection for function');
expect(openAPI.paths["/A1"].post["x-sap-operation-intent"]).toBe('read-collection for action');
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31');
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId).toBe('successorOperation');
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].notValidKey).toBeUndefined();
});
});

0 comments on commit 2a4934c

Please sign in to comment.