diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..ffe8fb63 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,16 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +module.exports = { + preset: "ts-jest", + testEnvironment: "node", + roots: [ + "/packages/docusaurus-plugin-openapi/src", + "/packages/docusaurus-preset-openapi/src", + "/packages/docusaurus-theme-openapi/src", + ], +}; diff --git a/packages/docusaurus-plugin-openapi/src/markdown/createParamsTable.ts b/packages/docusaurus-plugin-openapi/src/markdown/createParamsTable.ts index 76279644..20cd5537 100644 --- a/packages/docusaurus-plugin-openapi/src/markdown/createParamsTable.ts +++ b/packages/docusaurus-plugin-openapi/src/markdown/createParamsTable.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { ApiItem, ExampleObject, ParameterObject } from "../types"; +import { ApiItem } from "../types"; import { createDescription } from "./createDescription"; import { createFullWidthTable } from "./createFullWidthTable"; import { getSchemaName } from "./schema"; @@ -20,9 +20,7 @@ export function createParamsTable({ parameters, type }: Props) { if (parameters === undefined) { return undefined; } - const params = parameters.filter( - (param: any) => param?.in === type - ) as ParameterObject[]; + const params = parameters.filter((param) => param?.in === type); if (params.length === 0) { return undefined; } @@ -79,9 +77,7 @@ export function createParamsTable({ parameters, type }: Props) { style: { marginTop: "var(--ifm-table-cell-padding)" }, children: Object.entries(examples).map(([k, v]) => create("div", { - children: `Example (${k}): ${ - (v as ExampleObject).value - }`, + children: `Example (${k}): ${v.value}`, }) ), }) diff --git a/packages/docusaurus-plugin-openapi/src/markdown/createSchemaTable.ts b/packages/docusaurus-plugin-openapi/src/markdown/createSchemaTable.ts index 75d5cbf1..4e5ddfa0 100644 --- a/packages/docusaurus-plugin-openapi/src/markdown/createSchemaTable.ts +++ b/packages/docusaurus-plugin-openapi/src/markdown/createSchemaTable.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { Map, MediaTypeObject, SchemaObject } from "../types"; +import { MediaTypeObject, SchemaObject } from "../openapi/types"; import { createDescription } from "./createDescription"; import { createFullWidthTable } from "./createFullWidthTable"; import { getSchemaName } from "./schema"; @@ -64,11 +64,13 @@ function createRows({ schema }: RowsProps): string | undefined { marginBottom: "0px", }, children: create("tbody", { - children: Object.keys(schema.properties).map((key) => + children: Object.entries(schema.properties).map(([key, val]) => createRow({ name: key, - schema: schema.properties[key], - required: schema.required?.includes(key), + schema: val, + required: Array.isArray(schema.required) + ? schema.required.includes(key) + : false, }) ), }), @@ -91,11 +93,13 @@ interface RowsRootProps { function createRowsRoot({ schema }: RowsRootProps) { // object if (schema.properties !== undefined) { - return Object.keys(schema.properties).map((key) => + return Object.entries(schema.properties).map(([key, val]) => createRow({ name: key, - schema: schema.properties[key], - required: schema.required?.includes(key), + schema: val, + required: Array.isArray(schema.required) + ? schema.required.includes(key) + : false, }) ); } @@ -128,7 +132,9 @@ interface Props { style?: any; title: string; body: { - content: Map; + content: { + [key: string]: MediaTypeObject; + }; description?: string; required?: boolean; }; @@ -144,7 +150,11 @@ export function createSchemaTable({ title, body, ...rest }: Props) { const randomFirstKey = Object.keys(body.content)[0]; - const firstBody = body.content[randomFirstKey].schema as SchemaObject; + const firstBody = body.content[randomFirstKey].schema; + + if (firstBody === undefined) { + return undefined; + } // we don't show the table if there is no properties to show if (Object.keys(firstBody.properties ?? {}).length === 0) { diff --git a/packages/docusaurus-plugin-openapi/src/openapi.ts b/packages/docusaurus-plugin-openapi/src/openapi.ts deleted file mode 100644 index 6462e787..00000000 --- a/packages/docusaurus-plugin-openapi/src/openapi.ts +++ /dev/null @@ -1,290 +0,0 @@ -/* ============================================================================ - * Copyright (c) Cloud Annotations - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * ========================================================================== */ - -import { normalizeUrl } from "@docusaurus/utils"; -import fs from "fs-extra"; -import yaml from "js-yaml"; -import JsonRefs from "json-refs"; -import { kebabCase } from "lodash"; -import Converter from "openapi-to-postmanv2"; -import sdk, { Collection } from "postman-collection"; - -import { sampleFromSchema } from "./createExample"; -import { - OpenApiObject, - PathItemObject, - ApiSection, - OperationObject, - ServerObject, - ReferenceObject, - ParameterObject, - ApiItem, - RequestBodyObject, - SchemaObject, - HttpSecuritySchemeObject, - ApiKeySecuritySchemeObject, - Oauth2SecuritySchemeObject, - OpenIdConnectSecuritySchemeObject, -} from "./types"; - -function isHttpSecuritySchemeObject( - item: - | ReferenceObject - | ApiKeySecuritySchemeObject - | HttpSecuritySchemeObject - | Oauth2SecuritySchemeObject - | OpenIdConnectSecuritySchemeObject -): item is HttpSecuritySchemeObject { - return (item as HttpSecuritySchemeObject).type === "http"; -} - -function isOperationObject( - item: - | string - | PathItemObject - | ServerObject[] - | ReferenceObject[] - | ParameterObject[] -): item is OperationObject { - return (item as OperationObject).responses !== undefined; -} - -function getPaths(spec: OpenApiObject): ApiItem[] { - const seen: { [key: string]: number } = {}; - return Object.entries(spec.paths) - .map(([path, pathObject]) => { - const entries = Object.entries(pathObject); - return entries.map(([key, val]) => { - if (isOperationObject(val)) { - let method = key; - let operationObject = val as OperationObject; - - const title = - operationObject.summary ?? - operationObject.operationId ?? - "Missing summary"; - if (operationObject.description === undefined) { - operationObject.description = - operationObject.summary ?? operationObject.operationId ?? ""; - } - - const baseId = kebabCase(title); - let count = seen[baseId]; - - let id; - if (count) { - id = `${baseId}-${count}`; - seen[baseId] = count + 1; - } else { - id = baseId; - seen[baseId] = 1; - } - - const servers = - operationObject.servers ?? pathObject.servers ?? spec.servers; - - // TODO: Don't include summary temporarilly - const { summary, ...defaults } = operationObject; - - return { - ...defaults, - id, - title, - method, - path, - servers, - }; - } - return undefined; - }); - }) - .flat() - .filter((item) => item !== undefined) as ApiItem[]; -} - -function organizeSpec(spec: OpenApiObject) { - const paths = getPaths(spec); - - let tagNames: string[] = []; - let tagged: ApiSection[] = []; - if (spec.tags) { - tagged = spec.tags - .map((tag) => { - return { - title: tag.name, - description: tag.description || "", - items: paths.filter((p) => p.tags && p.tags.includes(tag.name)), - }; - }) - .filter((i) => i.items.length > 0); - tagNames = tagged.map((t) => t.title); - } - - const all = [ - ...tagged, - { - title: "API", - description: "", - items: paths.filter((p) => { - if (p.tags === undefined || p.tags.length === 0) { - return true; - } - for (let tag of p.tags) { - if (tagNames.includes(tag)) { - return false; - } - } - return true; - }), - }, - ]; - - return all; -} - -async function convertToPostman( - openapiData: OpenApiObject -): Promise { - // The conversions mutates whatever you pass here, create a new object. - const openapiClone = JSON.parse(JSON.stringify(openapiData)); - - // seems to be a weird bug with postman and servers... - delete openapiClone.servers; - for (let value of Object.values(openapiClone.paths)) { - let pathItemObject = value as PathItemObject; - delete pathItemObject.servers; - delete pathItemObject.get?.servers; - delete pathItemObject.put?.servers; - delete pathItemObject.post?.servers; - delete pathItemObject.delete?.servers; - delete pathItemObject.options?.servers; - delete pathItemObject.head?.servers; - delete pathItemObject.patch?.servers; - delete pathItemObject.trace?.servers; - } - - return await new Promise((resolve, reject) => { - Converter.convert( - { - type: "json", - data: openapiClone, - }, - {}, - (_: any, conversionResult: any) => { - if (!conversionResult.result) { - reject(conversionResult.reason); - return; - } else { - return resolve(new sdk.Collection(conversionResult.output[0].data)); - } - } - ); - }); -} - -export async function loadOpenapi( - openapiPath: string, - baseUrl: string, - routeBasePath: string -) { - const openapiString = await fs.readFile(openapiPath, "utf-8"); - const openapiData = yaml.load(openapiString) as OpenApiObject; - - // Attach a postman request object to the openapi spec. - const postmanCollection = await convertToPostman(openapiData); - postmanCollection.forEachItem((item) => { - const method = item.request.method.toLowerCase(); - // NOTE: This doesn't catch all variables for some reason... - // item.request.url.variables.each((pathVar) => { - // pathVar.value = `{${pathVar.key}}`; - // }); - const path = item.request.url - .getPath({ unresolved: true }) - .replace(/:([a-z0-9-_]+)/gi, "{$1}"); - - switch (method) { - case "get": - case "put": - case "post": - case "delete": - case "options": - case "head": - case "patch": - case "trace": - if (!openapiData.paths[path]) { - break; - } - - const operationObject = openapiData.paths[path][method]; - if (operationObject) { - operationObject.postman = item.request; - } - break; - default: - break; - } - }); - - const { resolved: dereffed } = await JsonRefs.resolveRefs(openapiData); - - const dereffedSpec = dereffed as OpenApiObject; - - const order = organizeSpec(dereffedSpec); - - order.forEach((category, i) => { - category.items.forEach((item, ii) => { - // don't override already defined servers. - if (item.servers === undefined) { - item.servers = dereffedSpec.servers; - } - - if (item.security === undefined) { - item.security = dereffedSpec.security; - } - - // Add security schemes so we know how to handle security. - item.securitySchemes = dereffedSpec.components?.securitySchemes; - - // Make sure schemes are lowercase. See: https://github.com/cloud-annotations/docusaurus-plugin-openapi/issues/79 - Object.values(item.securitySchemes ?? {}).forEach((auth) => { - if (isHttpSecuritySchemeObject(auth)) { - auth.scheme = auth.scheme.toLowerCase(); - } - }); - - item.permalink = normalizeUrl([baseUrl, routeBasePath, item.id]); - - const prev = - order[i].items[ii - 1] || - order[i - 1]?.items[order[i - 1].items.length - 1]; - const next = - order[i].items[ii + 1] || (order[i + 1] ? order[i + 1].items[0] : null); - - if (prev) { - item.previous = { - title: prev.title, - permalink: normalizeUrl([baseUrl, routeBasePath, prev.id]), - }; - } - - if (next) { - item.next = { - title: next.title, - permalink: normalizeUrl([baseUrl, routeBasePath, next.id]), - }; - } - - const content = (item.requestBody as RequestBodyObject)?.content; - const schema = content?.["application/json"]?.schema as SchemaObject; - if (schema) { - item.jsonRequestBodyExample = sampleFromSchema(schema); - } - }); - }); - - return order; -} diff --git a/packages/docusaurus-plugin-openapi/src/createExample.ts b/packages/docusaurus-plugin-openapi/src/openapi/createExample.ts similarity index 79% rename from packages/docusaurus-plugin-openapi/src/createExample.ts rename to packages/docusaurus-plugin-openapi/src/openapi/createExample.ts index debbc5ea..e94b1b16 100644 --- a/packages/docusaurus-plugin-openapi/src/createExample.ts +++ b/packages/docusaurus-plugin-openapi/src/openapi/createExample.ts @@ -5,7 +5,22 @@ * LICENSE file in the root directory of this source tree. * ========================================================================== */ -import { Primitives, Schema, Type } from "./types"; +import { SchemaObject } from "./types"; + +interface OASTypeToTypeMap { + string: string; + number: number; + integer: number; + boolean: boolean; + object: any; + array: any[]; +} + +type Primitives = { + [OASType in keyof OASTypeToTypeMap]: { + [format: string]: (schema: SchemaObject) => OASTypeToTypeMap[OASType]; + }; +}; const primitives: Primitives = { string: { @@ -25,14 +40,14 @@ const primitives: Primitives = { default: () => 0, }, boolean: { - default: (schema: Schema) => + default: (schema) => typeof schema.default === "boolean" ? schema.default : true, }, object: {}, array: {}, }; -export const sampleFromSchema = (schema: Schema = {}): any => { +export const sampleFromSchema = (schema: SchemaObject = {}): any => { let { type, example, properties, items } = schema; if (example !== undefined) { @@ -41,15 +56,15 @@ export const sampleFromSchema = (schema: Schema = {}): any => { if (!type) { if (properties) { - type = Type.object; + type = "object"; } else if (items) { - type = Type.array; + type = "array"; } else { return; } } - if (type === Type.object) { + if (type === "object") { let obj: any = {}; for (let [name, prop] of Object.entries(properties || {})) { if (prop && prop.deprecated) { @@ -60,7 +75,7 @@ export const sampleFromSchema = (schema: Schema = {}): any => { return obj; } - if (type === Type.array) { + if (type === "array") { if (Array.isArray(items?.anyOf)) { return items?.anyOf.map((item) => sampleFromSchema(item)); } @@ -82,7 +97,7 @@ export const sampleFromSchema = (schema: Schema = {}): any => { return primitive(schema); }; -function primitive(schema: Schema = {}) { +function primitive(schema: SchemaObject = {}) { let { type, format } = schema; if (type === undefined) { diff --git a/packages/docusaurus-plugin-openapi/src/openapi/index.ts b/packages/docusaurus-plugin-openapi/src/openapi/index.ts new file mode 100644 index 00000000..25d8590c --- /dev/null +++ b/packages/docusaurus-plugin-openapi/src/openapi/index.ts @@ -0,0 +1,8 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +export { loadOpenapi } from "./load"; diff --git a/packages/docusaurus-plugin-openapi/src/openapi/load.test.ts b/packages/docusaurus-plugin-openapi/src/openapi/load.test.ts new file mode 100644 index 00000000..c989ab52 --- /dev/null +++ b/packages/docusaurus-plugin-openapi/src/openapi/load.test.ts @@ -0,0 +1,210 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import { _loadOpenapi, _newLoadOpenapi } from "./load"; +import { OpenApiObjectWithRef } from "./types"; + +const example: OpenApiObjectWithRef = { + openapi: "3.0.3", + info: { title: "Cloud Object Storage", version: "1.0.1" }, + servers: [ + { + url: "https://s3.{region}.cloud-object-storage.appdomain.cloud", + variables: { + region: { + enum: [ + "us", + "eu", + "ap", + "us-south", + "us-east", + "eu-gb", + "eu-de", + "au-syd", + "jp-tok", + "ams03", + "che01", + "hkg02", + "mex01", + "mil01", + "mon01", + "osl01", + "par01", + "sjc04", + "sao01", + "seo01", + "sng01", + "tor01", + ], + default: "us", + }, + }, + }, + ], + paths: { + "/identity/token": { + post: { + tags: ["Authentication"], + summary: "Generating an IAM token", + description: + "Generate an IBM Cloud® Identity and Access Management (IAM) token by using either your [IAM API key](https://cloud.ibm.com/docs/iam?topic=iam-userapikey#userapikey) or a [service ID's API key](https://cloud.ibm.com/docs/iam?topic=iam-serviceidapikeys#serviceidapikeys) IBM Cloud APIs can be accessed only by users who are authorized by an assigned IAM role.\nEach user who is calling the API must pass credentials for the API to authenticate.\n\nYou can generate an IAM token by using either your IBM Cloud API key or a service ID's API key.\nThe API key is a permanent credential that can be reused if you don't lose the API key value or delete the API key in the account.\nThis process is also used if you are developing an application that needs to work with other IBM Cloud services.\nYou must use a service ID API key to get an access token to be passed to each of the IBM Cloud services.\n\n:::note\nAn access token is a temporary credential that expires after 1 hour.\nAfter the acquired token expires, you must generate a new token to continue calling IBM Cloud or service APIs, and you can perform only actions that are allowed by your level of assigned access within all accounts.\n:::", + requestBody: { + content: { + "application/x-www-form-urlencoded": { + schema: { $ref: "#/components/schemas/AuthForm" }, + }, + }, + required: true, + }, + security: [{ "//": [] }], + responses: { "200": { description: "ok" } }, + }, + }, + "/": { + get: { + tags: ["Bucket operations"], + summary: "List buckets", + description: + "A `GET` request sent to the endpoint root returns a list of buckets that are associated with the specified service instance.\nFor more information about endpoints, see [Endpoints and storage locations](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-endpoints#endpoints).", + parameters: [ + { $ref: "#/components/parameters/Extended" }, + { + name: "ibm-service-instance-id", + in: "header", + description: + "List buckets that were created in this service instance.", + required: true, + schema: { type: "string" }, + }, + ], + security: [{ BearerAuth: [] }, { BearerAuth: [], BasicAuth: [] }], + responses: { "200": { description: "ok" } }, + }, + }, + "/{bucketName}": { + put: { + tags: ["Bucket operations"], + summary: "Create a bucket", + description: + "A `PUT` request sent to the endpoint root followed by a string will create a bucket.\nFor more information about endpoints, see [Endpoints and storage locations](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-endpoints#endpoints).\nBucket names must be globally unique and DNS-compliant; names between 3 and 63 characters long must be made of lowercase letters, numbers, and dashes.\nBucket names must begin and end with a lowercase letter or number.\nBucket names resembling IP addresses are not allowed.\nThis operation doesn't make use of operation specific query parameters.\n\n:::info important\nBucket names must be unique because all buckets in the public cloud share a global namespace.\nThis allows for access to a bucket without needing to provide any service instance or account information.\nIt is also not possible to create a bucket with a name beginning with `cosv1-` or `account-` as these prefixes are reserved by the system.\n:::\n\n:::note\nPersonally Identifiable Information (PII): When creating buckets or adding objects, please ensure to not use any information that can identify any user (natural person) by name, location or any other means in the name of the bucket or object.\n:::\n\n## Create a bucket with a different storage class\n\nTo create a bucket with a different storage class, send an XML block specifying a bucket configuration with a `LocationConstraint` of `{provisioning code}` in the body of a `PUT` request to a bucket endpoint.\nFor more information about endpoints, see [Endpoints and storage locations](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-endpoints#endpoints).\nNote that standard bucket [naming rules](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-compatibility-api-bucket-operations#compatibility-api-new-bucket) apply.\nThis operation does not make use of operation specific query parameters.\n\nThe body of the request must contain an XML block with the following schema:\n\n```xml\n\n us-vault\n\n```\n\nA list of valid provisioning codes for `LocationConstraint` can be referenced in [the Storage Classes guide](https://cloud.ibm.com/docs/cloud-object-storage?topic=cloud-object-storage-classes#classes-locationconstraint).\n\n## Create a new bucket with Key Protect or Hyper Protect Crypto Services managed encryption keys (SSE-KP)\n\nTo create a bucket where the encryption keys are managed by Key Protect or Hyper Protect Crypto Services, it is necessary to have access to an active Key Protect or Hyper Protect Crypto Services service instance located in the same location as the new bucket.\nThis operation does not make use of operation specific query parameters.\n\nFor more information on using Key Protect to manage your encryption keys, [see the documentation for Key Protect](https://cloud.ibm.com/docs/key-protect?topic=key-protect-getting-started-tutorial).\n\nFor more information on Hyper Protect Crypto Services, [see the documentation](https://cloud.ibm.com/docs/hs-crypto?topic=hs-crypto-get-started).\n\n:::note\nNote that managed encryption is **not** available in a Cross Region configuration and any SSE-KP buckets must be Regional.\n:::", + parameters: [ + { + name: "bucketName", + in: "path", + required: true, + schema: { type: "string" }, + }, + { + name: "ibm-service-instance-id", + in: "header", + description: + "This header references the service instance where the bucket will be created and to which data usage will be billed.", + required: true, + schema: { type: "string" }, + }, + { + name: "ibm-sse-kp-encryption-algorithm", + in: "header", + description: + "This header is used to specify the algorithm and key size to use with the encryption key stored by using Key Protect. This value must be set to the string `AES256`.", + required: false, + schema: { type: "string" }, + }, + { + name: "ibm-sse-kp-customer-root-key-crn", + in: "header", + description: + "This header is used to reference the specific root key used by Key Protect or Hyper Protect Crypto Services to encrypt this bucket. This value must be the full CRN of the root key.", + required: false, + schema: { type: "string" }, + }, + ], + requestBody: { + content: { + "text/plain": { + schema: { + $ref: "#/components/schemas/CreateBucketConfiguration", + }, + }, + }, + }, + responses: { "200": { description: "ok" } }, + }, + head: { + tags: ["Bucket operations"], + summary: "Retrieve a bucket's headers", + description: + "A `HEAD` issued to a bucket will return the headers for that bucket.", + parameters: [ + { + name: "bucketName", + in: "path", + required: true, + schema: { type: "string" }, + }, + ], + responses: { "200": { description: "ok" } }, + }, + }, + }, + components: { + securitySchemes: { + BearerAuth: { type: "http", scheme: "BeAreR" }, + BasicAuth: { type: "http", scheme: "basic" }, + }, + schemas: { + AuthForm: { + type: "object", + properties: { + grant_type: { + type: "string", + enum: ["urn:ibm:params:oauth:grant-type:apikey"], + }, + apikey: { type: "string" }, + }, + required: ["grant_type", "apikey"], + }, + CreateBucketConfiguration: { + type: "object", + properties: { CreateBucketConfiguration: { type: "object" } }, + }, + }, + parameters: { + Extended: { + in: "query", + name: "extended", + description: "Provides `LocationConstraint` metadata in the listing.", + required: false, + schema: { type: "boolean" }, + allowEmptyValue: true, + }, + }, + }, + security: [{ BearerAuth: [] }], + tags: [ + { name: "Authentication" }, + { name: "Bucket operations" }, + { name: "Object operations" }, + ], +}; + +it("hello world", async () => { + const expected = await _loadOpenapi( + JSON.parse(JSON.stringify(example)), + "@", + "@" + ); + const actual = await _newLoadOpenapi( + JSON.parse(JSON.stringify(example)), + "@", + "@" + ); + + expect(JSON.parse(JSON.stringify(actual))).toStrictEqual( + JSON.parse(JSON.stringify(expected)) + ); +}); diff --git a/packages/docusaurus-plugin-openapi/src/openapi/load.ts b/packages/docusaurus-plugin-openapi/src/openapi/load.ts new file mode 100644 index 00000000..7d86989a --- /dev/null +++ b/packages/docusaurus-plugin-openapi/src/openapi/load.ts @@ -0,0 +1,502 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import { normalizeUrl } from "@docusaurus/utils"; +import fs from "fs-extra"; +import yaml from "js-yaml"; +import JsonRefs from "json-refs"; +import { kebabCase } from "lodash"; +import Converter from "openapi-to-postmanv2"; +import sdk, { Collection } from "postman-collection"; + +import { ApiItem, ApiSection } from "../types"; +import { sampleFromSchema } from "./createExample"; +import { OpenApiObject, OpenApiObjectWithRef } from "./types"; + +function getPaths(spec: OpenApiObject): ApiItem[] { + const seen: { [key: string]: number } = {}; + return Object.entries(spec.paths) + .map(([path, pathObject]) => { + const entries = Object.entries(pathObject); + return entries.map(([key, val]) => { + let method = key; + let operationObject = val; + + const title = + operationObject.summary ?? + operationObject.operationId ?? + "Missing summary"; + if (operationObject.description === undefined) { + operationObject.description = + operationObject.summary ?? operationObject.operationId ?? ""; + } + + const baseId = kebabCase(title); + let count = seen[baseId]; + + let id; + if (count) { + id = `${baseId}-${count}`; + seen[baseId] = count + 1; + } else { + id = baseId; + seen[baseId] = 1; + } + + const servers = + operationObject.servers ?? pathObject.servers ?? spec.servers; + + // TODO: Don't include summary temporarilly + const { summary, ...defaults } = operationObject; + + return { + ...defaults, + id, + title, + method, + path, + servers, + }; + }); + }) + .flat() + .filter((item) => item !== undefined) as ApiItem[]; +} + +function organizeSpec(spec: OpenApiObject) { + const paths = getPaths(spec); + + let tagNames: string[] = []; + let tagged: ApiSection[] = []; + if (spec.tags) { + tagged = spec.tags + .map((tag) => { + return { + title: tag.name, + description: tag.description || "", + items: paths.filter((p) => p.tags && p.tags.includes(tag.name)), + }; + }) + .filter((i) => i.items.length > 0); + tagNames = tagged.map((t) => t.title); + } + + const all = [ + ...tagged, + { + title: "API", + description: "", + items: paths.filter((p) => { + if (p.tags === undefined || p.tags.length === 0) { + return true; + } + for (let tag of p.tags) { + if (tagNames.includes(tag)) { + return false; + } + } + return true; + }), + }, + ]; + + return all; +} + +async function convertToPostman( + openapiData: OpenApiObjectWithRef +): Promise { + // The conversions mutates whatever you pass here, create a new object. + const openapiClone = JSON.parse( + JSON.stringify(openapiData) + ) as OpenApiObjectWithRef; + + // seems to be a weird bug with postman and servers... + delete openapiClone.servers; + for (let pathItemObject of Object.values(openapiClone.paths)) { + delete pathItemObject.servers; + delete pathItemObject.get?.servers; + delete pathItemObject.put?.servers; + delete pathItemObject.post?.servers; + delete pathItemObject.delete?.servers; + delete pathItemObject.options?.servers; + delete pathItemObject.head?.servers; + delete pathItemObject.patch?.servers; + delete pathItemObject.trace?.servers; + } + + return await new Promise((resolve, reject) => { + Converter.convert( + { + type: "json", + data: openapiClone, + }, + {}, + (_: any, conversionResult: any) => { + if (!conversionResult.result) { + reject(conversionResult.reason); + return; + } else { + return resolve(new sdk.Collection(conversionResult.output[0].data)); + } + } + ); + }); +} + +export async function _loadOpenapi( + openapiData: OpenApiObjectWithRef, + baseUrl: string, + routeBasePath: string +) { + // Attach a postman request object to the openapi spec. + const postmanCollection = await convertToPostman(openapiData); + postmanCollection.forEachItem((item) => { + const method = item.request.method.toLowerCase(); + // NOTE: This doesn't catch all variables for some reason... + // item.request.url.variables.each((pathVar) => { + // pathVar.value = `{${pathVar.key}}`; + // }); + const path = item.request.url + .getPath({ unresolved: true }) + .replace(/:([a-z0-9-_]+)/gi, "{$1}"); + + switch (method) { + case "get": + case "put": + case "post": + case "delete": + case "options": + case "head": + case "patch": + case "trace": + if (!openapiData.paths[path]) { + break; + } + + const operationObject = openapiData.paths[path][method]; + if (operationObject) { + // TODO + (operationObject as any).postman = item.request; + } + break; + default: + break; + } + }); + + // TODO: Why do we dereff here and not earlier? I think it had something to do with object names? + const { resolved: dereffed } = await JsonRefs.resolveRefs(openapiData); + + const dereffedSpec = dereffed as OpenApiObject; + + const order = organizeSpec(dereffedSpec); + + order.forEach((category, i) => { + category.items.forEach((item, ii) => { + // don't override already defined servers. + if (item.servers === undefined) { + item.servers = dereffedSpec.servers; + } + + if (item.security === undefined) { + item.security = dereffedSpec.security; + } + + // Add security schemes so we know how to handle security. + item.securitySchemes = dereffedSpec.components?.securitySchemes; + + // Make sure schemes are lowercase. See: https://github.com/cloud-annotations/docusaurus-plugin-openapi/issues/79 + Object.values(item.securitySchemes ?? {}).forEach((auth) => { + if (auth.type === "http") { + auth.scheme = auth.scheme.toLowerCase(); + } + }); + + item.permalink = normalizeUrl([baseUrl, routeBasePath, item.id]); + + const prev = + order[i].items[ii - 1] || + order[i - 1]?.items[order[i - 1].items.length - 1]; + const next = + order[i].items[ii + 1] || (order[i + 1] ? order[i + 1].items[0] : null); + + if (prev) { + item.previous = { + title: prev.title, + permalink: normalizeUrl([baseUrl, routeBasePath, prev.id]), + }; + } + + if (next) { + item.next = { + title: next.title, + permalink: normalizeUrl([baseUrl, routeBasePath, next.id]), + }; + } + + const content = item.requestBody?.content; + const schema = content?.["application/json"]?.schema; + if (schema) { + item.jsonRequestBodyExample = sampleFromSchema(schema); + } + }); + }); + + return order; +} + +/** + * Finds any reference objects in the OpenAPI definition and resolves them to a finalized value. + */ +async function resolveRefs(openapiData: OpenApiObjectWithRef) { + const { resolved } = await JsonRefs.resolveRefs(openapiData); + return resolved as OpenApiObject; +} + +/** + * Convenience function for converting raw JSON to a Postman Collection object. + */ +function jsonToCollection(data: OpenApiObject): Promise { + return new Promise((resolve, reject) => { + Converter.convert( + { type: "json", data }, + {}, + (_err: any, conversionResult: any) => { + if (!conversionResult.result) { + return reject(conversionResult.reason); + } + return resolve(new sdk.Collection(conversionResult.output[0].data)); + } + ); + }); +} + +/** + * Creates a Postman Collection object from an OpenAPI definition. + */ +async function createPostmanCollection( + openapiData: OpenApiObject +): Promise { + const data = JSON.parse(JSON.stringify(openapiData)) as OpenApiObject; + + // Including `servers` breaks postman, so delete all of them. + delete data.servers; + for (let pathItemObject of Object.values(data.paths)) { + delete pathItemObject.servers; + delete pathItemObject.get?.servers; + delete pathItemObject.put?.servers; + delete pathItemObject.post?.servers; + delete pathItemObject.delete?.servers; + delete pathItemObject.options?.servers; + delete pathItemObject.head?.servers; + delete pathItemObject.patch?.servers; + delete pathItemObject.trace?.servers; + } + + return await jsonToCollection(data); +} + +/** + * Find the ApiItem, given the method and path. + */ +function findApiItem(sections: ApiSection[], method: string, path: string) { + for (const section of sections) { + const found = section.items.find( + (item) => item.path === path && item.method === method + ); + if (found) { + return found; + } + } + return; +} + +function createItems(openapiData: OpenApiObject): ApiItem[] { + const seen: { [key: string]: number } = {}; + + // TODO + let items: Omit[] = []; + + for (let [path, pathObject] of Object.entries(openapiData.paths)) { + const { $ref, description, parameters, servers, summary, ...rest } = + pathObject; + for (let [method, operationObject] of Object.entries({ ...rest })) { + const title = + operationObject.summary ?? + operationObject.operationId ?? + "Missing summary"; + if (operationObject.description === undefined) { + operationObject.description = + operationObject.summary ?? operationObject.operationId ?? ""; + } + + const baseId = kebabCase(title); + let count = seen[baseId]; + + let id; + if (count) { + id = `${baseId}-${count}`; + seen[baseId] = count + 1; + } else { + id = baseId; + seen[baseId] = 1; + } + + const servers = + operationObject.servers ?? pathObject.servers ?? openapiData.servers; + + const security = operationObject.security ?? openapiData.security; + + // Add security schemes so we know how to handle security. + const securitySchemes = openapiData.components?.securitySchemes; + + // Make sure schemes are lowercase. See: https://github.com/cloud-annotations/docusaurus-plugin-openapi/issues/79 + if (securitySchemes) { + for (let securityScheme of Object.values(securitySchemes)) { + if (securityScheme.type === "http") { + securityScheme.scheme = securityScheme.scheme.toLowerCase(); + } + } + } + + let jsonRequestBodyExample; + const body = operationObject.requestBody?.content?.["application/json"]; + if (body?.schema) { + jsonRequestBodyExample = sampleFromSchema(body.schema); + } + + // TODO: Don't include summary temporarilly + const { summary, ...defaults } = operationObject; + + items.push({ + ...defaults, + id, + title, + method, + path, + servers, + security, + securitySchemes, + jsonRequestBodyExample, + }); + } + } + + // TODO + return items as ApiItem[]; +} + +function createSections(spec: OpenApiObject) { + const paths = createItems(spec); + + let tagNames: string[] = []; + let tagged: ApiSection[] = []; + if (spec.tags) { + tagged = spec.tags + .map((tag) => { + return { + title: tag.name, + description: tag.description || "", + items: paths.filter((p) => p.tags && p.tags.includes(tag.name)), + }; + }) + .filter((i) => i.items.length > 0); + tagNames = tagged.map((t) => t.title); + } + + const all = [ + ...tagged, + { + title: "API", + description: "", + items: paths.filter((p) => { + if (p.tags === undefined || p.tags.length === 0) { + return true; + } + for (let tag of p.tags) { + if (tagNames.includes(tag)) { + return false; + } + } + return true; + }), + }, + ]; + + return all; +} + +/** + * Attach Postman Request objects to the corresponding ApiItems. + */ +function bindCollectionToSections( + sections: ApiSection[], + postmanCollection: sdk.Collection +) { + postmanCollection.forEachItem((item) => { + const method = item.request.method.toLowerCase(); + const path = item.request.url + .getPath({ unresolved: true }) // unresolved returns "/:variableName" instead of "/" + .replace(/:([a-z0-9-_]+)/gi, "{$1}"); // replace "/:variableName" with "/{variableName}" + + const apiItem = findApiItem(sections, method, path); + if (apiItem) { + apiItem.postman = item.request; + } + }); +} + +export async function _newLoadOpenapi( + openapiDataWithRefs: OpenApiObjectWithRef, + baseUrl: string, + routeBasePath: string +) { + const openapiData = await resolveRefs(openapiDataWithRefs); + const postmanCollection = await createPostmanCollection(openapiData); + + const sections = createSections(openapiData); + + bindCollectionToSections(sections, postmanCollection); + + // flatten the references to make creating the previous/next data easier. + const items = sections.flatMap((s) => s.items); + for (let i = 0; i < items.length; i++) { + const current = items[i]; + const prev = items[i - 1]; + const next = items[i + 1]; + + current.permalink = normalizeUrl([baseUrl, routeBasePath, current.id]); + + if (prev) { + current.previous = { + title: prev.title, + permalink: normalizeUrl([baseUrl, routeBasePath, prev.id]), + }; + } + + if (next) { + current.next = { + title: next.title, + permalink: normalizeUrl([baseUrl, routeBasePath, next.id]), + }; + } + } + + return sections; +} + +export async function loadOpenapi( + openapiPath: string, + baseUrl: string, + routeBasePath: string +) { + const openapiString = await fs.readFile(openapiPath, "utf-8"); + const openapiData = yaml.load(openapiString) as OpenApiObjectWithRef; + + return _loadOpenapi(openapiData, baseUrl, routeBasePath); +} diff --git a/packages/docusaurus-plugin-openapi/src/openapi/types.ts b/packages/docusaurus-plugin-openapi/src/openapi/types.ts new file mode 100644 index 00000000..07df4fc7 --- /dev/null +++ b/packages/docusaurus-plugin-openapi/src/openapi/types.ts @@ -0,0 +1,429 @@ +/* ============================================================================ + * Copyright (c) Cloud Annotations + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * ========================================================================== */ + +import type { JSONSchema4, JSONSchema6, JSONSchema7 } from "json-schema"; + +interface Map { + [key: string]: T; +} + +export interface OpenApiObject { + openapi: string; + info: InfoObject; + servers?: ServerObject[]; + paths: PathsObject; + components?: ComponentsObject; + security?: SecurityRequirementObject[]; + tags?: TagObject[]; + externalDocs?: ExternalDocumentationObject; +} + +export interface OpenApiObjectWithRef { + openapi: string; + info: InfoObject; + servers?: ServerObject[]; + paths: PathsObjectWithRef; + components?: ComponentsObjectWithRef; + security?: SecurityRequirementObject[]; + tags?: TagObject[]; + externalDocs?: ExternalDocumentationObject; +} + +export interface InfoObject { + title: string; + description?: string; + termsOfService?: string; + contact?: ContactObject; + license?: LicenseObject; + version: string; +} + +export interface ContactObject { + name?: string; + url?: string; + email?: string; +} + +export interface LicenseObject { + name: string; + url?: string; +} + +export interface ServerObject { + url: string; + description?: string; + variables?: Map; +} + +export interface ServerVariable { + enum?: string[]; + default: string; + description?: string; +} + +export interface ComponentsObject { + schemas?: Map; + responses?: Map; + parameters?: Map; + examples?: Map; + requestBodies?: Map; + headers?: Map; + securitySchemes?: Map; + links?: Map; + callbacks?: Map; +} + +export interface ComponentsObjectWithRef { + schemas?: Map; + responses?: Map; + parameters?: Map; + examples?: Map; + requestBodies?: Map; + headers?: Map; + securitySchemes?: Map; + links?: Map; + callbacks?: Map; +} + +export type PathsObject = Map; + +export type PathsObjectWithRef = Map; + +export interface PathItemObject { + $ref?: string; + summary?: string; + description?: string; + get?: OperationObject; + put?: OperationObject; + post?: OperationObject; + delete?: OperationObject; + options?: OperationObject; + head?: OperationObject; + patch?: OperationObject; + trace?: OperationObject; + servers?: ServerObject[]; + parameters?: ParameterObject[]; +} + +export interface PathItemObjectWithRef { + $ref?: string; + summary?: string; + description?: string; + get?: OperationObjectWithRef; + put?: OperationObjectWithRef; + post?: OperationObjectWithRef; + delete?: OperationObjectWithRef; + options?: OperationObjectWithRef; + head?: OperationObjectWithRef; + patch?: OperationObjectWithRef; + trace?: OperationObjectWithRef; + servers?: ServerObject[]; + parameters?: (ParameterObjectWithRef | ReferenceObject)[]; +} + +export interface OperationObject { + tags?: string[]; + summary?: string; + description?: string; + externalDocs?: ExternalDocumentationObject; + operationId?: string; + parameters?: ParameterObject[]; + requestBody?: RequestBodyObject; + responses: ResponsesObject; + callbacks?: Map; + deprecated?: boolean; + security?: SecurityRequirementObject[]; + servers?: ServerObject[]; + + // extensions + "x-deprecated-description"?: string; +} + +export interface OperationObjectWithRef { + tags?: string[]; + summary?: string; + description?: string; + externalDocs?: ExternalDocumentationObject; + operationId?: string; + parameters?: (ParameterObjectWithRef | ReferenceObject)[]; + requestBody?: RequestBodyObjectWithRef | ReferenceObject; + responses: ResponsesObjectWithRef; + callbacks?: Map; + deprecated?: boolean; + security?: SecurityRequirementObject[]; + servers?: ServerObject[]; + + // extensions + "x-deprecated-description"?: string; +} + +export interface ExternalDocumentationObject { + description?: string; + url: string; +} + +export interface ParameterObject { + name: string; + in: string; + description?: string; + required?: boolean; + deprecated?: boolean; + allowEmptyValue?: boolean; + // + style?: string; + explode?: string; + allowReserved?: boolean; + schema?: SchemaObject; + example?: any; + examples?: Map; + // + content?: Map; + // ignoring stylings: matrix, label, form, simple, spaceDelimited, + // pipeDelimited and deepObject +} + +export interface ParameterObjectWithRef { + name: string; + in: string; + description?: string; + required?: boolean; + deprecated?: boolean; + allowEmptyValue?: boolean; + // + style?: string; + explode?: string; + allowReserved?: boolean; + schema?: SchemaObjectWithRef | ReferenceObject; + example?: any; + examples?: Map; + // + content?: Map; + // ignoring stylings: matrix, label, form, simple, spaceDelimited, + // pipeDelimited and deepObject +} + +export interface RequestBodyObject { + description?: string; + content: Map; + required?: boolean; +} + +export interface RequestBodyObjectWithRef { + description?: string; + content: Map; + required?: boolean; +} + +export interface MediaTypeObject { + schema?: SchemaObject; + example?: any; + examples?: Map; + encoding?: Map; +} + +export interface MediaTypeObjectWithRef { + schema?: SchemaObjectWithRef | ReferenceObject; + example?: any; + examples?: Map; + encoding?: Map; +} + +export interface EncodingObject { + contentType?: string; + headers?: Map; + style?: string; + explode?: boolean; + allowReserved?: boolean; +} + +export interface EncodingObjectWithRef { + contentType?: string; + headers?: Map; + style?: string; + explode?: boolean; + allowReserved?: boolean; +} + +export type ResponsesObject = Map; + +export type ResponsesObjectWithRef = Map< + ResponseObjectWithRef | ReferenceObject +>; + +export interface ResponseObject { + description: string; + headers?: Map; + content?: Map; + links?: Map; +} + +export interface ResponseObjectWithRef { + description: string; + headers?: Map; + content?: Map; + links?: Map; +} + +export type CallbackObject = Map; + +export type CallbackObjectWithRef = Map; + +export interface ExampleObject { + summary?: string; + description?: string; + value?: any; + externalValue?: string; +} + +export interface LinkObject { + operationRef?: string; + operationId?: string; + parameters?: Map; + requestBody?: any; + description?: string; + server?: ServerObject; +} + +export type HeaderObject = Omit; + +export type HeaderObjectWithRef = Omit; + +export interface TagObject { + name: string; + description?: string; + externalDocs?: ExternalDocumentationObject; +} + +export interface ReferenceObject { + $ref: string; +} + +export type JSONSchema = JSONSchema4 | JSONSchema6 | JSONSchema7; +export type SchemaObject = Omit< + JSONSchema, + | "type" + | "allOf" + | "oneOf" + | "anyOf" + | "not" + | "items" + | "properties" + | "additionalProperties" +> & { + // OpenAPI specific overrides + type?: "string" | "number" | "integer" | "boolean" | "object" | "array"; + allOf?: SchemaObject[]; + oneOf?: SchemaObject[]; + anyOf?: SchemaObject[]; + not?: SchemaObject; + items?: SchemaObject; + properties?: Map; + additionalProperties?: boolean | SchemaObject; + + // OpenAPI additions + nullable?: boolean; + discriminator?: DiscriminatorObject; + readOnly?: boolean; + writeOnly?: boolean; + xml?: XMLObject; + externalDocs?: ExternalDocumentationObject; + example?: any; + deprecated?: boolean; +}; + +export type SchemaObjectWithRef = Omit< + JSONSchema, + | "type" + | "allOf" + | "oneOf" + | "anyOf" + | "not" + | "items" + | "properties" + | "additionalProperties" +> & { + // OpenAPI specific overrides + type?: "string" | "number" | "integer" | "boolean" | "object" | "array"; + allOf?: (SchemaObject | ReferenceObject)[]; + oneOf?: (SchemaObject | ReferenceObject)[]; + anyOf?: (SchemaObject | ReferenceObject)[]; + not?: SchemaObject | ReferenceObject; + items?: SchemaObject | ReferenceObject; + properties?: Map; + additionalProperties?: boolean | SchemaObject | ReferenceObject; + + // OpenAPI additions + nullable?: boolean; + discriminator?: DiscriminatorObject; + readOnly?: boolean; + writeOnly?: boolean; + xml?: XMLObject; + externalDocs?: ExternalDocumentationObject; + example?: any; + deprecated?: boolean; +}; + +export interface DiscriminatorObject { + propertyName: string; + mapping?: Map; +} + +export interface XMLObject { + name?: string; + namespace?: string; + prefix?: string; + attribute?: boolean; + wrapped?: boolean; +} + +export type SecuritySchemeObject = + | ApiKeySecuritySchemeObject + | HttpSecuritySchemeObject + | Oauth2SecuritySchemeObject + | OpenIdConnectSecuritySchemeObject; + +export interface ApiKeySecuritySchemeObject { + type: "apiKey"; + description?: string; + name: string; + in: "query" | "header" | "cookie"; +} + +export interface HttpSecuritySchemeObject { + type: "http"; + description?: string; + scheme: string; + bearerFormat?: string; +} + +export interface Oauth2SecuritySchemeObject { + type: "oauth2"; + description?: string; + flows: OAuthFlowsObject; +} + +export interface OpenIdConnectSecuritySchemeObject { + type: "openIdConnect"; + description?: string; + openIdConnectUrl: string; +} + +export interface OAuthFlowsObject { + implicit?: OAuthFlowObject; + password?: OAuthFlowObject; + clientCredentials?: OAuthFlowObject; + authorizationCode?: OAuthFlowObject; +} + +export interface OAuthFlowObject { + authorizationUrl?: string; // required for some + tokenUrl?: string; // required for some + refreshUrl?: string; + scopes: Map; +} + +export type SecurityRequirementObject = Map; diff --git a/packages/docusaurus-plugin-openapi/src/types.ts b/packages/docusaurus-plugin-openapi/src/types.ts index 345f66a4..8eec39e5 100644 --- a/packages/docusaurus-plugin-openapi/src/types.ts +++ b/packages/docusaurus-plugin-openapi/src/types.ts @@ -8,6 +8,8 @@ import type { RemarkAndRehypePluginOptions } from "@docusaurus/mdx-loader"; import { Request } from "postman-collection"; +import { OperationObject, SecuritySchemeObject } from "./openapi/types"; + export interface PluginOptions extends RemarkAndRehypePluginOptions { id: string; path: string; @@ -23,330 +25,13 @@ export interface LoadedContent { loadedApi: ApiSection[]; } -export enum Type { - string = "string", - number = "number", - integer = "integer", - boolean = "boolean", - object = "object", - array = "array", -} - -export interface Schema { - type?: Type; - format?: string; - example?: any; - additionalProperties?: any; - enum?: any; - default?: any; - deprecated?: boolean; - - properties?: Schema; - items?: Schema; - oneOf?: Schema; - anyOf?: Schema; -} - -export interface Primitives { - string: { [key: string]: (schema: Schema) => any }; - number: { [key: string]: (schema: Schema) => any }; - integer: { [key: string]: (schema: Schema) => any }; - boolean: { [key: string]: (schema: Schema) => any }; - object: { [key: string]: (schema: Schema) => any }; - array: { [key: string]: (schema: Schema) => any }; -} - -export interface Sidebar { - [sidebarId: string]: SidebarItem[]; -} - -export type SidebarItem = SidebarItemLink | SidebarItemCategory; - -export interface SidebarItemLink { - type: "link"; - href: string; - label: string; -} - -export interface SidebarItemCategory { - type: "category"; - label: string; - items: SidebarItem[]; - collapsed: boolean; -} - -export interface Order { - [id: string]: OrderMetadata; -} - -export interface OrderMetadata { - previous?: string; - next?: string; - sidebar?: string; -} - -export interface OpenApiObject { - openapi: string; - info: InfoObject; - servers?: ServerObject[]; - paths: PathsObject; - components?: ComponentsObject; - security?: SecurityRequirementObject[]; - tags?: TagObject[]; - externalDocs?: ExternalDocumentationObject; -} - -export interface InfoObject { - title: string; - version: string; - description?: string; - termsOfService?: string; - contact?: ContactObject; - license?: LicenseObject; -} - -export interface ContactObject { - name?: string; - url?: string; - email?: string; -} - -export interface LicenseObject { - name: string; - url?: string; -} - -export interface ServerObject { - url: string; - description?: string; - variables?: Map; -} - -export interface ServerVariable { - default: string; - enum?: string[]; - description?: string; -} - -export interface ComponentsObject { - schemas?: Map; - responses?: Map; - parameters?: Map; - examples?: Map; - requestBodies?: Map; - headers?: Map; - securitySchemes?: Map< - | ApiKeySecuritySchemeObject - | HttpSecuritySchemeObject - | Oauth2SecuritySchemeObject - | OpenIdConnectSecuritySchemeObject - | ReferenceObject - >; - links?: Map; - callbacks?: Map; -} - -export interface PathsObject { - [path: string]: PathItemObject; -} - -export interface PathItemObject { - $ref?: string; - summary?: string; - description?: string; - get?: OperationObject; - put?: OperationObject; - post?: OperationObject; - delete?: OperationObject; - options?: OperationObject; - head?: OperationObject; - patch?: OperationObject; - trace?: OperationObject; - servers?: ServerObject[]; - parameters?: (ParameterObject | ReferenceObject)[]; -} - -export interface OperationObject { - responses: ResponsesObject; - tags?: string[]; - summary?: string; - description?: string; - externalDocs?: ExternalDocumentationObject; - operationId?: string; - parameters?: (ParameterObject | ReferenceObject)[]; - requestBody?: RequestBodyObject | ReferenceObject; - callbacks?: Map; - deprecated?: boolean; - "x-deprecated-description"?: string; - security?: SecurityRequirementObject[]; - servers?: ServerObject[]; - postman?: Request; -} - -export interface ExternalDocumentationObject { - url: string; - description?: string; -} - -export interface ParameterObject { - name: string; - in: string; - description?: string; - required?: boolean; - deprecated?: boolean; - allowEmptyValue?: boolean; - // - style?: string; - explode?: string; - allowReserved?: boolean; - schema?: SchemaObject | ReferenceObject; - example?: any; - examples?: Map; - // - content?: Map; - // ignoring stylings: matrix, label, form, simple, spaceDelimited, - // pipeDelimited and deepObject -} - -export interface RequestBodyObject { - content: Map; - description?: string; - required?: boolean; -} - -export interface MediaTypeObject { - schema?: SchemaObject | ReferenceObject; - example?: any; - examples?: Map; - encoding?: Map; -} - -export interface EncodingObject { - contentType?: string; - headers?: Map; - style?: string; - explode?: boolean; - allowReserved?: boolean; -} - -export interface ResponsesObject { - [code: string]: ResponseObject | ReferenceObject; -} - -export interface ResponseObject { - description: string; - headers?: Map; - content?: Map; - links?: Map; -} - -export interface CallbackObject { - [expression: string]: PathItemObject; -} - -export interface ExampleObject { - summary?: string; - description?: string; - value?: any; - externalValue?: string; -} - -export interface LinkObject { - operationRef?: string; - operationId?: string; - parameters?: Map; - requestBody?: any; - description?: string; - server?: ServerObject; -} - -export interface HeaderObject { - description?: string; - required?: boolean; - deprecated?: boolean; - allowEmptyValue?: boolean; - // - style?: string; - explode?: string; - allowReserved?: boolean; - schema?: SchemaObject | ReferenceObject; - example?: any; - examples?: Map; - // - content?: Map; - // ignoring stylings: matrix, label, form, simple, spaceDelimited, - // pipeDelimited and deepObject -} - -export interface TagObject { - name: string; - description?: string; - externalDocs?: ExternalDocumentationObject; -} - -export interface ReferenceObject { - $ref: string; -} - -// TODO: this could be expanded on. -export interface SchemaObject { - [key: string]: any; -} - -export interface ApiKeySecuritySchemeObject { - type: string; - description?: string; - name: string; - in: string; -} - -export interface HttpSecuritySchemeObject { - type: string; - description?: string; - scheme: string; - bearerFormat?: string; -} - -export interface Oauth2SecuritySchemeObject { - type: string; - description?: string; - flows: OAuthFlowsObject; -} - -export interface OpenIdConnectSecuritySchemeObject { - type: string; - description?: string; - openIdConnectUrl: string; -} - -export interface OAuthFlowsObject { - implicit?: OAuthFlowObject; - password?: OAuthFlowObject; - clientCredentials?: OAuthFlowObject; - authorizationCode?: OAuthFlowObject; -} - -export interface OAuthFlowObject { - authorizationUrl?: string; // required for some? - tokenUrl?: string; // required for some? - refreshUrl: string; - scopes: Map; -} - -export interface SecurityRequirementObject { - [name: string]: string[]; -} - -export interface Map { - [key: string]: T; -} - export interface ApiSection { title: string; description: string; items: ApiItem[]; } +// TODO: Clean up this object export interface ApiItem extends OperationObject { id: string; title: string; @@ -356,13 +41,10 @@ export interface ApiItem extends OperationObject { next: Page; previous: Page; jsonRequestBodyExample: string; - securitySchemes?: Map< - | ApiKeySecuritySchemeObject - | HttpSecuritySchemeObject - | Oauth2SecuritySchemeObject - | OpenIdConnectSecuritySchemeObject - | ReferenceObject - >; + securitySchemes?: { + [key: string]: SecuritySchemeObject; + }; + postman?: Request; } export interface Page {