Skip to content

WIP: inline enums schemas #631

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions tools/transformer/__tests__/removeEnums.test.js
Original file line number Diff line number Diff line change
@@ -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"
});
});
});
149 changes: 149 additions & 0 deletions tools/transformer/src/transformations/removeEnums.js
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The hardcoded array indices for ref parsing (refParts[1], refParts[2], refParts[3]) could fail if the reference format is unexpected. Consider adding validation to ensure refParts has the expected length and structure before accessing these indices.

Suggested change
if (refParts[1] === "components" && refParts[2] === "schemas") {
if (refParts.length >= 4 && refParts[1] === "components" && refParts[2] === "schemas" && refParts[3]) {

Copilot uses AI. Check for mistakes.

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`);
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] Using console.info for logging in a transformation function may not be appropriate for all environments. Consider using a proper logging framework or making logging configurable.

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remember: That is approved logging for Transformation

});
}

/**
* 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);
Copy link
Preview

Copilot AI Jul 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using Object.assign to replace the $ref object modifies the original object structure. This could potentially cause issues if other parts of the code expect the original object structure. Consider creating a new object instead of mutating the existing one.

Suggested change
Object.assign(obj, inlineDefinition);
const newObj = { ...obj, ...inlineDefinition };
Object.keys(obj).forEach(k => delete obj[k]); // Clear the original object
Object.assign(obj, newObj); // Copy new properties into the original object

Copilot uses AI. Check for mistakes.

} 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,
};
Loading