Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAPI Extension Annotations #51

Merged
merged 14 commits into from
Dec 1, 2024
Merged
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ 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

### Added

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

## Version 1.0.7 - 17.10.2024

### Fixed
Expand Down
76 changes: 76 additions & 0 deletions lib/compile/csdl2openapi.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,58 @@ 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('.');
RoshniNaveenaS marked this conversation as resolved.
Show resolved Hide resolved
keys[0] = "x-sap-" + keys[0];
if (keys.length === 1) {
extensionObj[keys[0]] = value;
} else {
nestedAnnotation(extensionObj, keys[0], keys, value);
}
}
}
return extensionObj;
}

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 @@ -1464,6 +1515,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 @@ -1574,6 +1629,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 @@ -1757,6 +1816,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
28 changes: 28 additions & 0 deletions test/lib/compile/openapi.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -391,4 +391,32 @@ describe('OpenAPI export', () => {
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: {
RoshniNaveenaS marked this conversation as resolved.
Show resolved Hide resolved
![compliance-level]: 'sap:base:v1'
}
service A {
@OpenAPI.Extensions: {
![dpp-is-potentially-sensitive]: 'true'
}
entity E1 {
key id: String(4);
oid: String(128);
}

@OpenAPI.Extensions: {
![operation-intent]: 'read-collection'
}
function F1(param: String) returns String;

}`);
const openAPI = toOpenApi(csn);
expect(openAPI).toBeDefined();
expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1');
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');
});
});