-
Notifications
You must be signed in to change notification settings - Fork 7
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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" | ||
}); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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`); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. Positive FeedbackNegative Feedback There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Copilot uses AI. Check for mistakes. Positive FeedbackNegative Feedback |
||||||||||
} 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, | ||||||||||
}; |
There was a problem hiding this comment.
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.
Copilot uses AI. Check for mistakes.