diff --git a/demo/examples/tests/examples.yaml b/demo/examples/tests/examples.yaml new file mode 100644 index 000000000..be999ac07 --- /dev/null +++ b/demo/examples/tests/examples.yaml @@ -0,0 +1,455 @@ +openapi: 3.1.0 +info: + title: Examples Demo API + description: Demonstrates various examples schema combinations. + version: 1.0.0 + license: + name: MIT + url: https://opensource.org/licenses/MIT +security: [] +servers: + - url: http://test.local:8080 + description: Local server +tags: + - name: examples + description: examples +paths: + /requestParameters/example: + get: + tags: + - examples + summary: example in request parameters + description: "description of request parameters example" + parameters: + - name: name + description: name example + in: query + schema: + type: string + example: "John Doe" + - name: age + description: age example + in: query + schema: + type: number + example: 25 + responses: + "204": + description: no content + + /requestParameters/examples: + get: + tags: + - examples + summary: examples in request parameters + description: "description of request parameters examples" + parameters: + - name: name + description: name example + in: query + schema: + type: string + examples: + example1: + summary: "name example 1" + description: "name example 1 description" + value: "John Doe" + example2: + summary: "name example 2" + description: "name example 2 description" + value: "Jane Smith" + responses: + "204": + description: no content + + /requestParameters/schema/example: + get: + tags: + - examples + summary: example in request parameters schema + description: "description of request parameters schema example" + parameters: + - name: name + description: name example + in: query + schema: + type: string + example: "John Doe" + - name: age + description: age example + in: query + schema: + type: number + example: 25 + responses: + "204": + description: no content + + /requestParameters/schema/examples: + get: + tags: + - examples + summary: examples in request parameters schema + description: "description of request parameters schema examples" + parameters: + - name: name + description: name example + in: query + schema: + type: string + examples: + - "John Doe" + - "Jane Smith" + - name: age + description: age example + in: query + schema: + type: number + examples: + - 25 + - 30 + responses: + "204": + description: no content + + /requestBody/mediaTypeObject/example: + post: + tags: + - examples + summary: example of media type object in requestBody + description: "description of requestBody media type object example" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + responses: + "204": + description: no content + + /requestBody/mediaTypeObject/examples: + post: + tags: + - examples + summary: examples of media type object in requestBody + description: "description of requestBody media type object examples" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + example1: + value: + name: "John Doe" + age: 25 + isStudent: false + example2: + value: + name: "Jane Smith" + age: 30 + isStudent: true + responses: + "204": + description: no content + + /requestBody/schema/example: + post: + tags: + - examples + summary: example of schema in requestBody + description: "description of requestBody schema example" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + responses: + "204": + description: no content + + /requestBody/schema/examples: + post: + tags: + - examples + summary: examples of schema in requestBody + description: "description of requestBody schema examples" + requestBody: + description: "description of requestBody" + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + - name: "John Doe" + age: 25 + isStudent: false + - name: "Jane Smith" + age: 30 + isStudent: true + responses: + "204": + description: no content + + /requestBody/schema/properties/example: + post: + tags: + - examples + summary: example of properties in requestBody schema + description: "description of requestBody schema properties example" + requestBody: + description: "description of requestBody" + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John Doe" + age: + type: number + example: 25 + isStudent: + type: boolean + example: false + responses: + "204": + description: no content + + /requestBody/schema/properties/examples: + post: + tags: + - examples + summary: examples of properties in requestBody schema + description: "description of requestBody schema properties examples" + requestBody: + description: "description of requestBody" + required: true + content: + application/json: + schema: + type: object + properties: + name: + type: string + examples: + - "John Doe" + - "Jane Smith" + age: + type: number + examples: + - 25 + - 30 + isStudent: + type: boolean + examples: + - true + - false + responses: + "204": + description: no content + + /response/mediaTypeObject/example: + get: + tags: + - examples + summary: example of media type object in response + description: "description of response media type object example" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + + /response/mediaTypeObject/examples: + get: + tags: + - examples + summary: examples of media type object in response + description: "description of response media type object examples" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + example1: + value: + name: "John Doe" + age: 25 + isStudent: false + example2: + value: + name: "Jane Smith" + age: 30 + isStudent: true + + /response/schema/example: + get: + tags: + - examples + summary: example of schema in response + description: "description of response schema example" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + example: + name: "John Doe" + age: 25 + isStudent: false + + /response/schema/examples: + get: + tags: + - examples + summary: examples of schema in response + description: "description of response schema examples" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + age: + type: number + isStudent: + type: boolean + examples: + - name: "John Doe" + age: 25 + isStudent: false + - name: "Jane Smith" + age: 30 + isStudent: true + + /response/schema/properties/example: + get: + tags: + - examples + summary: example of schema properties in response + description: "description of response schema properties example" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + example: "John Doe" + age: + type: number + example: 25 + isStudent: + type: boolean + example: false + + /response/schema/properties/examples: + get: + tags: + - examples + summary: examples of schema properties in response + description: "description of response schema properties examples" + responses: + "200": + description: successful response + content: + application/json: + schema: + type: object + properties: + name: + type: string + examples: + - "John Doe" + - "Jane Smith" + age: + type: number + examples: + - 25 + - 30 + isStudent: + type: boolean + examples: + - true + - false diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts index 72e15d590..876b7cff0 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createRequestExample.ts @@ -5,258 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import chalk from "chalk"; -import merge from "lodash/merge"; - +import { sampleFromSchema } from "./createSchemaExample"; import { SchemaObject } from "./types"; -import { mergeAllOf } from "../markdown/createSchema"; - -interface OASTypeToTypeMap { - string: string; - number: number; - integer: number; - boolean: boolean; - object: any; - array: any[]; - null: string | null; -} - -type Primitives = { - [OASType in keyof OASTypeToTypeMap]: { - [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; - }; -}; - -const primitives: Primitives = { - string: { - default: () => "string", - email: () => "user@example.com", - date: () => "2024-07-29", - "date-time": () => "2024-07-29T15:51:28.071Z", - uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", - hostname: () => "example.com", - ipv4: () => "198.51.100.42", - ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", - }, - number: { - default: () => 0, - float: () => 0.0, - }, - integer: { - default: () => 0, - }, - boolean: { - default: (schema) => - typeof schema.default === "boolean" ? schema.default : true, - }, - object: {}, - array: {}, - null: { - default: () => "null", - }, -}; - -function sampleRequestFromProp(name: string, prop: any, obj: any): any { - // Handle resolved circular props - if (typeof prop === "object" && Object.keys(prop).length === 0) { - obj[name] = prop; - return obj; - } - - // TODO: handle discriminators - - if (prop.oneOf) { - obj[name] = sampleRequestFromSchema(prop.oneOf[0]); - } else if (prop.anyOf) { - obj[name] = sampleRequestFromSchema(prop.anyOf[0]); - } else if (prop.allOf) { - const mergedSchemas = mergeAllOf(prop) as SchemaObject; - sampleRequestFromProp(name, mergedSchemas, obj); - } else { - obj[name] = sampleRequestFromSchema(prop); - } - return obj; -} export const sampleRequestFromSchema = (schema: SchemaObject = {}): any => { - try { - // deep copy schema before processing - let schemaCopy = JSON.parse(JSON.stringify(schema)); - let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; - - if (example !== undefined) { - return example; - } - - if (oneOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, oneOf[0]); - delete combinedSchemas.oneOf; - return sampleRequestFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleRequestFromSchema(oneOf[0]); - } - - if (anyOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, anyOf[0]); - delete combinedSchemas.anyOf; - return sampleRequestFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleRequestFromSchema(anyOf[0]); - } - - if (allOf) { - const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; - if (mergedSchemas.properties) { - for (const [key, value] of Object.entries(mergedSchemas.properties)) { - if ((value.readOnly && value.readOnly === true) || value.deprecated) { - delete mergedSchemas.properties[key]; - } - } - } - if (properties) { - const combinedSchemas = merge(schemaCopy, mergedSchemas); - delete combinedSchemas.allOf; - return sampleRequestFromSchema(combinedSchemas); - } - return sampleRequestFromSchema(mergedSchemas); - } - - if (!type) { - if (properties) { - type = "object"; - } else if (items) { - type = "array"; - } else { - return; - } - } - - if (type === "object") { - let obj: any = {}; - for (let [name, prop] of Object.entries(properties ?? {}) as any) { - if (prop.properties) { - for (const [key, value] of Object.entries(prop.properties) as any) { - if ( - (value.readOnly && value.readOnly === true) || - value.deprecated - ) { - delete prop.properties[key]; - } - } - } - - if (prop.items && prop.items.properties) { - for (const [key, value] of Object.entries( - prop.items.properties - ) as any) { - if ( - (value.readOnly && value.readOnly === true) || - value.deprecated - ) { - delete prop.items.properties[key]; - } - } - } - - if (prop.readOnly && prop.readOnly === true) { - continue; - } - - if (prop.deprecated) { - continue; - } - - // Resolve schema from prop recursively - obj = sampleRequestFromProp(name, prop, obj); - } - return obj; - } - - if (type === "array") { - if (Array.isArray(items?.anyOf)) { - return processArrayItems(items, "anyOf"); - } - - if (Array.isArray(items?.oneOf)) { - return processArrayItems(items, "oneOf"); - } - - return normalizeArray(sampleRequestFromSchema(items)); - } - - if (schemaCopy.enum) { - if (schemaCopy.default) { - return schemaCopy.default; - } - return normalizeArray(schemaCopy.enum)[0]; - } - - if ( - (schema.readOnly && schema.readOnly === true) || - schemaCopy.deprecated - ) { - return undefined; - } - - return primitive(schemaCopy); - } catch (err) { - console.error( - chalk.yellow("WARNING: failed to create example from schema object:", err) - ); - return; - } + return sampleFromSchema(schema, { type: "request" }); }; - -function primitive(schema: SchemaObject = {}) { - let { type, format } = schema; - - if (type === undefined) { - return; - } - - let fn = schema.default ? () => schema.default : primitives[type].default; - - if (format !== undefined) { - fn = primitives[type][format] || fn; - } - - if (fn) { - return fn(schema); - } - - return "Unknown Type: " + schema.type; -} - -function normalizeArray(arr: any) { - if (Array.isArray(arr)) { - return arr; - } - return [arr]; -} - -function processArrayItems( - items: SchemaObject, - schemaType: "anyOf" | "oneOf" -): any[] { - const itemsArray = items[schemaType] as SchemaObject[]; - return itemsArray.map((item: SchemaObject) => { - // If items has properties, merge them with each item - if (items.properties) { - const combinedSchema = { - ...item, - properties: { - ...items.properties, // Common properties from parent - ...item.properties, // Specific properties from this anyOf/oneOf item - }, - }; - // Remove anyOf/oneOf to prevent infinite recursion when calling sampleRequestFromSchema - delete combinedSchema[schemaType]; - return sampleRequestFromSchema(combinedSchema); - } - return sampleRequestFromSchema(item); - }); -} diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts index 5526421d9..602f872b4 100644 --- a/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createResponseExample.ts @@ -5,261 +5,9 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import chalk from "chalk"; -import merge from "lodash/merge"; - +import { sampleFromSchema } from "./createSchemaExample"; import { SchemaObject } from "./types"; -import { mergeAllOf } from "../markdown/createSchema"; - -interface OASTypeToTypeMap { - string: string; - number: number; - integer: number; - boolean: boolean; - object: any; - array: any[]; - null: string | null; -} - -type Primitives = { - [OASType in keyof OASTypeToTypeMap]: { - [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; - }; -}; - -const primitives: Primitives = { - string: { - default: () => "string", - email: () => "user@example.com", - date: () => "2024-07-29", - "date-time": () => "2024-07-29T15:51:28.071Z", - uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", - hostname: () => "example.com", - ipv4: () => "198.51.100.42", - ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", - }, - number: { - default: () => 0, - float: () => 0.0, - }, - integer: { - default: () => 0, - }, - boolean: { - default: (schema) => - typeof schema.default === "boolean" ? schema.default : true, - }, - object: {}, - array: {}, - null: { - default: () => "null", - }, -}; - -function sampleResponseFromProp(name: string, prop: any, obj: any): any { - // Handle resolved circular props - if (typeof prop === "object" && Object.keys(prop).length === 0) { - obj[name] = prop; - return obj; - } - - // TODO: handle discriminators - - if (prop.oneOf) { - obj[name] = sampleResponseFromSchema(prop.oneOf[0]); - } else if (prop.anyOf) { - obj[name] = sampleResponseFromSchema(prop.anyOf[0]); - } else if (prop.allOf) { - const mergedSchemas = mergeAllOf(prop) as SchemaObject; - sampleResponseFromProp(name, mergedSchemas, obj); - } else { - obj[name] = sampleResponseFromSchema(prop); - } - return obj; -} export const sampleResponseFromSchema = (schema: SchemaObject = {}): any => { - try { - // deep copy schema before processing - let schemaCopy = JSON.parse(JSON.stringify(schema)); - let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; - - if (example !== undefined) { - return example; - } - - if (allOf) { - const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; - if (mergedSchemas.properties) { - for (const [key, value] of Object.entries(mergedSchemas.properties)) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete mergedSchemas.properties[key]; - } - } - } - if (properties) { - const combinedSchemas = merge(schemaCopy, mergedSchemas); - delete combinedSchemas.allOf; - return sampleResponseFromSchema(combinedSchemas); - } - return sampleResponseFromSchema(mergedSchemas); - } - - if (oneOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, oneOf[0]); - delete combinedSchemas.oneOf; - return sampleResponseFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleResponseFromSchema(oneOf[0]); - } - - if (anyOf) { - if (properties) { - const combinedSchemas = merge(schemaCopy, anyOf[0]); - delete combinedSchemas.anyOf; - return sampleResponseFromSchema(combinedSchemas); - } - // Just go with first schema - return sampleResponseFromSchema(anyOf[0]); - } - - if (!type) { - if (properties) { - type = "object"; - } else if (items) { - type = "array"; - } else { - return; - } - } - - if (type === "object") { - let obj: any = {}; - for (let [name, prop] of Object.entries(properties ?? {}) as any) { - if (prop.properties) { - for (const [key, value] of Object.entries(prop.properties) as any) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete prop.properties[key]; - } - } - } - - if (prop.items && prop.items.properties) { - for (const [key, value] of Object.entries( - prop.items.properties - ) as any) { - if ( - (value.writeOnly && value.writeOnly === true) || - value.deprecated - ) { - delete prop.items.properties[key]; - } - } - } - - if (prop.writeOnly && prop.writeOnly === true) { - continue; - } - - if (prop.deprecated) { - continue; - } - - // Resolve schema from prop recursively - obj = sampleResponseFromProp(name, prop, obj); - } - return obj; - } - - if (type === "array") { - if (Array.isArray(items?.anyOf)) { - return processArrayItems(items, "anyOf"); - } - - if (Array.isArray(items?.oneOf)) { - return processArrayItems(items, "oneOf"); - } - - return [sampleResponseFromSchema(items)]; - } - - if (schemaCopy.enum) { - if (schemaCopy.default) { - return schemaCopy.default; - } - return normalizeArray(schemaCopy.enum)[0]; - } - - if ( - (schemaCopy.writeOnly && schemaCopy.writeOnly === true) || - schemaCopy.deprecated - ) { - return undefined; - } - - return primitive(schemaCopy); - } catch (err) { - console.error( - chalk.yellow("WARNING: failed to create example from schema object:", err) - ); - return; - } + return sampleFromSchema(schema, { type: "response" }); }; - -function primitive(schema: SchemaObject = {}) { - let { type, format } = schema; - - if (type === undefined) { - return; - } - - let fn = schema.default ? () => schema.default : primitives[type].default; - - if (format !== undefined) { - fn = primitives[type][format] || fn; - } - - if (fn) { - return fn(schema); - } - - return "Unknown Type: " + schema.type; -} - -function normalizeArray(arr: any) { - if (Array.isArray(arr)) { - return arr; - } - return [arr]; -} - -function processArrayItems( - items: SchemaObject, - schemaType: "anyOf" | "oneOf" -): any[] { - const itemsArray = items[schemaType] as SchemaObject[]; - return itemsArray.map((item: SchemaObject) => { - // If items has properties, merge them with each item - if (items.properties) { - const combinedSchema = { - ...item, - properties: { - ...items.properties, // Common properties from parent - ...item.properties, // Specific properties from this anyOf/oneOf item - }, - }; - // Remove anyOf/oneOf to prevent infinite recursion when calling sampleResponseFromSchema - delete combinedSchema[schemaType]; - return sampleResponseFromSchema(combinedSchema); - } - return sampleResponseFromSchema(item); - }); -} diff --git a/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts new file mode 100644 index 000000000..f8a6b2690 --- /dev/null +++ b/packages/docusaurus-plugin-openapi-docs/src/openapi/createSchemaExample.ts @@ -0,0 +1,292 @@ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import chalk from "chalk"; +import merge from "lodash/merge"; + +import { SchemaObject } from "./types"; +import { mergeAllOf } from "../markdown/createSchema"; + +interface OASTypeToTypeMap { + string: string; + number: number; + integer: number; + boolean: boolean; + object: any; + array: any[]; + null: string | null; +} + +type Primitives = { + [OASType in keyof OASTypeToTypeMap]: { + [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; + }; +}; + +const primitives: Primitives = { + string: { + default: () => "string", + email: () => "user@example.com", + date: () => "2024-07-29", + "date-time": () => "2024-07-29T15:51:28.071Z", + uuid: () => "3fa85f64-5717-4562-b3fc-2c963f66afa6", + hostname: () => "example.com", + ipv4: () => "198.51.100.42", + ipv6: () => "2001:0db8:5b96:0000:0000:426f:8e17:642a", + }, + number: { + default: () => 0, + float: () => 0.0, + }, + integer: { + default: () => 0, + }, + boolean: { + default: (schema) => + typeof schema.default === "boolean" ? schema.default : true, + }, + object: {}, + array: {}, + null: { + default: () => "null", + }, +}; + +export type ExampleType = "request" | "response"; + +export interface ExampleContext { + type: ExampleType; +} + +function shouldExcludeProperty( + prop: SchemaObject, + context: ExampleContext +): boolean { + if (prop.deprecated) { + return true; + } + + if (context.type === "request") { + return prop.readOnly === true; + } else { + return prop.writeOnly === true; + } +} + +function sampleFromProp( + name: string, + prop: any, + obj: any, + context: ExampleContext +): any { + // Handle resolved circular props + if (typeof prop === "object" && Object.keys(prop).length === 0) { + obj[name] = prop; + return obj; + } + + // TODO: handle discriminators + + if (prop.oneOf) { + obj[name] = sampleFromSchema(prop.oneOf[0], context); + } else if (prop.anyOf) { + obj[name] = sampleFromSchema(prop.anyOf[0], context); + } else if (prop.allOf) { + const mergedSchemas = mergeAllOf(prop) as SchemaObject; + sampleFromProp(name, mergedSchemas, obj, context); + } else { + obj[name] = sampleFromSchema(prop, context); + } + return obj; +} + +export const sampleFromSchema = ( + schema: SchemaObject = {}, + context: ExampleContext +): any => { + try { + // deep copy schema before processing + let schemaCopy = JSON.parse(JSON.stringify(schema)); + let { type, example, allOf, properties, items, oneOf, anyOf } = schemaCopy; + + if (example !== undefined) { + return example; + } + + if (oneOf) { + if (properties) { + const combinedSchemas = merge(schemaCopy, oneOf[0]); + delete combinedSchemas.oneOf; + return sampleFromSchema(combinedSchemas, context); + } + // Just go with first schema + return sampleFromSchema(oneOf[0], context); + } + + if (anyOf) { + if (properties) { + const combinedSchemas = merge(schemaCopy, anyOf[0]); + delete combinedSchemas.anyOf; + return sampleFromSchema(combinedSchemas, context); + } + // Just go with first schema + return sampleFromSchema(anyOf[0], context); + } + + if (allOf) { + const mergedSchemas = mergeAllOf(schemaCopy) as SchemaObject; + if (mergedSchemas.properties) { + for (const [key, value] of Object.entries(mergedSchemas.properties)) { + if (shouldExcludeProperty(value, context)) { + delete mergedSchemas.properties[key]; + } + } + } + if (properties) { + const combinedSchemas = merge(schemaCopy, mergedSchemas); + delete combinedSchemas.allOf; + return sampleFromSchema(combinedSchemas, context); + } + return sampleFromSchema(mergedSchemas, context); + } + + if (!type) { + if (properties) { + type = "object"; + } else if (items) { + type = "array"; + } else { + return; + } + } + + if (type === "object") { + let obj: any = {}; + for (let [name, prop] of Object.entries(properties ?? {}) as any) { + if (prop.properties) { + for (const [key, value] of Object.entries(prop.properties) as any) { + if (shouldExcludeProperty(value, context)) { + delete prop.properties[key]; + } + } + } + + if (prop.items && prop.items.properties) { + for (const [key, value] of Object.entries( + prop.items.properties + ) as any) { + if (shouldExcludeProperty(value, context)) { + delete prop.items.properties[key]; + } + } + } + + if (shouldExcludeProperty(prop, context)) { + continue; + } + + // Resolve schema from prop recursively + obj = sampleFromProp(name, prop, obj, context); + } + return obj; + } + + if (type === "array") { + if (Array.isArray(items?.anyOf)) { + return processArrayItems(items, "anyOf", context); + } + + if (Array.isArray(items?.oneOf)) { + return processArrayItems(items, "oneOf", context); + } + + return normalizeArray(sampleFromSchema(items, context)); + } + + if (schemaCopy.enum) { + if (schemaCopy.default) { + return schemaCopy.default; + } + return normalizeArray(schemaCopy.enum)[0]; + } + + if (shouldExcludeProperty(schemaCopy, context)) { + return undefined; + } + + return primitive(schemaCopy); + } catch (err) { + console.error( + chalk.yellow("WARNING: failed to create example from schema object:", err) + ); + return; + } +}; + +function primitive(schema: SchemaObject = {}) { + let { type, format } = schema; + + if (type === undefined) { + return; + } + + // If type is an array, use the first type + if (Array.isArray(type)) { + type = type[0]; + if (type === undefined) { + return; + } + } + + // Use schema default if available, otherwise use type default + if (schema.default !== undefined) { + return schema.default; + } + + const typeConfig = primitives[type]; + if (typeConfig) { + if (format !== undefined && typeConfig[format] !== undefined) { + return typeConfig[format](schema); + } + if (typeConfig.default !== undefined) { + return typeConfig.default(schema); + } + } + + return "Unknown Type: " + schema.type; +} + +function normalizeArray(arr: any) { + if (Array.isArray(arr)) { + return arr; + } + return [arr]; +} + +function processArrayItems( + items: SchemaObject, + schemaType: "anyOf" | "oneOf", + context: ExampleContext +): any[] { + const itemsArray = items[schemaType] as SchemaObject[]; + return itemsArray.map((item: SchemaObject) => { + // If items has properties, merge them with each item + if (items.properties) { + const combinedSchema = { + ...item, + properties: { + ...items.properties, // Common properties from parent + ...item.properties, // Specific properties from this anyOf/oneOf item + }, + }; + // Remove anyOf/oneOf to prevent infinite recursion when calling sampleFromSchema + delete combinedSchema[schemaType]; + return sampleFromSchema(combinedSchema, context); + } + return sampleFromSchema(item, context); + }); +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/BaseSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/BaseSchema/index.tsx new file mode 100644 index 000000000..3758af6fc --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/BaseSchema/index.tsx @@ -0,0 +1,159 @@ +/* ============================================================================ + * Copyright (c) Palo Alto Networks + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import React from "react"; + +import BrowserOnly from "@docusaurus/BrowserOnly"; +import Details from "@theme/Details"; +import { + ExampleFromSchema, + MimeExample, + MimeExamples, + SchemaExample, + SchemaExamples, +} from "@theme/Examples"; +import Markdown from "@theme/Markdown"; +import MimeTabs from "@theme/MimeTabs"; +import SchemaNode from "@theme/Schema"; +import SchemaTabs from "@theme/SchemaTabs"; +import SkeletonLoader from "@theme/SkeletonLoader"; +import TabItem from "@theme/TabItem"; +import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; + +interface Props { + style?: React.CSSProperties; + title: string; + body: { + content?: { + [key: string]: MediaTypeObject; + }; + description?: string; + required?: string[] | boolean; + }; + schemaType: "request" | "response"; +} + +const BaseSchemaComponent: React.FC = ({ + title, + body, + style, + schemaType, +}) => { + if ( + body === undefined || + body.content === undefined || + Object.keys(body).length === 0 || + Object.keys(body.content).length === 0 + ) { + return null; + } + + const mimeTypes = Object.keys(body.content); + if (mimeTypes && mimeTypes.length) { + return ( + + {mimeTypes.map((mimeType: any) => { + const mimeExamples = body.content![mimeType].examples; + const mimeExample = body.content![mimeType].example; + const schemaExamples = body.content![mimeType].schema?.examples; + const schemaExample = body.content![mimeType].schema?.example; + const firstBody = body.content![mimeType].schema; + + if ( + firstBody === undefined || + (firstBody.properties && + Object.keys(firstBody.properties).length === 0) + ) { + return null; + } + + if (firstBody) { + const tabTitle = "Schema"; + return ( + // @ts-ignore + + + {/* @ts-ignore */} + +
+ + + {title} + {body.required && ( + + required + + )} + + + + } + > +
+ {body.description && ( +
+ {body.description} +
+ )} +
+
    + +
