From 672d571dc211658daa1f333bd7a1062f78d03bd0 Mon Sep 17 00:00:00 2001 From: Zach Shilton <4624598+zchsh@users.noreply.github.com> Date: Tue, 12 Nov 2024 16:03:02 -0500 Subject: [PATCH] implement versioned docs for OpenAPI w split operation pages (#2623) * copy over utilities from previous view * clean up parse-and-validate-schema * implement versioning for OpenApiDocsViewV2 * update productData to product, for nav context * fix issue with productData * rejig default theme * fix metadata for landing --- .../parse-and-validate-open-api-schema.ts | 15 +- .../utils/get-props-from-preview-data.ts | 37 ++- .../components/landing-content/index.tsx | 49 +-- .../components/operation-content/index.tsx | 16 +- .../components/operation-content/server.ts | 14 +- .../operation-content/style.module.css | 13 +- .../components/version-alert/index.tsx | 91 ++++++ .../components/version-alert/types.ts | 11 + .../version-alert/version-alert.module.css | 15 + src/views/open-api-docs-view-v2/index.tsx | 59 ++-- src/views/open-api-docs-view-v2/server.ts | 303 ++++++++++++++---- src/views/open-api-docs-view-v2/types.ts | 105 +++++- .../utils/find-default-version.ts | 39 +++ .../utils/get-version-switcher-props.ts | 69 ++++ .../utils/parse-open-api-v2-url-context.ts | 44 +++ .../utils/slugify-operation-id.ts | 21 ++ .../utils/sort-date-version-data.ts | 22 ++ src/views/open-api-docs-view/server.ts | 9 +- src/views/open-api-docs-view/utils/index.ts | 1 - .../utils/parse-and-validate-schema.ts | 76 ----- 20 files changed, 786 insertions(+), 223 deletions(-) create mode 100644 src/views/open-api-docs-view-v2/components/version-alert/index.tsx create mode 100644 src/views/open-api-docs-view-v2/components/version-alert/types.ts create mode 100644 src/views/open-api-docs-view-v2/components/version-alert/version-alert.module.css create mode 100644 src/views/open-api-docs-view-v2/utils/find-default-version.ts create mode 100644 src/views/open-api-docs-view-v2/utils/get-version-switcher-props.ts create mode 100644 src/views/open-api-docs-view-v2/utils/parse-open-api-v2-url-context.ts create mode 100644 src/views/open-api-docs-view-v2/utils/slugify-operation-id.ts create mode 100644 src/views/open-api-docs-view-v2/utils/sort-date-version-data.ts delete mode 100644 src/views/open-api-docs-view/utils/parse-and-validate-schema.ts diff --git a/src/lib/api-docs/parse-and-validate-open-api-schema.ts b/src/lib/api-docs/parse-and-validate-open-api-schema.ts index d81497e349..7de5a4836f 100644 --- a/src/lib/api-docs/parse-and-validate-open-api-schema.ts +++ b/src/lib/api-docs/parse-and-validate-open-api-schema.ts @@ -30,9 +30,7 @@ import { OpenAPIV3 } from 'openapi-types' */ export async function parseAndValidateOpenApiSchema( fileString: string, - massageSchemaForClient: (schema: OpenAPIV3.Document) => OpenAPIV3.Document = ( - schema - ) => schema + schemaTransforms: ((schema: OpenAPIV3.Document) => OpenAPIV3.Document)[] = [] ): Promise { // Parse the fileString into raw JSON const rawSchemaJson = JSON.parse(fileString) @@ -41,7 +39,14 @@ export async function parseAndValidateOpenApiSchema( const schemaJsonWithRefs = await new OASNormalize(rawSchemaJson).validate({ convertToLatest: true, }) - const massagedSchema = massageSchemaForClient(schemaJsonWithRefs) + + /** + * Apply transform functions to the schema + */ + let transformedSchema = schemaJsonWithRefs + for (const schemaTransformFunction of schemaTransforms ?? []) { + transformedSchema = schemaTransformFunction(transformedSchema) + } /** * Dereference the schema. @@ -69,7 +74,7 @@ export async function parseAndValidateOpenApiSchema( * } * } */ - const schemaJson = await new OASNormalize(massagedSchema).deref() + const schemaJson = await new OASNormalize(transformedSchema).deref() // Return the dereferenced schema. // We know it's OpenAPI 3.0, so we cast it to the appropriate type. return schemaJson as OpenAPIV3.Document diff --git a/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts b/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts index 762d454d6d..2b10238274 100644 --- a/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts +++ b/src/views/open-api-docs-preview-v2/utils/get-props-from-preview-data.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: MPL-2.0 */ -import { getStaticProps } from 'views/open-api-docs-view-v2/server' +import { generateStaticProps } from 'views/open-api-docs-view-v2/server' // Utils import { getOperationGroupKeyFromPath } from 'views/open-api-docs-view-v2/utils/get-operation-group-key-from-path' import { schemaTransformShortenHcp } from 'views/open-api-docs-view-v2/schema-transforms/schema-transform-shorten-hcp' @@ -15,7 +15,6 @@ import type { OpenApiDocsViewV2Config, } from 'views/open-api-docs-view-v2/types' import type { OpenApiPreviewV2InputValues } from '../components/open-api-preview-inputs' -import { schemaModComponent } from 'views/open-api-docs-view/utils/massage-schema-utils' /** * Given preview data submitted by the user, which includes OpenAPI JSON, @@ -23,11 +22,6 @@ import { schemaModComponent } from 'views/open-api-docs-view/utils/massage-schem * a view for a specific operation, * * Return static props for the appropriate OpenAPI docs view. - * - * TODO: this is largely a placeholder for now. - * Will likely require a few more args to pass to getStaticProps, eg productData - * for example, but those types of details are not yet needed by the underlying - * view. */ export default async function getPropsFromPreviewData( previewData: OpenApiPreviewV2InputValues | null, @@ -59,7 +53,7 @@ export default async function getPropsFromPreviewData( }, ] // Build page configuration based on the input values - const pageConfig: OpenApiDocsViewV2Config = { + const pageConfig: Omit = { basePath: '/open-api-docs-preview-v2', breadcrumbLinksPrefix: [ { @@ -67,9 +61,8 @@ export default async function getPropsFromPreviewData( url: '/', }, ], - operationSlug, - openApiJsonString: previewData.openApiJsonString, schemaTransforms, + productContext: 'hcp', // A generic set of resource links, as a preview of what typically // gets added to an OpenAPI docs page. resourceLinks: [ @@ -90,8 +83,6 @@ export default async function getPropsFromPreviewData( href: 'https://www.hashicorp.com/customer-success', }, ], - // Release stage badge, to demo this feature - releaseStage: 'Preview', // Status indicator for HCP Services generally, to demo this feature statusIndicatorConfig: { pageUrl: 'https://status.hashicorp.com', @@ -106,5 +97,25 @@ export default async function getPropsFromPreviewData( pageConfig.getOperationGroupKey = getOperationGroupKeyFromPath } // Use the page config to generate static props for the view - return await getStaticProps(pageConfig) + const staticProps = await generateStaticProps({ + ...pageConfig, + versionData: [ + { + versionId: 'latest', + releaseStage: 'Preview', + sourceFile: previewData.openApiJsonString, + }, + ], + urlContext: { + isVersionedUrl: false, + versionId: 'latest', + operationSlug, + }, + }) + // If the specific view wasn't found, return null + if ('notFound' in staticProps) { + return null + } + // Otherwise, return the props, discarding the enclosing object + return staticProps.props } diff --git a/src/views/open-api-docs-view-v2/components/landing-content/index.tsx b/src/views/open-api-docs-view-v2/components/landing-content/index.tsx index c038ea497f..2a93c37303 100644 --- a/src/views/open-api-docs-view-v2/components/landing-content/index.tsx +++ b/src/views/open-api-docs-view-v2/components/landing-content/index.tsx @@ -15,18 +15,19 @@ import { DescriptionMdx } from './components/description-mdx' // Types import type { MDXRemoteSerializeResult } from 'lib/next-mdx-remote' import type { StatusIndicatorConfig } from 'views/open-api-docs-view-v2/types' - +import type { ReactNode } from 'react' import type { ProductSlug } from 'types/products' // Styles import s from './style.module.css' export interface LandingContentProps { - badgeText: string - descriptionMdx?: MDXRemoteSerializeResult heading: string - serviceProductSlug: ProductSlug + schemaFileString?: string + badgeText?: string + descriptionMdx?: MDXRemoteSerializeResult + serviceProductSlug?: ProductSlug statusIndicatorConfig?: StatusIndicatorConfig - schemaFileString: string + versionSwitcherSlot?: ReactNode } export function LandingContent({ @@ -36,6 +37,7 @@ export function LandingContent({ serviceProductSlug, statusIndicatorConfig, schemaFileString, + versionSwitcherSlot, }: LandingContentProps) { return (
@@ -53,26 +55,33 @@ export function LandingContent({ /> ) : null} - + {badgeText ? ( + + ) : null} + {versionSwitcherSlot ? ( +
{versionSwitcherSlot}
+ ) : null}
{descriptionMdx ? ( ) : null} - } - iconPosition="leading" - download="hcp.swagger.json" - href={`data:text/json;charset=utf-8,${encodeURIComponent( - schemaFileString - )}`} - /> + {schemaFileString ? ( + } + iconPosition="leading" + download="hcp.swagger.json" + href={`data:text/json;charset=utf-8,${encodeURIComponent( + schemaFileString + )}`} + /> + ) : null} ) } diff --git a/src/views/open-api-docs-view-v2/components/operation-content/index.tsx b/src/views/open-api-docs-view-v2/components/operation-content/index.tsx index b7e9602fd9..b4d25cec18 100644 --- a/src/views/open-api-docs-view-v2/components/operation-content/index.tsx +++ b/src/views/open-api-docs-view-v2/components/operation-content/index.tsx @@ -11,11 +11,11 @@ import { OperationExamples } from '../operation-examples' import { OperationDetails } from '../operation-details' // Types import type { PropertyDetailsSectionProps } from '../operation-details' +import type { ReactNode } from 'react' // Styles import s from './style.module.css' export interface OperationContentProps { - heading: string operationId: string tags: string[] slug: string @@ -32,6 +32,7 @@ export interface OperationContentProps { * word breaks to allow long URLs to wrap to multiple lines. */ urlPathForCodeBlock: string + versionSwitcherSlot?: ReactNode } /** @@ -47,17 +48,22 @@ export interface OperationProps { * Render detailed content for an individual operation. */ export default function OperationContent({ - heading, - slug, + operationId, type, path, urlPathForCodeBlock, requestData, responseData, + versionSwitcherSlot, }: OperationContentProps) { return ( <> -

