diff --git a/demo/docusaurus.config.ts b/demo/docusaurus.config.ts index 0c051fc29..ed511aece 100644 --- a/demo/docusaurus.config.ts +++ b/demo/docusaurus.config.ts @@ -71,6 +71,10 @@ const config: Config = { label: "Petstore (versioned)", to: "/category/petstore-versioned-api", }, + { + label: "Tests", + to: "/category/tests", + }, ], }, { @@ -268,6 +272,16 @@ const config: Config = { }, showSchemas: true, } satisfies OpenApiPlugin.Options, + tests: { + specPath: "examples/tests", + outputDir: "docs/tests", + sidebarOptions: { + groupPathsBy: "tag", + categoryLinkSource: "info", + }, + hideSendButton: true, + showSchemas: true, + } satisfies OpenApiPlugin.Options, } satisfies Plugin.PluginOptions, }, ], diff --git a/demo/examples/tests/allOf.yaml b/demo/examples/tests/allOf.yaml new file mode 100644 index 000000000..6d5dd9ae6 --- /dev/null +++ b/demo/examples/tests/allOf.yaml @@ -0,0 +1,282 @@ +openapi: 3.0.1 +info: + title: AllOf Variations API + description: Demonstrates various allOf schema combinations. + version: 1.0.0 +tags: + - name: allOf + description: allOf tests +paths: + /multiple-allof-nested: + get: + tags: + - allOf + summary: Multiple allOf with Nested Properties + description: | + Schema: + ```yaml + allOf: + - type: object + properties: + outerProp1: + type: object + properties: + innerProp1: + type: string + - type: object + properties: + outerProp2: + type: object + properties: + innerProp2: + type: number + ``` + responses: + "200": + description: Successful response + content: + application/json: + schema: + allOf: + - type: object + properties: + outerProp1: + type: object + properties: + innerProp1: + type: string + - type: object + properties: + outerProp2: + type: object + properties: + innerProp2: + type: number + + /allof-shared-required: + get: + tags: + - allOf + summary: allOf with Shared Required Properties + description: | + Schema: + ```yaml + allOf: + - type: object + properties: + sharedProp: + type: string + required: [sharedProp] + - type: object + properties: + anotherProp: + type: number + required: [anotherProp] + ``` + responses: + "200": + description: Successful response + content: + application/json: + schema: + allOf: + - type: object + properties: + sharedProp: + type: string + required: [sharedProp] + - type: object + properties: + anotherProp: + type: number + required: [anotherProp] + + # /allof-conflicting-properties: + # get: + # tags: + # - allOf + # summary: allOf with Conflicting Properties + # description: | + # Schema: + # ```yaml + # allOf: + # - type: object + # properties: + # conflictingProp: + # type: string + # - type: object + # properties: + # conflictingProp: + # type: number + # ``` + # responses: + # '200': + # description: Successful response + # content: + # application/json: + # schema: + # allOf: + # - type: object + # properties: + # conflictingProp: + # type: string + # - type: object + # properties: + # conflictingProp: + # type: number + + # /allof-mixed-data-types: + # get: + # tags: + # - allOf + # summary: allOf with Mixed Data Types + # description: | + # Schema: + # ```yaml + # allOf: + # - type: object + # properties: + # mixedTypeProp1: + # type: string + # - type: array + # items: + # type: number + # ``` + # responses: + # '200': + # description: Successful response + # content: + # application/json: + # schema: + # allOf: + # - type: object + # properties: + # mixedTypeProp1: + # type: string + # - type: array + # items: + # type: number + + /allof-deep-merging: + get: + tags: + - allOf + summary: allOf with Deep Merging + description: | + Schema: + ```yaml + allOf: + - type: object + properties: + deepProp: + type: object + properties: + innerProp1: + type: string + - type: object + properties: + deepProp: + type: object + properties: + innerProp2: + type: number + ``` + responses: + "200": + description: Successful response + content: + application/json: + schema: + allOf: + - type: object + properties: + deepProp: + type: object + properties: + innerProp1: + type: string + - type: object + properties: + deepProp: + type: object + properties: + innerProp2: + type: number + + # /allof-discriminator: + # get: + # tags: + # - allOf + # summary: allOf with Discriminator + # description: | + # Schema: + # ```yaml + # allOf: + # - type: object + # discriminator: + # propertyName: type + # properties: + # type: + # type: string + # - type: object + # properties: + # specificProp: + # type: string + # ``` + # responses: + # "200": + # description: Successful response + # content: + # application/json: + # schema: + # allOf: + # - type: object + # discriminator: + # propertyName: type + # properties: + # type: + # type: string + # - type: object + # properties: + # specificProp: + # type: string + + /allof-same-level-properties: + get: + tags: + - allOf + summary: allOf with Same-Level Properties + description: | + Schema: + ```yaml + allOf: + - type: object + properties: + allOfProp1: + type: string + allOfProp2: + type: string + properties: + parentProp1: + type: string + parentProp2: + type: string + ``` + responses: + "200": + description: Successful response + content: + application/json: + schema: + allOf: + - type: object + properties: + allOfProp1: + type: string + allOfProp2: + type: string + properties: + parentProp1: + type: string + parentProp2: + type: string diff --git a/demo/sidebars.ts b/demo/sidebars.ts index cc4e4255b..037e3c4cc 100644 --- a/demo/sidebars.ts +++ b/demo/sidebars.ts @@ -170,6 +170,20 @@ const sidebars: SidebarsConfig = { items: petstoreVersionSidebar, }, ], + + tests: [ + { + type: "category", + label: "Tests", + link: { + type: "generated-index", + title: "Tests", + description: "Various OpenAPI test cases", + slug: "/category/tests", + }, + items: require("./docs/tests/sidebar.js"), + }, + ], }; export default sidebars; diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap b/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap index 566438c29..82bff053d 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/__snapshots__/createSchema.test.ts.snap @@ -223,7 +223,152 @@ Array [ ] `; -exports[`createNodes should create readable MODs for oneOf primitive properties 1`] = ` +exports[`createNodes allOf should correctly deep merge properties in allOf schemas 1`] = ` +Array [ + " +
+ + + deepProp + object + + +
+ + +
+
+
; +", +] +`; + +exports[`createNodes allOf should correctly handle shared required properties across allOf schemas 1`] = ` +Array [ + "; +", + "; +", +] +`; + +exports[`createNodes allOf should correctly merge nested properties from multiple allOf schemas 1`] = ` +Array [ + " +
+ + + outerProp1 + object + + +
+ +
+
+
; +", + " +
+ + + outerProp2 + object + + +
+ +
+
+
; +", +] +`; + +exports[`createNodes allOf should render same-level properties with allOf 1`] = ` +Array [ + "; +", + "; +", + "; +", + "; +", +] +`; + +exports[`createNodes oneOf should create readable MODs for oneOf primitive properties 1`] = ` Array [ "
diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts index 978fcbf4e..a9a4f964a 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.test.ts @@ -11,50 +11,306 @@ import { createNodes } from "./createSchema"; import { SchemaObject } from "../openapi/types"; describe("createNodes", () => { - it("should create readable MODs for oneOf primitive properties", async () => { - const schema: SchemaObject = { - "x-tags": ["clown"], - type: "object", - properties: { - oneOfProperty: { - oneOf: [ - { - type: "object", - properties: { - noseLength: { - type: "number", + describe("oneOf", () => { + it("should create readable MODs for oneOf primitive properties", async () => { + const schema: SchemaObject = { + "x-tags": ["clown"], + type: "object", + properties: { + oneOfProperty: { + oneOf: [ + { + type: "object", + properties: { + noseLength: { + type: "number", + }, }, + required: ["noseLength"], + description: "Clown's nose length", + }, + { + type: "array", + items: { + type: "string", + }, + description: "Array of strings", + }, + { + type: "boolean", + }, + { + type: "number", + }, + { + type: "string", + }, + ], + }, + }, + }; + expect( + await Promise.all( + createNodes(schema, "request").map( + async (md: any) => await prettier.format(md, { parser: "babel" }) + ) + ) + ).toMatchSnapshot(); + }); + }); + + describe("allOf", () => { + it("should render same-level properties with allOf", async () => { + const schema: SchemaObject = { + allOf: [ + { + type: "object", + properties: { + allOfProp1: { + type: "string", + }, + allOfProp2: { + type: "string", }, - required: ["noseLength"], - description: "Clown's nose length", }, - { - type: "array", - items: { + }, + ], + properties: { + parentProp1: { + type: "string", + }, + parentProp2: { + type: "string", + }, + }, + }; + + expect( + await Promise.all( + createNodes(schema, "response").map( + async (md: any) => await prettier.format(md, { parser: "babel" }) + ) + ) + ).toMatchSnapshot(); + }); + + it("should correctly merge nested properties from multiple allOf schemas", async () => { + const schema: SchemaObject = { + allOf: [ + { + type: "object", + properties: { + outerProp1: { + type: "object", + properties: { + innerProp1: { + type: "string", + }, + }, + }, + }, + }, + { + type: "object", + properties: { + outerProp2: { + type: "object", + properties: { + innerProp2: { + type: "number", + }, + }, + }, + }, + }, + ], + }; + + expect( + await Promise.all( + createNodes(schema, "response").map( + async (md: any) => await prettier.format(md, { parser: "babel" }) + ) + ) + ).toMatchSnapshot(); + }); + + it("should correctly handle shared required properties across allOf schemas", async () => { + const schema: SchemaObject = { + allOf: [ + { + type: "object", + properties: { + sharedProp: { type: "string", }, - description: "Array of strings", }, - { - type: "boolean", + required: ["sharedProp"], + }, + { + type: "object", + properties: { + anotherProp: { + type: "number", + }, }, - { - type: "number", + required: ["anotherProp"], + }, + ], + }; + + expect( + await Promise.all( + createNodes(schema, "response").map( + async (md: any) => await prettier.format(md, { parser: "babel" }) + ) + ) + ).toMatchSnapshot(); + }); + + // Could not resolve values for path:"properties.conflictingProp.type". They are probably incompatible. Values: + // "string" + // "number" + // eslint-disable-next-line jest/no-commented-out-tests + // it("should handle conflicting properties in allOf schemas", async () => { + // const schema: SchemaObject = { + // allOf: [ + // { + // type: "object", + // properties: { + // conflictingProp: { + // type: "string", + // }, + // }, + // }, + // { + // type: "object", + // properties: { + // conflictingProp: { + // type: "number", + // }, + // }, + // }, + // ], + // }; + + // expect( + // await Promise.all( + // createNodes(schema, "response").map( + // async (md: any) => await prettier.format(md, { parser: "babel" }) + // ) + // ) + // ).toMatchSnapshot(); + // }); + + // Could not resolve values for path:"type". They are probably incompatible. Values: + // "object" + // "array" + // eslint-disable-next-line jest/no-commented-out-tests + // it("should handle mixed data types in allOf schemas", async () => { + // const schema: SchemaObject = { + // allOf: [ + // { + // type: "object", + // properties: { + // mixedTypeProp1: { + // type: "string", + // }, + // }, + // }, + // { + // type: "array", + // items: { + // type: "number", + // }, + // }, + // ], + // }; + + // expect( + // await Promise.all( + // createNodes(schema, "response").map( + // async (md: any) => await prettier.format(md, { parser: "babel" }) + // ) + // ) + // ).toMatchSnapshot(); + // }); + + it("should correctly deep merge properties in allOf schemas", async () => { + const schema: SchemaObject = { + allOf: [ + { + type: "object", + properties: { + deepProp: { + type: "object", + properties: { + innerProp1: { + type: "string", + }, + }, + }, }, - { - type: "string", + }, + { + type: "object", + properties: { + deepProp: { + type: "object", + properties: { + innerProp2: { + type: "number", + }, + }, + }, }, - ], - }, - }, - }; - expect( - await Promise.all( - createNodes(schema, "request").map( - async (md: any) => await prettier.format(md, { parser: "babel" }) + }, + ], + }; + + expect( + await Promise.all( + createNodes(schema, "response").map( + async (md: any) => await prettier.format(md, { parser: "babel" }) + ) ) - ) - ).toMatchSnapshot(); + ).toMatchSnapshot(); + }); + + // eslint-disable-next-line jest/no-commented-out-tests + // it("should handle discriminator with allOf schemas", async () => { + // const schema: SchemaObject = { + // allOf: [ + // { + // type: "object", + // discriminator: { + // propertyName: "type", + // }, + // properties: { + // type: { + // type: "string", + // }, + // }, + // }, + // { + // type: "object", + // properties: { + // specificProp: { + // type: "string", + // }, + // }, + // }, + // ], + // }; + + // expect( + // await Promise.all( + // createNodes(schema, "response").map( + // async (md: any) => await prettier.format(md, { parser: "babel" }) + // ) + // ) + // ).toMatchSnapshot(); + // }); }); describe("additionalProperties", () => { diff --git a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts index 79feea1d0..a0cc0775a 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/markdown/createSchema.ts @@ -615,6 +615,7 @@ function createEdges({ } const schemaName = getSchemaName(schema); + if (discriminator !== undefined && discriminator.propertyName === name) { return createPropertyDiscriminator( name, @@ -635,6 +636,47 @@ function createEdges({ ); } + if (schema.properties !== undefined) { + return createDetailsNode( + name, + schemaName, + schema, + required, + schema.nullable + ); + } + + if (schema.additionalProperties !== undefined) { + return createDetailsNode( + name, + schemaName, + schema, + required, + schema.nullable + ); + } + + // array of objects + if (schema.items?.properties !== undefined) { + return createDetailsNode( + name, + schemaName, + schema, + required, + schema.nullable + ); + } + + if (schema.items?.anyOf !== undefined || schema.items?.oneOf !== undefined) { + return createDetailsNode( + name, + schemaName, + schema, + required, + schema.nullable + ); + } + if (schema.allOf !== undefined) { const { mergedSchemas }: { mergedSchemas: SchemaObject } = mergeAllOf( schema.allOf @@ -707,47 +749,6 @@ function createEdges({ }); } - if (schema.properties !== undefined) { - return createDetailsNode( - name, - schemaName, - schema, - required, - schema.nullable - ); - } - - if (schema.additionalProperties !== undefined) { - return createDetailsNode( - name, - schemaName, - schema, - required, - schema.nullable - ); - } - - // array of objects - if (schema.items?.properties !== undefined) { - return createDetailsNode( - name, - schemaName, - schema, - required, - schema.nullable - ); - } - - if (schema.items?.anyOf !== undefined || schema.items?.oneOf !== undefined) { - return createDetailsNode( - name, - schemaName, - schema, - required, - schema.nullable - ); - } - // primitives and array of non-objects return create("SchemaItem", { collapsible: false, @@ -787,6 +788,19 @@ export function createNodes( nodes.push(createAnyOneOf(schema)); } + if (schema.properties !== undefined) { + nodes.push(createProperties(schema)); + } + + if (schema.additionalProperties !== undefined) { + nodes.push(createAdditionalProperties(schema)); + } + + // TODO: figure out how to handle array of objects + if (schema.items !== undefined) { + nodes.push(createItems(schema)); + } + if (schema.allOf !== undefined) { const { mergedSchemas } = mergeAllOf(schema.allOf); @@ -802,19 +816,6 @@ export function createNodes( } } - if (schema.properties !== undefined) { - nodes.push(createProperties(schema)); - } - - if (schema.additionalProperties !== undefined) { - nodes.push(createAdditionalProperties(schema)); - } - - // TODO: figure out how to handle array of objects - if (schema.items !== undefined) { - nodes.push(createItems(schema)); - } - if (nodes.length && nodes.length > 0) { return nodes.filter(Boolean).flat(); }