+
+
+ {firstBody && + ExampleFromSchema({ + schema: firstBody, + mimeType, + context: { type: schemaType }, + })} + + {mimeExamples && + MimeExamples({ examples: mimeExamples, mimeType })} + + {mimeExample && + MimeExample({ example: mimeExample, mimeType })} + + {schemaExamples && + SchemaExamples({ examples: schemaExamples, mimeType })} + + {schemaExample && + SchemaExample({ example: schemaExample, mimeType })} +
+
+ ); + } + return null; + })} +
+ ); + } + return null; +}; + +const BaseSchema: React.FC = (props) => { + return ( + }> + {() => { + return ; + }} + + ); +}; + +export default BaseSchema; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/_CodeSamples.scss b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/_CodeSamples.scss new file mode 100644 index 000000000..2b79a3633 --- /dev/null +++ b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/_CodeSamples.scss @@ -0,0 +1,3 @@ +.openapi-code__code-samples-container { + margin-top: 2rem; +} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/index.tsx similarity index 62% rename from packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/index.tsx rename to packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/index.tsx index dc0f759ab..9fbde2345 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/CodeSamples/index.tsx @@ -11,21 +11,16 @@ import CodeBlock from "@theme/CodeBlock"; import { Language } from "prism-react-renderer"; export interface Props { - readonly responseExample: string; + readonly example: string; readonly language: Language; } -function ResponseSamples({ - responseExample, - language, -}: Props): React.JSX.Element { +function CodeSamples({ example, language }: Props): React.JSX.Element { return ( -
- - {responseExample} - +
+ {example}
); } -export default ResponseSamples; +export default CodeSamples; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx similarity index 53% rename from packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx rename to packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx index 67eb5361e..5d8d779b7 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseExamples/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Examples/index.tsx @@ -7,10 +7,13 @@ import React from "react"; +import CodeSamples from "@theme/CodeSamples"; import Markdown from "@theme/Markdown"; -import ResponseSamples from "@theme/ResponseSamples"; import TabItem from "@theme/TabItem"; -import { sampleResponseFromSchema } from "docusaurus-plugin-openapi-docs/lib/openapi/createResponseExample"; +import { + sampleFromSchema, + ExampleContext, +} from "docusaurus-plugin-openapi-docs/lib/openapi/createSchemaExample"; import format from "xml-formatter"; export function json2xml(o: Record, tab: string): string { @@ -50,23 +53,56 @@ export function json2xml(o: Record, tab: string): string { return tab ? xml.replace(/\t/g, tab) : xml.replace(/\t|\n/g, ""); } -interface ResponseExamplesProps { - responseExamples: any; +export function getLanguageFromMimeType(mimeType: string): string { + let language = "shell"; + if (mimeType.endsWith("json")) language = "json"; + if (mimeType.endsWith("xml")) language = "xml"; + return language; +} + +export interface MimeExampleProps { + example: any; mimeType: string; } -export const ResponseExamples: React.FC = ({ - responseExamples, + +export const MimeExample: React.FC = ({ + example, + mimeType, +}) => { + const language = getLanguageFromMimeType(mimeType); + + const isObject = typeof example === "object"; + const exampleContent = isObject ? JSON.stringify(example, null, 2) : example; + + return ( + // @ts-ignore + + {example.summary && ( + + {example.summary} + + )} + + + ); +}; + +export interface MimeExamplesProps { + examples: any; + mimeType: string; +} + +export const MimeExamples: React.FC = ({ + examples, mimeType, }): any => { - let language = "shell"; - if (mimeType.endsWith("json")) language = "json"; - if (mimeType.endsWith("xml")) language = "xml"; + const language = getLanguageFromMimeType(mimeType); - // Map response examples to an array of TabItem elements - const examplesArray = Object.entries(responseExamples).map( + // Map examples to an array of TabItem elements + const examplesArray = Object.entries(examples).map( ([exampleName, exampleValue]: any) => { const isObject = typeof exampleValue.value === "object"; - const responseExample = isObject + const exampleContent = isObject ? JSON.stringify(exampleValue.value, null, 2) : exampleValue.value; @@ -78,10 +114,7 @@ export const ResponseExamples: React.FC = ({ {exampleValue.summary} )} - + ); } @@ -90,70 +123,94 @@ export const ResponseExamples: React.FC = ({ return examplesArray; }; -interface ResponseExampleProps { - responseExample: any; +export interface SchemaExampleProps { + example: any; mimeType: string; } -export const ResponseExample: React.FC = ({ - responseExample, +export const SchemaExample: React.FC = ({ + example, mimeType, }) => { - let language = "shell"; - if (mimeType.endsWith("json")) { - language = "json"; - } - if (mimeType.endsWith("xml")) { - language = "xml"; - } + const language = getLanguageFromMimeType(mimeType); - const isObject = typeof responseExample === "object"; - const exampleContent = isObject - ? JSON.stringify(responseExample, null, 2) - : responseExample; + const isObject = typeof example === "object"; + const exampleContent = isObject ? JSON.stringify(example, null, 2) : example; return ( // @ts-ignore - {responseExample.summary && ( + {example.summary && ( - {responseExample.summary} + {example.summary} )} - + ); }; -interface ExampleFromSchemaProps { +export interface SchemaExamplesProps { + examples: any[]; + mimeType: string; +} + +export const SchemaExamples: React.FC = ({ + examples, + mimeType, +}) => { + const language = getLanguageFromMimeType(mimeType); + + // Map examples to an array of TabItem elements + const examplesArray = examples.map((example: any, i: number) => { + const exampleName = `Example ${i + 1}`; + const isObject = typeof example === "object"; + const exampleContent = isObject + ? JSON.stringify(example, null, 2) + : example; + + return ( + // @ts-ignore + + + + ); + }); + + return examplesArray; +}; + +export interface ExampleFromSchemaProps { schema: any; mimeType: string; + context: ExampleContext; } export const ExampleFromSchema: React.FC = ({ schema, mimeType, + context, }) => { - const responseExample = sampleResponseFromSchema(schema); + const example = sampleFromSchema(schema, context); if (mimeType.endsWith("xml")) { - let responseExampleObject; + let exampleObject; try { - responseExampleObject = JSON.parse(JSON.stringify(responseExample)); + exampleObject = JSON.parse(JSON.stringify(example)); } catch { return null; } - if (typeof responseExampleObject === "object") { + if (typeof exampleObject === "object") { let xmlExample; try { - xmlExample = format(json2xml(responseExampleObject, ""), { + xmlExample = format(json2xml(exampleObject, ""), { indentation: " ", lineSeparator: "\n", collapseContent: true, }); } catch { - const xmlExampleWithRoot = { root: responseExampleObject }; + const xmlExampleWithRoot = { root: exampleObject }; try { xmlExample = format(json2xml(xmlExampleWithRoot, ""), { indentation: " ", @@ -161,27 +218,24 @@ export const ExampleFromSchema: React.FC = ({ collapseContent: true, }); } catch { - xmlExample = json2xml(responseExampleObject, ""); + xmlExample = json2xml(exampleObject, ""); } } return ( // @ts-ignore - + ); } } - if ( - typeof responseExample === "object" || - typeof responseExample === "string" - ) { + if (typeof example === "object" || typeof example === "string") { return ( // @ts-ignore - diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss b/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss index 96e992073..41a8f8b92 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss +++ b/packages/docusaurus-theme-openapi-docs/src/theme/Markdown/Details/_Details.scss @@ -24,11 +24,7 @@ /* Top-Level Details Caret Styling */ .openapi-left-panel__container > .openapi-markdown__details > summary::before, .openapi-markdown__details.mime > summary::before { - top: 0.1rem; -} - -.openapi-markdown__details.response > summary::before { - top: 0.25rem; /* TODO: figure out why this is necessary */ + top: 0.25rem; } /* End of Top-Level Details Caret Styling */ diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx index 9eeceb4bd..00ec65e24 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/RequestSchema/index.tsx @@ -7,13 +7,7 @@ import React from "react"; -import BrowserOnly from "@docusaurus/BrowserOnly"; -import Details from "@theme/Details"; -import Markdown from "@theme/Markdown"; -import MimeTabs from "@theme/MimeTabs"; // Assume these components exist -import SchemaNode from "@theme/Schema"; -import SkeletonLoader from "@theme/SkeletonLoader"; -import TabItem from "@theme/TabItem"; +import BaseSchema from "@theme/BaseSchema"; import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; interface Props { @@ -28,130 +22,8 @@ interface Props { }; } -const RequestSchemaComponent: React.FC = ({ title, body, style }) => { - if ( - body === undefined || - body.content === undefined || - Object.keys(body).length === 0 || - Object.keys(body.content).length === 0 - ) { - return null; - } - - const mimeTypes = Object.keys(body.content); - - if (mimeTypes.length > 1) { - return ( - - {mimeTypes.map((mimeType) => { - const firstBody = body.content![mimeType].schema; - if ( - firstBody === undefined || - (firstBody.properties && - Object.keys(firstBody.properties).length === 0) - ) { - return null; - } - return ( - // @ts-ignore - -
- -

- {title} - {body.required === true && ( - - required - - )} -

-
- - } - > -
- {body.description && ( -
- {body.description} -
- )} -
-
    - -
-
-
- ); - })} -
- ); - } - - const randomFirstKey = mimeTypes[0]; - const firstBody = - body.content[randomFirstKey].schema ?? body.content![randomFirstKey]; - - if (firstBody === undefined) { - return null; - } - - return ( - - {/* @ts-ignore */} - -
- -

- {title} - {firstBody.type === "array" && ( - array - )} - {body.required && ( - - required - - )} -

-
- - } - > -
- {body.description && ( -
- {body.description} -
- )} -
-
    - -
