From d995aacdf1cff604a5c6b053e054ce365b8e5a03 Mon Sep 17 00:00:00 2001 From: Daniel O'Grady Date: Tue, 1 Sep 2026 08:45:48 +0200 Subject: [PATCH] Preserve camel case tags --- CHANGELOG.md | 1 + lib/compile/csdl2openapi.js | 6 +-- test/lib/compile/csdl2openapi.test.js | 71 +++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c7be42e..64824c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/). ### Deprecated ### Removed ### Fixed +- `@Common.Label` values containing i18n placeholders (e.g. `{i18n>TasksPlural}`) are no longer corrupted by camelCase word-splitting in tag names - Entities that are transitively autoexposed and should still be considered readonly, do not generate documentation for write endpoints anymore ### Security diff --git a/lib/compile/csdl2openapi.js b/lib/compile/csdl2openapi.js index 2fddefd..ea69e90 100644 --- a/lib/compile/csdl2openapi.js +++ b/lib/compile/csdl2openapi.js @@ -403,9 +403,9 @@ module.exports.csdl2openapi = function ( * @returns {T} */ function normaliseTag(tag) { - const normalise = s => s - .replaceAll('_', ' ') - .replace(/([a-z])([A-Z])/g, '$1 $2'); // "camelCase" to "camel Case" + const normalise = s => s.startsWith('{') ? s + : s.replaceAll('_', ' ') + .replace(/([a-z])([A-Z])/g, '$1 $2'); // "camelCase" to "camel Case" if (typeof tag === 'string') { tag = normalise(tag); } else { diff --git a/test/lib/compile/csdl2openapi.test.js b/test/lib/compile/csdl2openapi.test.js index e50aec8..4a2f4ec 100644 --- a/test/lib/compile/csdl2openapi.test.js +++ b/test/lib/compile/csdl2openapi.test.js @@ -2709,6 +2709,77 @@ see [Expand](http://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part1-prot "MaxLength" ); }); + + it("preserves i18n placeholders in tag names without splitting camelCase", () => { + const csdl = { + $Version: "4.01", + $Reference: { + dummy: { + $Include: [{ $Namespace: "com.sap.vocabularies.Common.v1", $Alias: "Common" }], + }, + }, + $EntityContainer: "TestService.Container", + TestService: { + Container: { + $Kind: "EntityContainer", + Tasks: { $Collection: true, $Type: "TestService.Tasks" }, + }, + Tasks: { + $Kind: "EntityType", + $Key: ["ID"], + ID: { $Type: "Edm.Guid" }, + }, + $Annotations: { + "TestService.Tasks": { "@Common.Label": "{i18n>TasksPlural}" }, + }, + }, + }; + const openapi = lib.csdl2openapi(csdl, {}); + assert.strictEqual( + openapi.tags[0].name, + "{i18n>TasksPlural}", + "i18n placeholder must not be split by camelCase normalisation" + ); + const getOp = openapi.paths["/Tasks"]?.get; + assert.ok(getOp, "GET /Tasks operation must exist"); + assert.strictEqual( + getOp.tags[0], + "{i18n>TasksPlural}", + "operation-level tag must also preserve the placeholder" + ); + }); + + it("still splits plain camelCase label into words", () => { + const csdl = { + $Version: "4.01", + $Reference: { + dummy: { + $Include: [{ $Namespace: "com.sap.vocabularies.Common.v1", $Alias: "Common" }], + }, + }, + $EntityContainer: "TestService.Container", + TestService: { + Container: { + $Kind: "EntityContainer", + Tasks: { $Collection: true, $Type: "TestService.Tasks" }, + }, + Tasks: { + $Kind: "EntityType", + $Key: ["ID"], + ID: { $Type: "Edm.Guid" }, + }, + $Annotations: { + "TestService.Tasks": { "@Common.Label": "TasksPlural" }, + }, + }, + }; + const openapi = lib.csdl2openapi(csdl, {}); + assert.strictEqual( + openapi.tags[0].name, + "Tasks Plural", + "plain camelCase label must still be split into words" + ); + }); }); describe("Bound action path naming", () => {