{heading}

+
+

{operationId}

+ {versionSwitcherSlot ? ( +
{versionSwitcherSlot}
+ ) : null} +
@@ -66,7 +72,7 @@ export default function OperationContent({ } examplesSlot={ - + } detailsSlot={ { - const operationObjects = getOperationObjects(schemaData) - const operation = operationObjects.find( - (operation) => operation.operationId === operationSlug - ) /** * The API's base URL is used to prefix the operation path, * so users can quickly copy the full path to the operation */ const apiBaseUrl = getApiBaseUrl(schemaData) + const operationSlug = slugifyOperationId(operation.operationId) /** * Parse request and response details for this operation */ @@ -53,8 +52,7 @@ export default async function getOperationContentProps( * Return the operation content props */ return { - heading: operationSlug, - operationId: operationSlug, + operationId: operation.operationId, tags: operation.tags, slug: operationSlug, type: operation.type, diff --git a/src/views/open-api-docs-view-v2/components/operation-content/style.module.css b/src/views/open-api-docs-view-v2/components/operation-content/style.module.css index f683f79e81..007089067c 100644 --- a/src/views/open-api-docs-view-v2/components/operation-content/style.module.css +++ b/src/views/open-api-docs-view-v2/components/operation-content/style.module.css @@ -3,6 +3,15 @@ * SPDX-License-Identifier: MPL-2.0 */ +/* HEADER AREA */ + +.header { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 16px; +} + .heading { composes: hds-typography-display-600 from global; @@ -13,7 +22,9 @@ margin: 0 0 9px 0; } -/* HEADER AREA */ +.versionSwitcherSlot { + flex-shrink: 0; +} .methodAndPath { display: flex; diff --git a/src/views/open-api-docs-view-v2/components/version-alert/index.tsx b/src/views/open-api-docs-view-v2/components/version-alert/index.tsx new file mode 100644 index 0000000000..cbc531706e --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/version-alert/index.tsx @@ -0,0 +1,91 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { IconInfo16 } from '@hashicorp/flight-icons/svg-react/info-16' +import InlineLink from 'components/inline-link' +import PageAlert from 'components/page-alert' +import type { OpenApiV2VersionAlertProps } from './types' +import s from './version-alert.module.css' + +/** + * Display a version alert for API documentation + */ +export function OpenApiV2VersionAlert({ + isVersionedUrl, + currentVersion, + latestStableVersion, + basePath, +}: OpenApiV2VersionAlertProps) { + /** + * If this isn't a versioned URL, we won't show a version alert. + */ + if (!isVersionedUrl) { + return null + } + + /** + * If this is a versioned URL, but it's the same content as the latest URL, + * we also won't show a version alert. + */ + if (latestStableVersion.versionId === currentVersion.versionId) { + return null + } + + /** + * Otherwise, build a message and link, and show the version alert. + */ + const latestLinkText = 'View latest version' + let versionMessage: string + if (currentVersion.releaseStage === 'preview') { + // May be a preview version + versionMessage = `You are viewing documentation for the preview version ${currentVersion.versionId}.` + } else { + // Otherwise, is some other version, such as non-latest stable version + versionMessage = `You are viewing documentation for version ${currentVersion.versionId}.` + } + + return ( + + ) +} + +/** + * Display a generic version alert + */ +function VersionAlert({ + message, + latestLinkUrl, + latestLinkText, +}: { + message: string + latestLinkUrl: string + latestLinkText: string +}) { + return ( + + {message}{' '} + + {latestLinkText} + + . + + } + icon={} + type="highlight" + /> + ) +} diff --git a/src/views/open-api-docs-view-v2/components/version-alert/types.ts b/src/views/open-api-docs-view-v2/components/version-alert/types.ts new file mode 100644 index 0000000000..b23ae6328a --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/version-alert/types.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +export interface OpenApiV2VersionAlertProps { + isVersionedUrl: boolean + currentVersion: { versionId: string; releaseStage?: string } + latestStableVersion: { versionId: string } + basePath: string +} diff --git a/src/views/open-api-docs-view-v2/components/version-alert/version-alert.module.css b/src/views/open-api-docs-view-v2/components/version-alert/version-alert.module.css new file mode 100644 index 0000000000..0297d3a650 --- /dev/null +++ b/src/views/open-api-docs-view-v2/components/version-alert/version-alert.module.css @@ -0,0 +1,15 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +.root { + padding-left: 24px; + padding-right: 24px; +} + +.versionAlertLink { + /* Note: !important seems necessary here to ensure the color is applied + to all states. @TODO: update InlineLink to better support theming. */ + color: var(--token-color-foreground-primary) !important; +} diff --git a/src/views/open-api-docs-view-v2/index.tsx b/src/views/open-api-docs-view-v2/index.tsx index bae12dc389..ecb63c159e 100644 --- a/src/views/open-api-docs-view-v2/index.tsx +++ b/src/views/open-api-docs-view-v2/index.tsx @@ -10,14 +10,18 @@ import { mobileMenuLevelMain, mobileMenuLevelProduct, } from '@components/mobile-menu-levels/level-components' -import { OpenApiV2SidebarContents } from './components/sidebar' import { SidebarHorizontalRule } from '@components/sidebar/components' -import { SidebarResourceLinks } from './components/sidebar-resource-links' -import { LandingContent } from './components/landing-content' import MobileMenuLevels from '@components/mobile-menu-levels' -import OperationContent from './components/operation-content' import SidebarBackToLink from '@components/sidebar/components/sidebar-back-to-link' import BreadcrumbBar from '@components/breadcrumb-bar' +import VersionSwitcher from '@components/version-switcher' +import NoIndexTagIfVersioned from '@components/no-index-tag-if-versioned' +// Components, local +import { OpenApiV2SidebarContents } from './components/sidebar' +import { SidebarResourceLinks } from './components/sidebar-resource-links' +import { LandingContent } from './components/landing-content' +import OperationContent from './components/operation-content' +import { OpenApiV2VersionAlert } from './components/version-alert' // Types import type { OpenApiDocsViewV2Props } from './types' // Styles @@ -36,7 +40,9 @@ export default function OpenApiDocsViewV2({ landingLink, operationLinkGroups, resourceLinks, - productData, + product, + versionMetadata, + versionSwitcherProps, ...restProps }: OpenApiDocsViewV2Props) { // @@ -44,7 +50,6 @@ export default function OpenApiDocsViewV2({ - {/* Back to link, meant for navigating up a level of context */} {backToLink ? ( ) : null} @@ -60,18 +65,11 @@ export default function OpenApiDocsViewV2({ ) : null} } - /** - * TODO: implement mobile menu. May be tempting to try to re-use the data - * that feeds the sidebar, and this MAY be the right call, or MAY make - * sense to have them a bit more separate (more flexibility in how we - * present the sidebar, without having to disentangle all the complexity - * of the mobile menu quite yet). - */ mobileMenuSlot={ } > + +
- {/** - * TODO: implement version selector, likely alongside breadcrumbs. - * Previously was part of "overview content", but now version - * selector will need to be present on individual operation pages - * as well. - */} {'operationContentProps' in restProps ? ( - + + ) : null + } + /> ) : 'landingProps' in restProps ? ( + ) : null + } heading={restProps.landingProps.heading} badgeText={restProps.landingProps.badgeText} serviceProductSlug={restProps.landingProps.serviceProductSlug} diff --git a/src/views/open-api-docs-view-v2/server.ts b/src/views/open-api-docs-view-v2/server.ts index 17462c234a..7b2ebe71d3 100644 --- a/src/views/open-api-docs-view-v2/server.ts +++ b/src/views/open-api-docs-view-v2/server.ts @@ -3,26 +3,129 @@ * SPDX-License-Identifier: MPL-2.0 */ +// Utils +import { cachedGetProductData } from 'lib/get-product-data' +import { fetchCloudApiVersionData } from 'lib/api-docs' +import { getVersionSwitcherProps } from 'views/open-api-docs-view/utils' +import { isDeployPreview } from 'lib/env-checks' import { parseAndValidateOpenApiSchema } from 'lib/api-docs/parse-and-validate-open-api-schema' +import { serialize } from 'lib/next-mdx-remote/serialize' import { stripUndefinedProperties } from 'lib/strip-undefined-props' +import fetchGithubFile from 'lib/fetch-github-file' import isAbsoluteUrl from 'lib/is-absolute-url' -// Utils +// Utils, local +import { findDefaultVersion } from './utils/find-default-version' import getOperationContentProps from './components/operation-content/server' -import { serialize } from 'lib/next-mdx-remote/serialize' import { getOperationObjects, OperationObject, } from './utils/get-operation-objects' -import { wordBreakCamelCase } from './utils/word-break-camel-case' import { groupItemsByKey } from './utils/group-items-by-key' -import { cachedGetProductData } from 'lib/get-product-data' +import { slugifyOperationId } from './utils/slugify-operation-id' +import { wordBreakCamelCase } from './utils/word-break-camel-case' +import { parseOpenApiV2UrlContext } from './utils/parse-open-api-v2-url-context' // Types +import type { ApiDocsVersionData } from 'lib/api-docs/types' +import type { BreadcrumbLink } from '@components/breadcrumb-bar' +import type { GetStaticPaths } from 'next' +import type { GithubDir } from 'lib/fetch-github-file-tree' import type { + ApiDocsUrlContext, + OpenApiDocsV2Params, + OpenApiDocsViewV2Config, OpenApiDocsViewV2Props, SharedProps, - OpenApiDocsViewV2Config, } from 'views/open-api-docs-view-v2/types' -import { OpenAPIV3 } from 'openapi-types' +import type { OpenAPIV3 } from 'openapi-types' + +/** + * Generate static paths for an OpenAPI docs view. + */ +export async function generateStaticPaths({ + schemaSource, + schemaTransforms = [], + transformVersionData = (versionData) => versionData, +}: { + schemaSource: ApiDocsVersionData[] | GithubDir + schemaTransforms?: ((schema: OpenAPIV3.Document) => OpenAPIV3.Document)[] + transformVersionData?: ( + versionData: ApiDocsVersionData[] + ) => ApiDocsVersionData[] +}): Promise>> { + // If we are in a product repo deploy preview, don't pre-render any paths + if (isDeployPreview()) { + return { paths: [], fallback: 'blocking' } + } + /** + * If we're in production, statically render the non-versioned landing view, + * as well as the non-versioned operation views. + * + * We use `fallback: blocking` for versioned views. We could in theory + * statically render all pages across all versions, but this would increase + * our build times. + * + * We fetch and parse the default version of the OpenAPI schema to figure + * out which operation slugs to statically render. Note the "default version" + * is the latest stable version, or if there are no stable versions, + * then the latest version regardless of release stage. + */ + // Fetch version data + const rawVersionData = Array.isArray(schemaSource) + ? schemaSource + : await fetchCloudApiVersionData(schemaSource) + const versionData = transformVersionData(rawVersionData) + // Determine the default version + const defaultVersion = findDefaultVersion(versionData) + // Parse the default version + const schemaFileString = + typeof defaultVersion.sourceFile === 'string' + ? defaultVersion.sourceFile + : await fetchGithubFile(defaultVersion.sourceFile) + const schemaData = await parseAndValidateOpenApiSchema( + schemaFileString, + schemaTransforms + ) + // Extract operation objects, and map to slugs + const operationObjects = getOperationObjects(schemaData) + const operationSlugs = operationObjects.map((operation) => { + return slugifyOperationId(operation.operationId) + }) + // Generate path objects for each operation slug + const pathObjects = operationSlugs.map((operationSlug) => { + return { params: { page: [operationSlug] } } + }) + // Return paths + return { + paths: [{ params: { page: [] } }, ...pathObjects], + fallback: 'blocking', + } +} + +/** + * Wrapper around `generateStaticProps` that handles the common production + * use case of fetching version data from GitHub. + */ +export async function generateStaticPropsVersioned( + pageConfig: OpenApiDocsViewV2Config, + params: string[] | never +): Promise<{ props: OpenApiDocsViewV2Props } | { notFound: true }> { + // Fetch version data + const rawVersionData = Array.isArray(pageConfig.schemaSource) + ? pageConfig.schemaSource + : await fetchCloudApiVersionData(pageConfig.schemaSource) + const versionData = + typeof pageConfig.transformVersionData === 'function' + ? pageConfig.transformVersionData(rawVersionData) + : rawVersionData + // Parse the URL context, to determine the version and operationSlug. + const urlContext = parseOpenApiV2UrlContext(params) + // Return static props, or may return `{ notFound: true }`. + return await generateStaticProps({ + ...pageConfig, + versionData, + urlContext, + }) +} /** * Build static props for an OpenAPI docs view. @@ -30,111 +133,185 @@ import { OpenAPIV3 } from 'openapi-types' * There are two main views: * - Landing view, for the basePath, when no operationSlug is provided * - Operation view, for the specific operationSlug that's been provided + * + * This function expects `versionData`, an array of objects. This accommodates + * both the HCP-centric use case, where we fetch `versionData` from GitHub, + * with each `versionData` entry referencing a `GithubFile` as its `sourceFile`, + * as well as the more general use case, where `versionData` can be an array + * with a single object, with the entry passing the schema file string + * directly as the `sourceFile`. */ -export async function getStaticProps({ +export async function generateStaticProps({ + backToLink, basePath, - breadcrumbLinksPrefix, + breadcrumbLinksPrefix = [], getOperationGroupKey = (o: OperationObject) => (o.tags.length && o.tags[0]) ?? 'Other', - openApiJsonString, - operationSlug, - schemaTransforms, - theme = 'hcp', - backToLink, - statusIndicatorConfig, - releaseStage, resourceLinks = [], -}: OpenApiDocsViewV2Config): Promise { + statusIndicatorConfig, + schemaTransforms, + productContext, + theme = productContext, + versionData, + urlContext: { isVersionedUrl, versionId, operationSlug }, +}: Omit & { /** - * Grab product data for this context + * Data for all versions of target OpenAPI schema, include the release + * stage of each version, the source file, and the version ID. */ - const productData = cachedGetProductData(theme) + versionData: ApiDocsVersionData[] + /** + * The URL context in which we're fetching static props. This affects: + * - versioning, as the target versionId is determined by the URL + * - operation vs landing page, as an operationSlug may be present in the URL + */ + urlContext: ApiDocsUrlContext +}): Promise<{ props: OpenApiDocsViewV2Props } | { notFound: true }> { + /** + * Parse the version to render, or 404 if a non-existent version is requested. + */ + const defaultVersion = findDefaultVersion(versionData) + // Resolve the current version + let targetVersion: ApiDocsVersionData | undefined + if (isVersionedUrl) { + targetVersion = versionData.find((v) => v.versionId === versionId) + } else { + targetVersion = defaultVersion + } + // If we can't resolve the current version, render a 404 page + if (!targetVersion) { + return { notFound: true } + } + + /** + * Fetch the OpenAPI schema string for this version. + */ + const { sourceFile } = targetVersion + const schemaFileString = + typeof sourceFile === 'string' + ? sourceFile + : await fetchGithubFile(sourceFile) /** * Fetch, parse, and validate the OpenAPI schema for this version. * Also apply any schema transforms. */ const schemaData = await parseAndValidateOpenApiSchema( - openApiJsonString, - (schema: OpenAPIV3.Document) => { - let transformedSchema = schema - for (const schemaTransformFunction of schemaTransforms ?? []) { - transformedSchema = schemaTransformFunction(transformedSchema) - } - return transformedSchema - } + schemaFileString, + schemaTransforms ) /** - * TODO: add breadcrumb bar. Or, could be done separately for each view, - * if we have well-abstracted composable functions to build breadcrumbs? - * (maybe already started, build breadcrumb from URL path segments?) + * Build version selector and version alert props */ + const versionSwitcherProps = getVersionSwitcherProps({ + projectName: schemaData.info.title, + versionData, + targetVersion, + defaultVersion, + basePath, + }) /** - * TODO: version selector. Probably needs to come a little later, but - * seems like something that would be duplicated pretty exactly between - * the landing and operation views. That being said, maybe there's an - * opportunity here to build a clever linking strategy so that we don't - * link to 404s... most basic version might be "always link to the root of - * the current version, and let the user navigate from there."... but more - * complex version may end up meaning significant differences in the logic - * to generate the version selector depending on landing vs operation view. + * Grab product data for this context */ + const productData = cachedGetProductData(productContext) /** - * Build links for the sidebar. + * Determine if we're on a specific operation page or not */ const operationObjects = getOperationObjects(schemaData) + // If we're on a specific operation page, grab the target operation + let targetOperation: OperationObject | undefined + if (operationSlug) { + targetOperation = operationObjects.find( + (operation) => slugifyOperationId(operation.operationId) === operationSlug + ) + } + // If we have an operationSlug, but no target operation, return a 404 + if (typeof operationSlug === 'string' && !targetOperation) { + return { + notFound: true, + } + } + + /** + * Build links for the sidebar. + */ const operationGroups = groupItemsByKey( operationObjects, getOperationGroupKey ) + const landingUrl = isVersionedUrl ? `${basePath}/${versionId}` : basePath const landingLink = { theme, text: schemaData.info.title, - href: basePath, + href: landingUrl, isActive: !operationSlug, } const operationLinkGroups = operationGroups.map((group) => ({ // Note: we word break to avoid long strings breaking the sidebar layout text: wordBreakCamelCase(group.key), items: group.items.map(({ operationId }) => { + const operationSlug = slugifyOperationId(operationId) + const operationUrl = isVersionedUrl + ? `${basePath}/${versionId}/${operationSlug}` + : `${basePath}/${operationSlug}` return { text: wordBreakCamelCase(operationId), - href: `${basePath}/${operationId}`, + href: operationUrl, isActive: operationSlug === operationId, } }), })) /** - * Gather props shared between the landing and individual operation views. + * Build breadcrumb links */ - // Build breadcrumb links - const urlPath = [basePath, operationSlug].filter(Boolean).join('/') - const breadcrumbLinks = [...breadcrumbLinksPrefix] + const breadcrumbLinks: BreadcrumbLink[] = [...breadcrumbLinksPrefix] + // Push a link for the root of these docs breadcrumbLinks.push({ title: schemaData.info.title, url: basePath, }) + // If we have a versioned URL, push a link for the specific version + if (isVersionedUrl) { + breadcrumbLinks.push({ + title: versionId, + url: `${basePath}/${versionId}`, + }) + } // If we're on a specific operation page, add a breadcrumb link accordingly - if (operationSlug) { + if (targetOperation) { breadcrumbLinks.push({ - title: operationSlug, - url: urlPath, + title: targetOperation.operationId, + url: [basePath, operationSlug].filter(Boolean).join('/'), }) } // Mark the last breadcrumb link as the current page breadcrumbLinks[breadcrumbLinks.length - 1].isCurrentPage = true - // Build shared props + + /** + * Gather props shared between the landing and individual operation views. + */ const sharedProps: SharedProps = { basePath, backToLink, breadcrumbLinks, landingLink, operationLinkGroups, - productData, + product: productData, + versionMetadata: { + isVersionedUrl, + currentVersion: { + versionId: targetVersion.versionId, + releaseStage: targetVersion.releaseStage, + }, + latestStableVersion: { + versionId: defaultVersion.versionId, + }, + }, + versionSwitcherProps, resourceLinks: resourceLinks.map((item) => { return { ...item, isExternal: isAbsoluteUrl(item.href) } }), @@ -144,21 +321,37 @@ export async function getStaticProps({ * If we have an operation slug, build and return operation view props. * Otherwise, assume a landing view, and build and return landing view props. */ - if (operationSlug) { + if (targetOperation) { const operationContentProps = await getOperationContentProps( - operationSlug, + targetOperation, schemaData ) - return stripUndefinedProperties({ ...sharedProps, operationContentProps }) + return { + props: stripUndefinedProperties({ + ...sharedProps, + metadata: { + title: `${targetOperation.operationId} | ${schemaData.info.title}`, + }, + operationContentProps, + }), + } } else { const landingProps = { heading: schemaData.info.title, - badgeText: releaseStage, + badgeText: targetVersion.releaseStage, serviceProductSlug: theme, statusIndicatorConfig, descriptionMdx: await serialize(schemaData.info.description), - schemaFileString: openApiJsonString, + schemaFileString: schemaFileString, + } + return { + props: stripUndefinedProperties({ + ...sharedProps, + metadata: { + title: schemaData.info.title, + }, + landingProps, + }), } - return stripUndefinedProperties({ ...sharedProps, landingProps }) } } diff --git a/src/views/open-api-docs-view-v2/types.ts b/src/views/open-api-docs-view-v2/types.ts index 876cdd33cb..4e84265491 100644 --- a/src/views/open-api-docs-view-v2/types.ts +++ b/src/views/open-api-docs-view-v2/types.ts @@ -3,23 +3,38 @@ * SPDX-License-Identifier: MPL-2.0 */ -// Types +// Third-party +import type { NextParsedUrlQuery } from 'next/dist/server/request-meta' import type { OpenAPIV3 } from 'openapi-types' -import type { OperationContentProps } from './components/operation-content' +// App-wide types +import type { ApiDocsVersionData } from 'lib/api-docs/types' +import type { BreadcrumbLink } from '@components/breadcrumb-bar' +import type { GithubDir } from 'lib/fetch-github-file-tree' +import type { ProductData, ProductSlug } from 'types/products' +// Local types import type { LandingContentProps } from './components/landing-content' +import type { VersionSwitcherProps } from '@components/version-switcher' +import type { OperationContentProps } from './components/operation-content' import type { OperationObject } from './utils/get-operation-objects' -import type { ProductData, ProductSlug } from 'types/products' -import type { BreadcrumbLink } from '@components/breadcrumb-bar' /** * Shared props are common to both the "landing" and "operation" views. */ export interface SharedProps { + /** + * The URL path at which the docs are located. + */ basePath: string + /** + * Back to link, meant for navigating up a level of context + */ backToLink: { text: string href: string } + /** + * Breadcrumb links to render on the page. + */ breadcrumbLinks: { title: string /** @@ -46,12 +61,18 @@ export interface SharedProps { url?: string isCurrentPage?: boolean }[] + /** + * Link to the landing page, used in the sidebar and mobile menu. + */ landingLink: { text: string href: string isActive: boolean theme: ProductSlug } + /** + * Array of operation link groups to render in the sidebar and mobile menu. + */ operationLinkGroups: { text: string items: { @@ -60,12 +81,30 @@ export interface SharedProps { isActive: boolean }[] }[] - productData: ProductData + /** + * Product data, used to render links in the mobile menu. Due to a code path + * in `_app`, this also affects the navigation context. + */ + product: ProductData resourceLinks?: { text: string href: string isExternal: boolean }[] + /** + * Optional version metadata. Enables rendering of a version alert + * on non-default versions of the OpenAPI docs. + */ + versionMetadata?: { + isVersionedUrl: boolean + currentVersion: { versionId: string; releaseStage?: string } + latestStableVersion: { versionId: string } + } + /** + * Optional props to render a version selector. Note that if there's + * only a single version, the version selector will not be rendered. + */ + versionSwitcherProps?: VersionSwitcherProps } /** @@ -100,12 +139,17 @@ export type OpenApiDocsViewV2Props = * * We want to keep these configuration options as simple as possible, as * they must be configured for each new set of OpenAPI specs that we add. + * + * Ideally, we'll find a way at some point in the near future to make it + * easier for content authors and other folks managing OpenAPI docs to + * edit this configuration, and to add new pages. */ export interface OpenApiDocsViewV2Config { /** * The URL path at which the docs are located. For example, the URL * `developer.hashicorp.com/hcp/api-docs/hcp-vault-secrets` has the basePath - * `/hcp/api-docs/hcp-vault-secrets`. */ + * `/hcp/api-docs/hcp-vault-secrets` + */ basePath: string /** * Optional breadcrumb links to render before the generated breadcrumb links. @@ -141,17 +185,24 @@ export interface OpenApiDocsViewV2Config { */ getOperationGroupKey?: (o: OperationObject) => string /** - * The OpenAPI schema as a JSON string. + * Define a source for the OpenAPI schema. This can be a string, + * which is assumed to be the OpenAPI schema in JSON format, or it + * can be a GithubDir object, which is used to fetch versioned schema + * data from a structured directory in a specific Github repo. */ - openApiJsonString: string + schemaSource: ApiDocsVersionData[] | GithubDir /** - * Optional operation slug to render a specific operation view. + * The product context for the OpenAPI docs. This is used to set the top bar + * nav elements, and the mobile menu layer the "level up" from the + * operation navigation links within the OpenAPI spec. */ - operationSlug?: string + productContext: ProductSlug /** * Optional theme value to add specific product chrome to the view. * For example, when the `vault` value is provided, the Vault logo will * be shown in the sidebar and by the title of the OpenAPI spec. + * + * If omitted, will default to the value provided to `productContext`. */ theme?: ProductSlug /** @@ -176,8 +227,36 @@ export interface OpenApiDocsViewV2Config { */ statusIndicatorConfig?: StatusIndicatorConfig /** - * Optional release stage to display in a badge at the top of the page. - * Typical values include `Stable`, `Preview`, etc. + * Optional hook to allow transformation of versionData after it's been + * fetched from GitHub. Ideally we'd avoid this, the current use case + * is filtering out a specific version for `/hcp/api-docs/consul`. + */ + transformVersionData?: (v: ApiDocsVersionData[]) => ApiDocsVersionData[] +} + +/** + * Params type for `getStaticPaths` and `getStaticProps`. + * Encodes our assumption that a `[[...page]].tsx` file is being used. + */ +export interface OpenApiDocsV2Params extends NextParsedUrlQuery { + page: string[] +} + +/** + * URL context derived from params, used to encode the version ID and + * operation slug from the URL. + */ +export interface ApiDocsUrlContext { + /** + * Boolean describing whether the URL matches the versioned format. + */ + isVersionedUrl: boolean + /** + * The version ID from the URL, or "latest" if the URL is not versioned. + */ + versionId: string + /** + * The operation slug from the URL, if present. */ - releaseStage?: string + operationSlug: string | undefined } diff --git a/src/views/open-api-docs-view-v2/utils/find-default-version.ts b/src/views/open-api-docs-view-v2/utils/find-default-version.ts new file mode 100644 index 0000000000..15918de755 --- /dev/null +++ b/src/views/open-api-docs-view-v2/utils/find-default-version.ts @@ -0,0 +1,39 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import { ApiDocsVersionData } from 'lib/api-docs/types' +import { sortDateVersionData } from './sort-date-version-data' + +/** + * Given an array of version data, + * Return the default version that should be shown. + * + * - If there is exactly one version, we treat it as the default version. + * - If there are multiple versions, but no `stable` version, we'll show + * the very latest version as the default version (even if not `stable`). + * - If there are multiple versions, and at least one `stable` version, + * we'll show the latest `stable` version as the default version. + * + * Note: only supports date-based version formats, for example "2023-01-15". + * We'd need to update the sort logic in order to support other formats. + */ +export function findDefaultVersion( + versionData: ApiDocsVersionData[] +): ApiDocsVersionData { + // If we have exactly one version, we'll show that as the default. + if (versionData.length === 1) { + return versionData[0] + } + // Sort versions in descending order + const versionsDescending = sortDateVersionData(versionData) + // Ideally, we'll show the latest 'stable' release as the default. + const latestStableVersion = versionsDescending.find( + (v) => v.releaseStage === 'stable' + ) + // Fall back to the latest version (any stage!) if there's no 'stable' version + const defaultVersion = latestStableVersion || versionsDescending[0] + // Return the default version + return defaultVersion +} diff --git a/src/views/open-api-docs-view-v2/utils/get-version-switcher-props.ts b/src/views/open-api-docs-view-v2/utils/get-version-switcher-props.ts new file mode 100644 index 0000000000..d2bc7de89c --- /dev/null +++ b/src/views/open-api-docs-view-v2/utils/get-version-switcher-props.ts @@ -0,0 +1,69 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +// Types +import type { VersionSwitcherProps } from 'components/version-switcher' +import type { ApiDocsVersionData } from 'lib/api-docs/types' + +/** + * Given version data and other OpenAPI docs details, + * Return version switcher dropdown props for use in `OpenApiDocsView`. + * + * Note: If there is only one version, we return null. + */ +export function getVersionSwitcherProps({ + projectName, + versionData, + targetVersion, + defaultVersion, + basePath, +}: { + projectName: string + versionData: ApiDocsVersionData[] + targetVersion: ApiDocsVersionData + defaultVersion: ApiDocsVersionData + basePath: string +}): VersionSwitcherProps | null { + // Return null early if we only have one version + if (versionData.length === 1) { + return null + } + + // Otherwise, we have multiple versions, we need to build dropdown props + const label = projectName + + // Each version becomes an option in the dropdown + const options = versionData.map( + ({ versionId, releaseStage }: ApiDocsVersionData) => { + /** + * Determine the version label suffix to show. + * - Default case is to show the version only, no (suffix) + * - If this is the default version, show 'latest'. For information on + * what "default version" means, see `findDefaultVersion`. + * - If we have a defined releaseStage that isn't 'stable', show it + */ + const isLatest = versionId === defaultVersion.versionId + let versionLabelSuffix = '' + if (isLatest) { + versionLabelSuffix = ' (latest)' + } else if (releaseStage && releaseStage !== 'stable') { + versionLabelSuffix = ` (${releaseStage})` + } + // Construct the label to show in the dropdown + const label = versionId + versionLabelSuffix + // Construct the aria-label for the version dropdown. + const ariaLabel = `Choose a version of the API docs for ${projectName}. Currently viewing version ${label}.` + // Construct the `href` for this version, which is special if latest + const href = isLatest ? basePath : `${basePath}/${versionId}` + // Mark this version option as selected if it's the current option + const isSelected = versionId === targetVersion.versionId + // Return all the props + return { ariaLabel, href, isLatest, isSelected, label } + } + ) + + // Return the dropdown props + return { label, options } +} diff --git a/src/views/open-api-docs-view-v2/utils/parse-open-api-v2-url-context.ts b/src/views/open-api-docs-view-v2/utils/parse-open-api-v2-url-context.ts new file mode 100644 index 0000000000..c4faf4dc4f --- /dev/null +++ b/src/views/open-api-docs-view-v2/utils/parse-open-api-v2-url-context.ts @@ -0,0 +1,44 @@ +import { ApiDocsUrlContext } from '../types' + +/** + * Given an array of URL params, parse the context for an OpenAPI v2 URL, and + * Return { isVersionedUrl, versionId, operationSlug } parsed from the URL. + */ +export function parseOpenApiV2UrlContext( + params: string[] | never +): ApiDocsUrlContext { + /** + * We expect URLs in one of the two following formats: + * + * 1. Un-versioned, for the "latest" version: + * - / (landing page) + * - //[operationSlug] (operation page) + * + * 2. Versioned, for a specific version: + * - //[versionId] (landing page) + * - //[versionId]/[operationSlug] (operation page) + * + * We need to determine which format we're in, and then + * generate the appropriate static props. + * + * We expect versions to be in the format `YYYY-MM-DD`. + * If the first param is a version, we're in a versioned context. + * Note that we may not have a first param, if we're on the landing + * page in an un-versioned context. + */ + const pathParts = Array.isArray(params) ? params : [] + const isVersionedUrl = /^\d{4}-\d{2}-\d{2}$/.test(pathParts[0]) + const versionId = isVersionedUrl ? pathParts[0] : 'latest' + + // Note: operationSlug may be undefined, eg if we're on a landing page + const operationSlug = isVersionedUrl ? pathParts[1] : pathParts[0] + + /** + * Return the parsed context + */ + return { + isVersionedUrl, + versionId, + operationSlug, + } +} diff --git a/src/views/open-api-docs-view-v2/utils/slugify-operation-id.ts b/src/views/open-api-docs-view-v2/utils/slugify-operation-id.ts new file mode 100644 index 0000000000..0cca710cb4 --- /dev/null +++ b/src/views/open-api-docs-view-v2/utils/slugify-operation-id.ts @@ -0,0 +1,21 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import slugify from 'slugify' + +/** + * Given an operation ID, return a slugified version of it. + * + * This function has been abstracted as it's important for all instances + * of slugify-ing operation IDs to function in exactly the same way, + * as they're used for generating and then later matching URLs for + * individual operation pages. + * + * @param operationId + * @returns + */ +export function slugifyOperationId(operationId: string): string { + return slugify(operationId, { lower: true }) +} diff --git a/src/views/open-api-docs-view-v2/utils/sort-date-version-data.ts b/src/views/open-api-docs-view-v2/utils/sort-date-version-data.ts new file mode 100644 index 0000000000..ff30b63902 --- /dev/null +++ b/src/views/open-api-docs-view-v2/utils/sort-date-version-data.ts @@ -0,0 +1,22 @@ +/** + * Copyright (c) HashiCorp, Inc. + * SPDX-License-Identifier: MPL-2.0 + */ + +import type { ApiDocsVersionData } from 'lib/api-docs/types' + +/** + * Sort version data in descending order. + * + * Note: only works with `YYYY-MM-DD` version formats. + */ +export function sortDateVersionData( + versionData: ApiDocsVersionData[] +): ApiDocsVersionData[] { + return versionData.sort((a, b) => { + // We expect consistent YYYY-MM-DD formatting, so string compare works fine + const aBeforeB = a.versionId > b.versionId + const bBeforeA = b.versionId > a.versionId + return aBeforeB ? -1 : bBeforeA ? 1 : 0 + }) +} diff --git a/src/views/open-api-docs-view/server.ts b/src/views/open-api-docs-view/server.ts index 9d1289f50e..32385ed015 100644 --- a/src/views/open-api-docs-view/server.ts +++ b/src/views/open-api-docs-view/server.ts @@ -10,13 +10,13 @@ import { stripUndefinedProperties } from 'lib/strip-undefined-props' import { cachedGetProductData } from 'lib/get-product-data' import { getBreadcrumbLinks } from 'lib/get-breadcrumb-links' import { serialize } from 'lib/next-mdx-remote/serialize' +import { parseAndValidateOpenApiSchema } from 'lib/api-docs/parse-and-validate-open-api-schema' // Utilities import { findDefaultVersion, getNavItems, getOperationProps, groupOperations, - parseAndValidateOpenApiSchema, getVersionSwitcherProps, } from './utils' // Types @@ -109,10 +109,9 @@ export async function getStaticProps({ typeof sourceFile === 'string' ? sourceFile : await fetchGithubFile(sourceFile) - const schemaData = await parseAndValidateOpenApiSchema( - schemaFileString, - massageSchemaForClient - ) + const schemaData = await parseAndValidateOpenApiSchema(schemaFileString, [ + massageSchemaForClient, + ]) const operationProps = await getOperationProps(schemaData) const operationGroups = groupOperations(operationProps, groupOperationsByPath) const navItems = getNavItems({ diff --git a/src/views/open-api-docs-view/utils/index.ts b/src/views/open-api-docs-view/utils/index.ts index a9dcc8d341..0dece77858 100644 --- a/src/views/open-api-docs-view/utils/index.ts +++ b/src/views/open-api-docs-view/utils/index.ts @@ -13,7 +13,6 @@ export { export { getRequestData } from './get-request-data' export { getResponseData } from './get-response-data' export { groupOperations } from './group-operations' -export { parseAndValidateOpenApiSchema } from './parse-and-validate-schema' export { sortDateVersionData } from './sort-date-version-data' export { truncateHcpOperationPath } from './truncate-hcp-operation-path' export { getVersionSwitcherProps } from './get-version-switcher-props' diff --git a/src/views/open-api-docs-view/utils/parse-and-validate-schema.ts b/src/views/open-api-docs-view/utils/parse-and-validate-schema.ts deleted file mode 100644 index d81497e349..0000000000 --- a/src/views/open-api-docs-view/utils/parse-and-validate-schema.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright (c) HashiCorp, Inc. - * SPDX-License-Identifier: MPL-2.0 - */ - -import OASNormalize from 'oas-normalize' -import { OpenAPIV3 } from 'openapi-types' - -/** - * Given a fileString representing an OpenAPI schema specification, - * Return a parsed and validated OpenAPIV3 document. - * - * Note that there are multiple possible versions of OpenAPI schemas we - * may receive as input here. We use the `oas-normalize` package to - * convert the input to the latest version, and then dereference the - * schema to resolve any `$ref` references. - * - * - * The input OpenAPI specification can be in any of the following versions: - * - OpenAPI 2.0 (formerly known as Swagger) - * - OpenAPI 3.0 - * - * Note as well that the terminology associated with OpenAPI can be confusing. - * From the OpenAPI website: - * OpenAPI refers to the specification itself. - * Swagger refers to tooling for implementing the specification. - * - * However, this isn't always consistent, and Swagger is often used to refer - * to the specification as well, as that's how it was known for version 2.0. - */ -export async function parseAndValidateOpenApiSchema( - fileString: string, - massageSchemaForClient: (schema: OpenAPIV3.Document) => OpenAPIV3.Document = ( - schema - ) => schema -): Promise { - // Parse the fileString into raw JSON - const rawSchemaJson = JSON.parse(fileString) - - // Validate the file string, and up-convert it to OpenAPI 3.0 - const schemaJsonWithRefs = await new OASNormalize(rawSchemaJson).validate({ - convertToLatest: true, - }) - const massagedSchema = massageSchemaForClient(schemaJsonWithRefs) - - /** - * Dereference the schema. - * - * For context, in OpenAPI schemas, there are often pointers or references - * to shared definitions within the schema. In JSON, these might look like: - * - * "schema": { - * "$ref": "#/definitions/..." - * } - * - * Example: https://github.com/hashicorp/hcp-specs/blob/e65c7e982b65ce408ab7e456049a4bf3d5fa7ce0/specs/cloud-vault-secrets/preview/2023-06-13/hcp.swagger.json#L28 - * - * With the full schema file available, these references can be resolved - * by looking up the referenced definition and replacing the reference. - * For our purposes, it seems preferable to resolve these references - * so that we can pass data to presentational components that do not need - * to be aware of the full schema file in order to render the data. - * After de-referencing, the schema might look like: - * - * "schema": { - * "type": "object", - * "properties": { - * "some-referenced-stuff": "..." - * } - * } - */ - const schemaJson = await new OASNormalize(massagedSchema).deref() - // Return the dereferenced schema. - // We know it's OpenAPI 3.0, so we cast it to the appropriate type. - return schemaJson as OpenAPIV3.Document -}