Skip to content

Commit 2a4934c

Browse files
OpenAPI Extension Annotations (#51)
* 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
1 parent 8e981f2 commit 2a4934c

File tree

3 files changed

+181
-1
lines changed

3 files changed

+181
-1
lines changed

CHANGELOG.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@ All notable changes to this project will be documented in this file.
44
This project adheres to [Semantic Versioning](http://semver.org/).
55
The format is based on [Keep a Changelog](http://keepachangelog.com/).
66

7+
## Version 1.1.0 - tbd
78

8-
## Version 1.1.0 - tbd
9+
### Added
10+
11+
- Now supports `x-sap` extensions using `@OpenAPI.Extensions` annotations in service, entity and function/action level.
912

1013
### Fixed
1114

lib/compile/csdl2openapi.js

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,107 @@ module.exports.csdl2openapi = function (
106106
if (externalDocs && Object.keys(externalDocs).length > 0) {
107107
openapi.externalDocs = externalDocs;
108108
}
109+
const extensions = getExtensions(csdl, 'root');
110+
if (extensions && Object.keys(extensions).length > 0) {
111+
Object.assign(openapi, extensions);
112+
}
113+
114+
// function to read @OpenAPI.Extensions and get them in the generated openAPI document
115+
function getExtensions(csdl, level) {
116+
let extensionObj = {};
117+
let containerSchema = {};
118+
if (level ==='root'){
119+
const namespace = csdl.$EntityContainer ? nameParts(csdl.$EntityContainer).qualifier : null;
120+
containerSchema = csdl.$EntityContainer ? csdl[namespace] : {};
121+
}
122+
else if(level === 'schema' || level === 'operation'){
123+
containerSchema = csdl;
124+
}
125+
126+
for (const [key, value] of Object.entries(containerSchema)) {
127+
if (key.startsWith('@OpenAPI.Extensions')) {
128+
const annotationProperties = key.split('@OpenAPI.Extensions.')[1];
129+
const keys = annotationProperties.split('.');
130+
if (!keys[0].startsWith("x-sap-")) {
131+
keys[0] = (keys[0].startsWith("sap-") ? "x-" : "x-sap-") + keys[0];
132+
}
133+
if (keys.length === 1) {
134+
extensionObj[keys[0]] = value;
135+
} else {
136+
nestedAnnotation(extensionObj, keys[0], keys, value);
137+
}
138+
}
139+
}
140+
let extensionEnums = {
141+
"x-sap-compliance-level": {allowedValues: ["sap:base:v1", "sap:core:v1", "sap:core:v2" ] } ,
142+
"x-sap-api-type": {allowedValues: [ "ODATA", "ODATAV4", "REST" , "SOAP"] },
143+
"x-sap-direction": {allowedValues: ["inbound", "outbound", "mixed"] , default : "inbound" },
144+
"x-sap-dpp-entity-semantics": {allowedValues: ["sap:DataSubject", "sap:DataSubjectDetails", "sap:Other"] },
145+
"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"] },
146+
};
147+
checkForExtentionEnums(extensionObj, extensionEnums);
148+
149+
let extenstionSchema = {
150+
"x-sap-stateInfo": ['state', 'deprecationDate', 'decomissionedDate', 'link'],
151+
"x-sap-ext-overview": ['name', 'values'],
152+
"x-sap-deprecated-operation" : ['deprecationDate', 'successorOperationRef', "successorOperationId"],
153+
"x-sap-odm-semantic-key" : ['name', 'values'],
154+
};
155+
156+
checkForExtentionSchema(extensionObj, extenstionSchema);
157+
return extensionObj;
158+
}
159+
160+
function checkForExtentionEnums(extensionObj, extensionEnums){
161+
for (const [key, value] of Object.entries(extensionObj)) {
162+
if(extensionEnums[key] && extensionEnums[key].allowedValues && !extensionEnums[key].allowedValues.includes(value)){
163+
if(extensionEnums[key].default){
164+
extensionObj[key] = extensionEnums[key].default;
165+
}
166+
else{
167+
delete extensionObj[key];
168+
}
169+
}
170+
}
171+
}
172+
173+
function checkForExtentionSchema(extensionObj, extenstionSchema) {
174+
for (const [key, value] of Object.entries(extensionObj)) {
175+
if (extenstionSchema[key]) {
176+
if (Array.isArray(value)) {
177+
extensionObj[key] = value.filter((v) => extenstionSchema[key].includes(v));
178+
} else if (typeof value === "object" && value !== null) {
179+
for (const field in value) {
180+
if (!extenstionSchema[key].includes(field)) {
181+
delete extensionObj[key][field];
182+
}
183+
}
184+
}
185+
}
186+
}
187+
}
188+
189+
190+
function nestedAnnotation(resObj, openapiProperty, keys, value) {
191+
if (resObj[openapiProperty] === undefined) {
192+
resObj[openapiProperty] = {};
193+
}
194+
195+
let node = resObj[openapiProperty];
196+
197+
// traverse the annotation property and define the objects if they're not defined
198+
for (let nestedIndex = 1; nestedIndex < keys.length - 1; nestedIndex++) {
199+
const nestedElement = keys[nestedIndex];
200+
if (node[nestedElement] === undefined) {
201+
node[nestedElement] = {};
202+
}
203+
node = node[nestedElement];
204+
}
109205

206+
// set value annotation property
207+
node[keys[keys.length - 1]] = value;
208+
}
209+
110210
if (!csdl.$EntityContainer) {
111211
delete openapi.servers;
112212
delete openapi.tags;
@@ -1466,6 +1566,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
14661566
: response(204, "Success", undefined, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses),
14671567
}
14681568
};
1569+
const actionExtension = getExtensions(overload, 'operation');
1570+
if (Object.keys(actionExtension).length > 0) {
1571+
Object.assign(pathItem.post, actionExtension);
1572+
}
14691573
const description = actionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription];
14701574
if (description) pathItem.post.description = description;
14711575
if (prefixParameters.length > 0) pathItem.post.parameters = [...prefixParameters];
@@ -1576,6 +1680,10 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
15761680
responses: response(200, "Success", overload.$ReturnType, overload[voc.Capabilities.OperationRestrictions]?.ErrorResponses),
15771681
}
15781682
};
1683+
const functionExtension = getExtensions(overload, 'operation');
1684+
if (Object.keys(functionExtension).length > 0) {
1685+
Object.assign(pathItem.get, functionExtension);
1686+
}
15791687
const iDescription = functionImport[voc.Core.LongDescription] || overload[voc.Core.LongDescription];
15801688
if (iDescription) pathItem.get.description = iDescription;
15811689
customParameters(pathItem.get, overload[voc.Capabilities.OperationRestrictions] || {});
@@ -1759,6 +1867,23 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot
17591867
break;
17601868
}
17611869
}
1870+
1871+
// Add @OpenAPI.Extensions at entity level to schema object
1872+
Object.keys(csdl).filter(name => isIdentifier(name)).forEach(namespace => {
1873+
const schema = csdl[namespace];
1874+
Object.keys(schema).filter(name => isIdentifier(name)).forEach(name => {
1875+
const type = schema[name];
1876+
if (type.$Kind === 'EntityType' || type.$Kind === 'ComplexType') {
1877+
const schemaName = namespace + "." + name + SUFFIX.read;
1878+
const extensions = getExtensions(type, 'schema');
1879+
if (Object.keys(extensions).length > 0) {
1880+
unordered[schemaName] = unordered[schemaName] || {};
1881+
Object.assign(unordered[schemaName], extensions);
1882+
}
1883+
}
1884+
});
1885+
});
1886+
17621887
const ordered = {};
17631888
for (const name of Object.keys(unordered).sort()) {
17641889
ordered[name] = unordered[name];

test/lib/compile/openapi.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,4 +424,56 @@ service CatalogService {
424424
expect(openAPI.externalDocs.url).toBe('https://help.sap.com/docs/product/123.html');
425425
}
426426
);
427+
428+
test('OpenAPI annotations: @OpenAPI.Extensions annotation is added to the openapi document', () => {
429+
const csn = cds.compile.to.csn(`
430+
namespace sap.OpenAPI.test;
431+
@OpenAPI.Extensions: {
432+
![compliance-level]: 'sap:base:v1',
433+
![x-sap-ext-overview]: {
434+
name : 'Communication Scenario',
435+
values: {
436+
text : 'Planning Calendar API Integration',
437+
format: 'plain'
438+
}
439+
}
440+
}
441+
service A {
442+
@OpenAPI.Extensions: {
443+
![dpp-is-potentially-sensitive]: 'true'
444+
}
445+
entity E1 {
446+
key id: String(4);
447+
oid: String(128);
448+
}
449+
450+
@OpenAPI.Extensions: {
451+
![x-sap-operation-intent]: 'read-collection for function',
452+
![sap-deprecated-operation] : {
453+
deprecationDate: '2022-12-31',
454+
successorOperationId: 'successorOperation',
455+
notValidKey: 'notValidValue'
456+
}
457+
}
458+
function F1(param: String) returns String;
459+
460+
@OpenAPI.Extensions: {
461+
![x-sap-operation-intent]: 'read-collection for action'
462+
}
463+
action A1(param: String) returns String;
464+
465+
}`);
466+
const openAPI = toOpenApi(csn);
467+
expect(openAPI).toBeDefined();
468+
expect(openAPI['x-sap-compliance-level']).toBe('sap:base:v1');
469+
expect(openAPI['x-sap-ext-overview'].name).toBe('Communication Scenario');
470+
expect(openAPI['x-sap-ext-overview'].values.text).toBe('Planning Calendar API Integration');
471+
expect(openAPI['x-sap-ext-overview'].values.format).toBe('plain');
472+
expect(openAPI.components.schemas["sap.OpenAPI.test.A.E1"]["x-sap-dpp-is-potentially-sensitive"]).toBe('true');
473+
expect(openAPI.paths["/F1"].get["x-sap-operation-intent"]).toBe('read-collection for function');
474+
expect(openAPI.paths["/A1"].post["x-sap-operation-intent"]).toBe('read-collection for action');
475+
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].deprecationDate).toBe('2022-12-31');
476+
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].successorOperationId).toBe('successorOperation');
477+
expect(openAPI.paths["/F1"].get["x-sap-deprecated-operation"].notValidKey).toBeUndefined();
478+
});
427479
});

0 commit comments

Comments
 (0)