diff --git a/tools/transformer/__tests__/removeEnums.test.js b/tools/transformer/__tests__/removeEnums.test.js new file mode 100644 index 000000000..ace8e3f7e --- /dev/null +++ b/tools/transformer/__tests__/removeEnums.test.js @@ -0,0 +1,202 @@ +const { applyRemoveEnumsTransformations } = require("../src/transformations/removeEnums"); + +describe("removeEnums transformation", () => { + test("should handle enum-only schemas referenced via $ref", () => { + const api = { + components: { + schemas: { + // This is an enum-only schema that gets referenced + StatusEnum: { + description: "Status of the resource", + enum: ["ACTIVE", "INACTIVE", "PENDING"], + example: "ACTIVE", + title: "Status Types", + type: "string" + }, + // This schema references the enum-only schema + Resource: { + type: "object", + properties: { + id: { + type: "string" + }, + status: { + $ref: "#/components/schemas/StatusEnum" + } + } + } + } + }, + paths: { + "/resource": { + get: { + responses: { + "200": { + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Resource" + } + } + } + } + } + } + } + } + }; + + const result = applyRemoveEnumsTransformations(api); + + // The StatusEnum schema should be removed + expect(result.components.schemas.StatusEnum).toBeUndefined(); + + // The reference should be replaced with inline type definition + expect(result.components.schemas.Resource.properties.status).toEqual({ + description: "Status of the resource", + example: "ACTIVE", + title: "Status Types", + type: "string" + }); + + // Other properties should remain untouched + expect(result.components.schemas.Resource.properties.id).toEqual({ + type: "string" + }); + }); + + test("should remove enums from regular properties", () => { + const api = { + components: { + schemas: { + Resource: { + type: "object", + properties: { + status: { + type: "string", + enum: ["ACTIVE", "INACTIVE"], + description: "Status of the resource" + } + } + } + } + } + }; + + const result = applyRemoveEnumsTransformations(api); + + // The enum should be removed but other properties preserved + expect(result.components.schemas.Resource.properties.status).toEqual({ + type: "string", + description: "Status of the resource" + }); + }); + + test("should not remove schemas that have properties even if they have enums", () => { + const api = { + components: { + schemas: { + ComplexSchema: { + type: "object", + enum: ["VALUE1", "VALUE2"], // This has enum but also properties + properties: { + id: { + type: "string" + } + } + }, + Resource: { + type: "object", + properties: { + complex: { + $ref: "#/components/schemas/ComplexSchema" + } + } + } + } + } + }; + + const result = applyRemoveEnumsTransformations(api); + + // ComplexSchema should still exist because it has properties + expect(result.components.schemas.ComplexSchema).toBeDefined(); + expect(result.components.schemas.ComplexSchema.properties).toBeDefined(); + + // But enum should be removed + expect(result.components.schemas.ComplexSchema.enum).toBeUndefined(); + + // Reference should remain unchanged + expect(result.components.schemas.Resource.properties.complex).toEqual({ + $ref: "#/components/schemas/ComplexSchema" + }); + }); + + test("should handle oneOf with enum schemas", () => { + const api = { + components: { + schemas: { + // Enum-only schemas that will be used in oneOf + StatusEnum: { + description: "Status enumeration", + enum: ["ACTIVE", "INACTIVE"], + example: "ACTIVE", + title: "Status", + type: "string" + }, + PriorityEnum: { + description: "Priority enumeration", + enum: ["HIGH", "MEDIUM", "LOW"], + example: "HIGH", + title: "Priority", + type: "string" + }, + // Schema with oneOf referencing enum schemas + Resource: { + type: "object", + properties: { + id: { + type: "string" + }, + category: { + oneOf: [ + { $ref: "#/components/schemas/StatusEnum" }, + { $ref: "#/components/schemas/PriorityEnum" } + ] + } + } + } + } + } + }; + + const result = applyRemoveEnumsTransformations(api); + + // The enum-only schemas should be removed + expect(result.components.schemas.StatusEnum).toBeUndefined(); + expect(result.components.schemas.PriorityEnum).toBeUndefined(); + + // The oneOf should be preserved but with inlined type definitions + expect(result.components.schemas.Resource.properties.category).toEqual({ + oneOf: [ + { + description: "Status enumeration", + example: "ACTIVE", + title: "Status", + type: "string" + }, + { + description: "Priority enumeration", + example: "HIGH", + title: "Priority", + type: "string" + } + ] + }); + + // Other properties should remain untouched + expect(result.components.schemas.Resource.properties.id).toEqual({ + type: "string" + }); + }); +}); diff --git a/tools/transformer/src/transformations/removeEnums.js b/tools/transformer/src/transformations/removeEnums.js index cab10047b..c98f611b9 100644 --- a/tools/transformer/src/transformations/removeEnums.js +++ b/tools/transformer/src/transformations/removeEnums.js @@ -9,9 +9,158 @@ const removeField = require("../engine/removeField"); * @returns OpenAPI JSON File */ function applyRemoveEnumsTransformations(api) { + // First handle schemas that are primarily enum definitions and are referenced + handleEnumOnlySchemas(api); + + // Then remove enum fields from remaining schemas return removeField(api, "enum"); } +/** + * Handle schemas that are primarily enum definitions and are referenced via $ref. + * For such schemas, inline the base type where they're referenced and remove the schema. + * @param {*} api OpenAPI JSON File + */ +function handleEnumOnlySchemas(api) { + if (!api.components || !api.components.schemas) { + return; + } + + // Find all $ref references in the document + const allRefs = []; + findRefs(api, allRefs); + + // Extract schema references + const referencedSchemas = new Set(); + allRefs.forEach((ref) => { + const refParts = ref.split("/"); + if (refParts[1] === "components" && refParts[2] === "schemas") { + referencedSchemas.add(refParts[3]); + } + }); + + // Identify schemas that are primarily enum definitions + const enumOnlySchemas = []; + Object.keys(api.components.schemas).forEach((schemaName) => { + const schema = api.components.schemas[schemaName]; + if (isEnumOnlySchema(schema) && referencedSchemas.has(schemaName)) { + enumOnlySchemas.push(schemaName); + } + }); + + // For each enum-only schema, replace references with inline type definition + enumOnlySchemas.forEach((schemaName) => { + const schema = api.components.schemas[schemaName]; + const inlineDefinition = createInlineDefinition(schema); + + // Replace all references to this schema with the inline definition + replaceSchemaReferences(api, schemaName, inlineDefinition); + + // Remove the schema + delete api.components.schemas[schemaName]; + + console.info(`Removed enum-only schema '${schemaName}' and inlined its type definition`); + }); +} + +/** + * Check if a schema is primarily an enum definition + * @param {*} schema + * @returns boolean + */ +function isEnumOnlySchema(schema) { + // A schema is considered enum-only if: + // 1. It has an enum field + // 2. It has a type field (usually string) + // 3. It doesn't have properties or complex structures + return schema.enum && + schema.type && + !schema.properties && + !schema.allOf && + !schema.oneOf && + !schema.anyOf && + !schema.items; +} + +/** + * Create an inline definition from an enum schema + * @param {*} schema + * @returns object + */ +function createInlineDefinition(schema) { + const inlineDefinition = { + type: schema.type + }; + + // Preserve important metadata but remove enum + if (schema.description) { + inlineDefinition.description = schema.description; + } + if (schema.example) { + inlineDefinition.example = schema.example; + } + if (schema.title) { + inlineDefinition.title = schema.title; + } + + return inlineDefinition; +} + +/** + * Replace all references to a schema with an inline definition + * @param {*} api + * @param {*} schemaName + * @param {*} inlineDefinition + */ +function replaceSchemaReferences(api, schemaName, inlineDefinition) { + const targetRef = `#/components/schemas/${schemaName}`; + + function replaceRefs(obj) { + if (typeof obj !== "object" || obj === null) { + return; + } + + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + replaceRefs(obj[i]); + } + } else { + for (const key in obj) { + if (key === "$ref" && obj[key] === targetRef) { + // Replace the $ref with the inline definition + delete obj[key]; + Object.assign(obj, inlineDefinition); + } else { + replaceRefs(obj[key]); + } + } + } + } + + replaceRefs(api); +} + +/** + * Recursive function for finding all $ref occurrences in the OpenAPI document + * @param {*} obj + * @param {*} refs + */ +function findRefs(obj, refs) { + if (typeof obj === "object" && obj !== null) { + if (Array.isArray(obj)) { + obj.forEach((item) => findRefs(item, refs)); + } else { + Object.keys(obj).forEach((key) => { + if (key === "$ref") { + refs.push(obj[key]); + } else { + findRefs(obj[key], refs); + } + }); + } + } +} + module.exports = { applyRemoveEnumsTransformations, };