-
-
-
- ); -}; - const RequestSchema: React.FC = (props) => { - return ( - }> - {() => { - return ; - }} - - ); + return ; }; export default RequestSchema; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/_ResponseSamples.scss b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/_ResponseSamples.scss deleted file mode 100644 index 798db36bd..000000000 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSamples/_ResponseSamples.scss +++ /dev/null @@ -1,3 +0,0 @@ -.openapi-code__response-samples-container { - margin-top: 2rem; -} diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx index ac4a64954..023385765 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx +++ b/packages/docusaurus-theme-openapi-docs/src/theme/ResponseSchema/index.tsx @@ -7,19 +7,7 @@ import React from "react"; -import BrowserOnly from "@docusaurus/BrowserOnly"; -import Details from "@theme/Details"; -import Markdown from "@theme/Markdown"; -import MimeTabs from "@theme/MimeTabs"; // Assume these components exist -import { - ExampleFromSchema, - ResponseExample, - ResponseExamples, -} from "@theme/ResponseExamples"; -import SchemaNode from "@theme/Schema"; -import SchemaTabs from "@theme/SchemaTabs"; -import SkeletonLoader from "@theme/SkeletonLoader"; -import TabItem from "@theme/TabItem"; +import BaseSchema from "@theme/BaseSchema"; import { MediaTypeObject } from "docusaurus-plugin-openapi-docs/lib/openapi/types"; interface Props { @@ -34,111 +22,8 @@ interface Props { }; } -const ResponseSchemaComponent: React.FC = ({ - title, - body, - style, -}): any => { - if ( - body === undefined || - body.content === undefined || - Object.keys(body).length === 0 || - Object.keys(body.content).length === 0 - ) { - return null; - } - - // Get all MIME types, including vendor-specific - const mimeTypes = Object.keys(body.content); - if (mimeTypes && mimeTypes.length) { - return ( - - {mimeTypes.map((mimeType: any) => { - const responseExamples = body.content![mimeType].examples; - const responseExample = body.content![mimeType].example; - const firstBody: any = - body.content![mimeType].schema ?? body.content![mimeType]; - - if ( - firstBody === undefined && - responseExample === undefined && - responseExamples === undefined - ) { - return undefined; - } - - if (firstBody) { - return ( - // @ts-ignore - - - {/* @ts-ignore */} - -
- - - {title} - {body.required === true && ( - - required - - )} - - - - } - > -
- {body.description && ( -
- {body.description} -
- )} -
-
    - -
-
-
- {firstBody && - ExampleFromSchema({ - schema: firstBody, - mimeType: mimeType, - })} - - {responseExamples && - ResponseExamples({ responseExamples, mimeType })} - - {responseExample && - ResponseExample({ responseExample, mimeType })} -
-
- ); - } - return undefined; - })} -
- ); - } - return undefined; -}; - const ResponseSchema: React.FC = (props) => { - return ( - }> - {() => { - return ; - }} - - ); + return ; }; export default ResponseSchema; diff --git a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss index db8a648ee..da1b29937 100644 --- a/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss +++ b/packages/docusaurus-theme-openapi-docs/src/theme/styles.scss @@ -38,7 +38,7 @@ @use "./SchemaTabs/SchemaTabs"; @use "./OperationTabs/OperationTabs"; /* Code Samples */ -@use "./ResponseSamples/ResponseSamples"; +@use "./CodeSamples/CodeSamples"; /* Markdown Styling */ @use "./Markdown/Details/Details";