+
{!hideHeader && (
-
{lang}
-
+
+ {header ?? lang}
+
+
)}
);
}
-
-interface Props {
- children: string;
- lang: string;
- header?: boolean;
-}
diff --git a/src/components/CodeBlock/CodeBlockSync.tsx b/src/components/CodeBlock/CodeBlockSync.tsx
new file mode 100644
index 00000000..40db360b
--- /dev/null
+++ b/src/components/CodeBlock/CodeBlockSync.tsx
@@ -0,0 +1,68 @@
+'use client';
+
+import { transformerNotationHighlight } from '@shikijs/transformers';
+import { cva, cx } from 'class-variance-authority';
+
+import { useHighlighter } from '@/lib/highlight/client';
+import { theme } from '@/lib/highlight/theme';
+
+import { ClipboardCopy } from '../ClipboardCopy/ClipboardCopy';
+import styles from './CodeBlock.module.css';
+import { Props } from './props';
+
+const codeBlock = cva(styles.root, {
+ variants: {
+ hideLineNumbers: {
+ false: styles.withLineNumbers,
+ },
+ hideHeader: {
+ true: styles.hideHeader,
+ },
+ darkerBackground: {
+ true: styles.darker,
+ },
+ },
+});
+
+export function CodeBlockSync({
+ variant = 'default',
+ children,
+ lang,
+ header,
+ hideHeader: _hideHeader = false,
+ className,
+}: Props) {
+ const hideHeader = (!lang && !header) || _hideHeader;
+ const hideLineNumbers = lang === 'bash' || lang === 'text';
+ const darkerBackground = variant === 'darker';
+
+ const { highlighter, isFetching, isError } = useHighlighter();
+
+ const html = highlighter?.codeToHtml(children, {
+ lang,
+ theme,
+ transformers: [
+ // Add more transformers when needed from https://shiki.style/packages/transformers
+ transformerNotationHighlight({ matchAlgorithm: 'v3' }),
+ ],
+ });
+
+ return (
+
+ {!hideHeader && (
+
+
+ {header ?? lang}
+
+
+
+ )}
+
+ {isFetching &&
Loading code block
}
+
+ {isError &&
Something went wrong while rendering code block
}
+
+ {html &&
}
+
+ );
+}
diff --git a/src/components/CodeBlock/index.ts b/src/components/CodeBlock/index.ts
index 5f5f442d..c3556c49 100644
--- a/src/components/CodeBlock/index.ts
+++ b/src/components/CodeBlock/index.ts
@@ -1 +1 @@
-export * from './CodeBlock';
\ No newline at end of file
+export * from './CodeBlock';
diff --git a/src/components/CodeBlock/props.ts b/src/components/CodeBlock/props.ts
new file mode 100644
index 00000000..057488d8
--- /dev/null
+++ b/src/components/CodeBlock/props.ts
@@ -0,0 +1,10 @@
+import { ReactNode } from 'react';
+
+export interface Props {
+ children: string;
+ className?: string;
+ lang: string;
+ header?: ReactNode;
+ hideHeader?: boolean;
+ variant?: 'default' | 'darker';
+}
diff --git a/src/components/Note/Note.tsx b/src/components/Note/Note.tsx
index 18dc43d6..6a2e54b2 100644
--- a/src/components/Note/Note.tsx
+++ b/src/components/Note/Note.tsx
@@ -1,5 +1,7 @@
import { cva, VariantProps } from 'class-variance-authority';
+
import { Icon } from '@/icons';
+
import styles from './Note.module.css';
const defaultHeadings = {
@@ -29,9 +31,9 @@ const note = cva(styles.root, {
},
});
-export function Note({ variant, headline, children, noHeadline, ...rest }: NoteProps) {
+export function Note({ variant, headline, children, noHeadline, className, ...rest }: NoteProps) {
return (
-
+
{noHeadline ? (
) : (
diff --git a/src/components/Tag/Tag.module.css b/src/components/Tag/Tag.module.css
index 47e677e2..e44feefc 100644
--- a/src/components/Tag/Tag.module.css
+++ b/src/components/Tag/Tag.module.css
@@ -120,18 +120,15 @@
}
}
-
-
-
/* legacy tags */
.legacy {
border-radius: 12px;
- font-size: .75em;
+ font-size: 0.75em;
font-weight: 500;
padding: 2px 10px 1px 10px;
text-decoration: none;
- transition: background-color .4s;
+ transition: background-color 0.4s;
display: inline-flex;
flex-shrink: 0;
@@ -144,7 +141,7 @@
}
.blue {
- --method-bg-color: #1478FF;
+ --method-bg-color: #1478ff;
--method-text-color: #fff;
}
@@ -153,6 +150,11 @@
--method-text-color: #fff;
}
+.light-red {
+ --method-bg-color: #ffebef;
+ --method-text-color: #b73b55;
+}
+
.purple {
--method-bg-color: #6f6fef;
--method-text-color: #fff;
@@ -169,12 +171,12 @@
}
.dark-grey {
- --method-bg-color: #9D9D9D;
+ --method-bg-color: #9d9d9d;
--method-text-color: #fff;
}
.dark-green {
- --method-bg-color: #0D4449;
+ --method-bg-color: #0d4449;
--method-text-color: #fff;
}
diff --git a/src/components/Tag/Tag.tsx b/src/components/Tag/Tag.tsx
index c5a7028f..f9907b4c 100644
--- a/src/components/Tag/Tag.tsx
+++ b/src/components/Tag/Tag.tsx
@@ -1,4 +1,4 @@
-import { VariantProps, cva } from 'class-variance-authority';
+import { cva, VariantProps } from 'class-variance-authority';
import { OpenAPIV3 } from 'openapi-types';
import styles from './Tag.module.css';
@@ -23,6 +23,7 @@ const tagVariants = cva(styles.root, {
yellow: styles.yellow,
lightyellow: styles.lightyellow,
red: styles.red,
+ 'light-red': styles['light-red'],
grey: styles.grey,
blue: styles.blue,
@@ -64,13 +65,13 @@ const statusCodes: { [key in Tag.HttpResponseStatusCodes]: Tag.VariantsProps['va
200: 'green',
201: 'green',
204: 'green',
- 400: 'red',
- 401: 'red',
- 402: 'red',
- 403: 'red',
- 404: 'red',
- 417: 'red',
- 422: 'red',
+ 400: 'light-red',
+ 401: 'light-red',
+ 402: 'light-red',
+ 403: 'light-red',
+ 404: 'light-red',
+ 417: 'light-red',
+ 422: 'light-red',
};
export const Tag = ({ theme, size, type, active, mobileDarkMode, className, ...props }: Tag.Props) => {
diff --git a/src/icons/icon-registry.tsx b/src/icons/icon-registry.tsx
index cfe4bdb8..1d27fad6 100644
--- a/src/icons/icon-registry.tsx
+++ b/src/icons/icon-registry.tsx
@@ -1,31 +1,20 @@
-import { createIconRegistry } from './util/create-icon-registry';
-
-import { RegoIcon } from './svgs/Rego';
-import { MenuIcon } from './svgs/Menu';
-import { ChevronIcon } from './svgs/Chevron';
-import { ChevronSmallIcon } from './svgs/ChevronSmall';
-import { ArrowIcon } from './svgs/Arrow';
-import { SearchIcon } from './svgs/Search';
-import { ExternalIcon } from './svgs/External';
-import { EditIcon } from './svgs/Edit';
-import { GithubIcon } from './svgs/Github';
-import { InfoIcon } from './svgs/Info';
-
-import { UtilityDocumentationIcon } from './svgs/utility/Documentation';
-import { UtilityGuideIcon } from './svgs/utility/Guide';
-import { UtilityApiIcon } from './svgs/utility/Api';
-
+import { ActionApiIcon } from './svgs/action/Api';
+import { ActionCheckIcon } from './svgs/action/Check';
import { CloseIcon } from './svgs/action/Close';
+import { ActionCopyIcon } from './svgs/action/Copy';
import { ActionDocumentationIcon } from './svgs/action/Documentation';
+import { ActionErrorIcon } from './svgs/action/Error';
+import { ActionEyeIcon } from './svgs/action/Eye';
+import { ActionEyeSlashedIcon } from './svgs/action/EyeSlashed';
import { ActionGuideIcon } from './svgs/action/Guide';
-import { ActionApiIcon } from './svgs/action/Api';
import { ActionLinkIcon } from './svgs/action/Link';
import { ActionPlayIcon } from './svgs/action/Play';
-import { ActionCopyIcon } from './svgs/action/Copy';
-import { ActionCheckIcon } from './svgs/action/Check';
-import { ActionErrorIcon } from './svgs/action/Error';
+import { ArrowIcon } from './svgs/Arrow';
+import { ChevronIcon } from './svgs/Chevron';
+import { ChevronSmallIcon } from './svgs/ChevronSmall';
+import { EditIcon } from './svgs/Edit';
import { EnterIcon } from './svgs/Enter';
-
+import { ExternalIcon } from './svgs/External';
import { FormatAlpineIcon } from './svgs/format/Alpine';
import { FormatCargoIcon } from './svgs/format/Cargo';
import { FormatChocolateyIcon } from './svgs/format/Chocolatey';
@@ -39,9 +28,9 @@ import { FormatDebianIcon } from './svgs/format/Debian';
import { FormatDockerIcon } from './svgs/format/Docker';
import { FormatGoIcon } from './svgs/format/Go';
import { FormatGradleIcon } from './svgs/format/Gradle';
-import { FormatHuggingFaceIcon } from './svgs/format/HuggingFace';
import { FormatHelmIcon } from './svgs/format/Helm';
import { FormatHexIcon } from './svgs/format/Hex';
+import { FormatHuggingFaceIcon } from './svgs/format/HuggingFace';
import { FormatLuarocksIcon } from './svgs/format/Luarocks';
import { FormatMavenIcon } from './svgs/format/Maven';
import { FormatNpmIcon } from './svgs/format/Npm';
@@ -57,14 +46,14 @@ import { FormatSwiftIcon } from './svgs/format/Swift';
import { FormatTerraformIcon } from './svgs/format/Terraform';
import { FormatUnityIcon } from './svgs/format/Unity';
import { FormatVagrantIcon } from './svgs/format/Vagrant';
-
+import { GithubIcon } from './svgs/Github';
+import { HomepageAPIIcon } from './svgs/homepage/API';
import { HomepageDocumentationIcon } from './svgs/homepage/Documentation';
import { HomepageGuideIcon } from './svgs/homepage/Guide';
-import { HomepageAPIIcon } from './svgs/homepage/API';
-
-import { IntegrationArgoCDIcon } from './svgs/integration/ArgoCD';
+import { InfoIcon } from './svgs/Info';
import { IntegrationAikidoIcon } from './svgs/integration/Aikido';
import { IntegrationAnsibleIcon } from './svgs/integration/Ansible';
+import { IntegrationArgoCDIcon } from './svgs/integration/ArgoCD';
import { IntegrationAWSCodeBuildIcon } from './svgs/integration/AWSCodeBuild';
import { IntegrationAzureDevOpsIcon } from './svgs/integration/AzureDevOps';
import { IntegrationBitbucketPipelinesIcon } from './svgs/integration/BitbucketPipelines';
@@ -94,8 +83,16 @@ import { IntegrationTeamCityIcon } from './svgs/integration/TeamCity';
import { IntegrationTerraformIcon } from './svgs/integration/Terraform';
import { IntegrationTheiaIDEIcon } from './svgs/integration/TheiaIDE';
import { IntegrationTravisCIIcon } from './svgs/integration/TravisCI';
-import { IntegrationZapierIcon } from './svgs/integration/Zapier';
import { IntegrationVSCodeIcon } from './svgs/integration/VSCode';
+import { IntegrationZapierIcon } from './svgs/integration/Zapier';
+import { MenuIcon } from './svgs/Menu';
+import { QuestionIcon } from './svgs/Question';
+import { RegoIcon } from './svgs/Rego';
+import { SearchIcon } from './svgs/Search';
+import { UtilityApiIcon } from './svgs/utility/Api';
+import { UtilityDocumentationIcon } from './svgs/utility/Documentation';
+import { UtilityGuideIcon } from './svgs/utility/Guide';
+import { createIconRegistry } from './util/create-icon-registry';
export const iconRegistry = createIconRegistry({
menu: MenuIcon,
@@ -127,6 +124,7 @@ export const iconRegistry = createIconRegistry({
edit: EditIcon,
github: GithubIcon,
info: InfoIcon,
+ question: QuestionIcon,
'utility/documentation': UtilityDocumentationIcon,
'utility/guide': UtilityGuideIcon,
'utility/api': UtilityApiIcon,
@@ -139,6 +137,8 @@ export const iconRegistry = createIconRegistry({
'action/copy': ActionCopyIcon,
'action/check': ActionCheckIcon,
'action/error': ActionErrorIcon,
+ 'action/eye': ActionEyeIcon,
+ 'action/eye-slashed': ActionEyeSlashedIcon,
'format/alpine': FormatAlpineIcon,
'format/cargo': FormatCargoIcon,
'format/chocolatey': FormatChocolateyIcon,
diff --git a/src/icons/svgs/Question.tsx b/src/icons/svgs/Question.tsx
new file mode 100644
index 00000000..b271480d
--- /dev/null
+++ b/src/icons/svgs/Question.tsx
@@ -0,0 +1,18 @@
+import { createIcon, SpecificIconProps } from '../util/create-icon';
+
+export const QuestionIcon = createIcon(
+ 'question',
+ ({ width = 16, height = 16, ...props }) => ({
+ ...props,
+ width,
+ height,
+ viewBox: '0 0 16 16',
+ fill: 'none',
+ children: (
+
+ ),
+ }),
+);
diff --git a/src/icons/svgs/action/Eye.tsx b/src/icons/svgs/action/Eye.tsx
new file mode 100644
index 00000000..1ad63a8e
--- /dev/null
+++ b/src/icons/svgs/action/Eye.tsx
@@ -0,0 +1,20 @@
+import { createIcon } from '../../util/create-icon';
+
+export const ActionEyeIcon = createIcon('action/eye', (props) => ({
+ ...props,
+ fill: 'none',
+ children: (
+ <>
+
+
+ >
+ ),
+}));
diff --git a/src/icons/svgs/action/EyeSlashed.tsx b/src/icons/svgs/action/EyeSlashed.tsx
new file mode 100644
index 00000000..22b5ad3a
--- /dev/null
+++ b/src/icons/svgs/action/EyeSlashed.tsx
@@ -0,0 +1,12 @@
+import { createIcon } from '../../util/create-icon';
+
+export const ActionEyeSlashedIcon = createIcon('action/eye-slashed', (props) => ({
+ ...props,
+ fill: 'none',
+ children: (
+
+ ),
+}));
diff --git a/src/icons/svgs/action/eyeslashed.svg b/src/icons/svgs/action/eyeslashed.svg
new file mode 100644
index 00000000..ba9ad5ee
--- /dev/null
+++ b/src/icons/svgs/action/eyeslashed.svg
@@ -0,0 +1,3 @@
+
diff --git a/src/lib/highlight/client.ts b/src/lib/highlight/client.ts
new file mode 100644
index 00000000..40e797f5
--- /dev/null
+++ b/src/lib/highlight/client.ts
@@ -0,0 +1,28 @@
+import { useEffect, useState } from 'react';
+
+import type { Highlighter } from 'shiki';
+
+import { getHighlighter } from './server';
+
+export const useHighlighter = () => {
+ const [highlighter, setHighlighter] = useState(null);
+ const [fetching, setFetching] = useState(false);
+ const [error, setError] = useState(false);
+
+ useEffect(() => {
+ if (!highlighter && !fetching) {
+ setFetching(true);
+ getHighlighter()
+ .then((h) => {
+ setHighlighter(h);
+ setFetching(false);
+ })
+ .catch(() => {
+ setError(true);
+ setFetching(false);
+ });
+ }
+ }, [highlighter, fetching]);
+
+ return { highlighter, isFetching: fetching, isError: error };
+};
diff --git a/src/lib/highlight.tsx b/src/lib/highlight/server.ts
similarity index 73%
rename from src/lib/highlight.tsx
rename to src/lib/highlight/server.ts
index 74eb1c7e..caa5e23a 100644
--- a/src/lib/highlight.tsx
+++ b/src/lib/highlight/server.ts
@@ -1,7 +1,8 @@
+import type { Highlighter } from 'shiki';
-import { createHighlighter, type Highlighter } from 'shiki';
+import { createHighlighter } from 'shiki';
-export const theme = 'github-dark-default';
+import { theme } from './theme';
let highlighter: Highlighter | null = null;
@@ -10,7 +11,7 @@ export async function getHighlighter() {
highlighter = await createHighlighter({
themes: [theme],
langs: [
- () => import('./lang/rego.json'),
+ () => import('../lang/rego.json'),
'js',
'jsx',
'ts',
@@ -29,9 +30,9 @@ export async function getHighlighter() {
'xml',
'scala',
'python',
- 'scss',
+ 'scss',
'ruby',
- 'csv'
+ 'csv',
],
});
}
diff --git a/src/lib/highlight/theme.ts b/src/lib/highlight/theme.ts
new file mode 100644
index 00000000..928ea4c1
--- /dev/null
+++ b/src/lib/highlight/theme.ts
@@ -0,0 +1 @@
+export const theme = 'github-dark-default';
diff --git a/src/lib/operations/hooks.ts b/src/lib/operations/hooks.ts
new file mode 100644
index 00000000..7403c19f
--- /dev/null
+++ b/src/lib/operations/hooks.ts
@@ -0,0 +1,100 @@
+import { useCallback, useState } from 'react';
+
+import { operationUrl } from '@/lib/operations/util';
+import { ApiOperation } from '@/lib/swagger/types';
+
+import { callApi } from './server';
+
+export const useApi = (
+ op: ApiOperation,
+ parameters: {
+ path: Record;
+ query: Record;
+ body: Record>;
+ },
+ header: 'apikey' | 'basic' | null,
+ headerValue: string | null,
+) => {
+ const [response, setResponse] = useState<
+ | {
+ status: null;
+ body: { error: string };
+ }
+ | {
+ status: number;
+ body: object;
+ }
+ | undefined
+ >(undefined);
+ const [isFetching, setIsFetching] = useState(false);
+
+ const call = () => {
+ setIsFetching(true);
+ setResponse(undefined);
+
+ const baseUrl = operationUrl(op);
+
+ const pathReplacedUrl = Object.entries(parameters.path).reduce((url, current) => {
+ const [param, value] = current;
+ const template = `{${param}}`;
+ if (value !== '' && url.includes(template)) {
+ return url.replaceAll(`{${param}}`, value);
+ }
+ return url;
+ }, baseUrl);
+
+ const cleanedUrl = pathReplacedUrl.replaceAll('\{', '').replaceAll('\}', '');
+
+ const finalUrl = Object.entries(parameters.query)
+ .filter((entry) => entry[1] !== '')
+ .reduce((url, current, index) => {
+ let currenUrl: string = url;
+ if (index === 0) {
+ currenUrl += '?';
+ } else {
+ currenUrl += '&';
+ }
+ currenUrl += `${current[0]}=${current[1]}`;
+
+ return currenUrl;
+ }, cleanedUrl);
+
+ const headers: HeadersInit = {
+ accept: 'application/json',
+ 'content-type': 'application/json',
+ };
+
+ if (header && headerValue) {
+ const headerKey = header === 'apikey' ? 'X-Api-Key' : 'authorization';
+ const value = header === 'apikey' ? headerValue : `Basic ${btoa(headerValue)}`;
+ headers[headerKey] = value;
+ }
+
+ const body: Record = {};
+ if (parameters.body && parameters.body['application/json']) {
+ const bodyParams = Object.entries(parameters.body['application/json']).filter(
+ (entry) => entry[1] != '',
+ );
+ if (bodyParams.length > 0) {
+ bodyParams.forEach((param) => {
+ body[param[0]] = param[1];
+ });
+ }
+ }
+
+ callApi(finalUrl, op.method, headers, Object.keys(body).length > 0 ? JSON.stringify(body) : undefined)
+ .then((response) => {
+ setResponse(response);
+ })
+ .catch((response) => {
+ setResponse(response);
+ })
+ .finally(() => setIsFetching(false));
+ };
+
+ const reset = useCallback(() => {
+ setResponse(undefined);
+ }, [setResponse]);
+
+ return { response, isFetching, call, reset };
+};
diff --git a/src/lib/operations/server.ts b/src/lib/operations/server.ts
new file mode 100644
index 00000000..2826e6bc
--- /dev/null
+++ b/src/lib/operations/server.ts
@@ -0,0 +1,35 @@
+'use server';
+
+export const callApi = async (
+ url: string,
+ method: string,
+ headers: HeadersInit,
+ body?: BodyInit,
+): Promise<
+ | {
+ status: null;
+ body: { error: string };
+ }
+ | {
+ status: number;
+ body: object;
+ }
+> => {
+ try {
+ const response = await fetch(url, {
+ method,
+ body,
+ headers,
+ });
+ const responseBody = await response.json();
+ return {
+ status: response.status,
+ body: responseBody,
+ };
+ } catch {
+ return {
+ status: null,
+ body: { error: 'Something went wrong. Please try again later.' },
+ };
+ }
+};
diff --git a/src/lib/operations/util.ts b/src/lib/operations/util.ts
new file mode 100644
index 00000000..5a75e899
--- /dev/null
+++ b/src/lib/operations/util.ts
@@ -0,0 +1,213 @@
+import { ApiOperation, SchemaObject } from '../swagger/types';
+
+export const operationUrl = (operation: ApiOperation) =>
+ `${process.env.NEXT_PUBLIC_CLOUDSMITH_API_URL}/${operation.version}${operation.path}/`;
+
+/**
+ * Turns an operation slug into a fully qualified local path to use in links
+ */
+export const operationPath = (slug: string): string => {
+ return `/api/${slug}`;
+};
+
+export const getParametersByParam = (operation: ApiOperation, param: string) =>
+ operation.parameters?.filter((p) => p.in === param);
+
+export const getHeaderOptions = (operation: ApiOperation) =>
+ Array.from(
+ new Set(
+ (operation.security ?? [])
+ .flatMap((s) => Object.keys(s))
+ .filter((s) => s === 'apikey' || s === 'basic'),
+ ),
+ ).toSorted();
+
+export const operationKey = (op: ApiOperation) => `${op.method}-${op.path}`;
+
+export const curlCommand = (
+ op: ApiOperation,
+ parameters: {
+ path: Record;
+ query: Record;
+ body: Record>;
+ },
+ _header: ['apikey' | 'basic' | null, string | null],
+) => {
+ let command = `curl --request ${op.method.toUpperCase()} \\\n`;
+
+ const baseUrl = operationUrl(op);
+
+ const pathReplacedUrl = Object.entries(parameters.path).reduce((url, current) => {
+ const [param, value] = current;
+ const template = `{${param}}`;
+ if (value !== '' && url.includes(template)) {
+ return url.replaceAll(`{${param}}`, value);
+ }
+ return url;
+ }, baseUrl);
+
+ const cleanedUrl = pathReplacedUrl.replaceAll('\{', '').replaceAll('\}', '');
+
+ const finalUrl = Object.entries(parameters.query)
+ .filter((entry) => entry[1] !== '')
+ .reduce((url, current, index) => {
+ let currenUrl: string = url;
+ if (index === 0) {
+ currenUrl += '?';
+ } else {
+ currenUrl += '&';
+ }
+ currenUrl += `${current[0]}=${current[1]}`;
+
+ return currenUrl;
+ }, cleanedUrl);
+
+ command += ` --url '${finalUrl}' \\\n`;
+
+ const [header, headerValue] = _header;
+
+ if (header && headerValue) {
+ const headerStart = header === 'apikey' ? 'X-Api-Key: ' : 'authorization: Basic ';
+ const headerEnd = header === 'apikey' ? headerValue : btoa(headerValue);
+ command += ` --header '${headerStart}${headerEnd}' \\\n`;
+ }
+
+ command += ` --header 'accept:application/json' \\\n`;
+ command += ` --header 'content-type: application/json' `;
+
+ if (parameters.body && parameters.body['application/json']) {
+ const bodyParams = Object.entries(parameters.body['application/json']).filter((entry) => entry[1] != '');
+ if (bodyParams.length > 0) {
+ command += `\\\n`;
+ command += ` --data '\n`;
+ command += `{\n`;
+ bodyParams.forEach((param, index) => {
+ command += ` "${param[0]}": "${param[1]}"`;
+ if (index < bodyParams.length - 1) {
+ command += ',';
+ }
+ command += '\n';
+ });
+ command += `}\n`;
+ command += `'`;
+ }
+ }
+
+ return command;
+};
+
+/*
+ Based on:
+ https://spec.openapis.org/oas/v3.0.3.html#properties
+ https://datatracker.ietf.org/doc/html/draft-wright-json-schema-validation-00#section-5
+*/
+export const textualSchemaRules = (schema: SchemaObject) =>
+ Object.entries(schema).reduce((acc, [key, value]) => {
+ if (!value) {
+ return acc;
+ }
+
+ switch (key) {
+ // Validation keywords for numeric instances
+ case 'multipleOf':
+ acc.push(`multiple of ${value}`);
+ break;
+ case 'maximum':
+ if (!schema.minimum) {
+ acc.push(`length ≤ ${value}${schema.exclusiveMaximum ? ' (exclusive)' : ''}`);
+ }
+ break;
+ case 'minimum':
+ if (!schema.maximum) {
+ acc.push(`length ≥ ${value}${schema.exclusiveMinimum ? ' (exclusive)' : ''}`);
+ }
+ if (schema.maximum) {
+ acc.push(`length between ${value} and ${schema.maximum}`);
+ }
+ break;
+
+ // Validation keywords for strings
+ case 'maxLength':
+ if (!schema.minLength) {
+ acc.push(`length ≤ ${value}`);
+ }
+ break;
+ case 'minLength':
+ if (!schema.maxLength) {
+ acc.push(`length ≥ ${value}`);
+ }
+ if (schema.maxLength) {
+ acc.push(`length between ${value} and ${schema.maxLength}`);
+ }
+ break;
+ case 'pattern':
+ acc.push(`${value}`);
+ break;
+
+ // Validation keywords for arrays
+ case 'additionalItems':
+ if (value === false) {
+ acc.push('No additional items allowed');
+ }
+ break;
+ case 'maxItems':
+ if (!schema.minItems) {
+ acc.push(`items ≤ ${value}`);
+ }
+ break;
+ case 'minItems':
+ if (!schema.maxItems) {
+ acc.push(`items ≥ ${value}`);
+ }
+ if (schema.maxItems) {
+ const uniqueText = schema.uniqueItems ? ' (unique)' : '';
+ acc.push(`items between ${value} and ${schema.maxItems}${uniqueText}`);
+ }
+ break;
+ case 'uniqueItems':
+ if (value === true && !schema.minItems && !schema.maxItems) {
+ acc.push('Items must be unique');
+ }
+ break;
+
+ // Validation keywords for objects
+ case 'maxProperties':
+ acc.push(`length ≤ ${value}`);
+ break;
+ case 'minProperties':
+ acc.push(`length ≥ ${value}`);
+ break;
+ case 'additionalProperties':
+ if (value === false) {
+ acc.push('no additional properties allowed');
+ }
+ break;
+
+ // Validation keywords for any instance type
+ case 'enum':
+ acc.push(`Allowed values: ${value.join(', ')}`);
+ break;
+ case 'format':
+ // TODO: Decide what to do with format
+ // acc.push(`${value}`);
+ break;
+ case 'default':
+ acc.push(`Defaults to ${value}`);
+ break;
+
+ // TODO: What should we render here?
+ case 'allOf':
+ acc.push('Must match all schemas');
+ break;
+ case 'anyOf':
+ acc.push('Must match any schema');
+ break;
+ case 'oneOf':
+ acc.push('Must match exactly one schema');
+ break;
+ case 'not':
+ acc.push('Must not match schema');
+ break;
+ }
+ return acc;
+ }, []);
diff --git a/src/lib/search/server.ts b/src/lib/search/server.ts
index c52d7c9c..c9f2274b 100644
--- a/src/lib/search/server.ts
+++ b/src/lib/search/server.ts
@@ -1,14 +1,15 @@
'use server';
-import path from 'path';
import { readFile } from 'fs/promises';
+import path from 'path';
+
import { FullOptions, Searcher } from 'fast-fuzzy';
-import { SearchInput, SearchResult } from './types';
-import { parseSchemas, toOperations } from '../swagger/parse';
-import { apiOperationPath } from '../swagger/util';
import { contentPath, loadMdxInfo } from '../markdown/util';
import { extractMdxMetadata } from '../metadata/util';
+import { operationPath } from '../operations/util';
+import { parseSchemas, toOperations } from '../swagger/parse';
+import { SearchInput, SearchResult } from './types';
let fuzzySearcher: Searcher>;
@@ -47,7 +48,7 @@ export const performSearch = async (
items.push({
title: op.title,
content: op.description || 'Default content',
- path: apiOperationPath(op.slug),
+ path: operationPath(op.slug),
section: 'api',
method: op.method,
});
diff --git a/src/lib/swagger/parse.ts b/src/lib/swagger/parse.ts
index d3cca489..cfcd6c97 100644
--- a/src/lib/swagger/parse.ts
+++ b/src/lib/swagger/parse.ts
@@ -5,8 +5,9 @@ import SwaggerParser from '@apidevtools/swagger-parser';
import { OpenAPIV3 } from 'openapi-types';
import { MenuItem } from '../menu/types';
+import { operationPath } from '../operations/util';
import { ApiOperation, ParameterObject } from './types';
-import { apiOperationPath, createSlug, createTitle, isHttpMethod, parseMenuSegments } from './util';
+import { createSlug, createTitle, isHttpMethod, parseMenuSegments } from './util';
const SCHEMAS_DIR = 'src/content/schemas';
@@ -92,6 +93,7 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str
};
for (const { schema, version } of schemas) {
+ const defaultSecurity = schema.security;
for (const path in schema.paths) {
const pathObject = schema.paths[path];
for (const field in pathObject) {
@@ -144,6 +146,7 @@ export const toOperations = (schemas: { schema: OpenAPIV3.Document; version: str
slug,
title: createTitle(menuSegments),
experimental: operation.tags?.includes('experimental') === true,
+ security: operation.security ?? defaultSecurity,
});
}
}
@@ -173,11 +176,11 @@ export const toMenuItems = (operations: ApiOperation[]): MenuItem[] => {
if (!existing) {
existing = { title };
if (isLast) {
- existing.path = apiOperationPath(operation.slug);
+ existing.path = operationPath(operation.slug);
existing.method = operation.method;
} else {
if (!existing.path) {
- existing.path = apiOperationPath(operation.slug);
+ existing.path = operationPath(operation.slug);
}
existing.children = [];
}
diff --git a/src/lib/swagger/types.ts b/src/lib/swagger/types.ts
index c499e946..fbd019cd 100644
--- a/src/lib/swagger/types.ts
+++ b/src/lib/swagger/types.ts
@@ -100,7 +100,7 @@ interface ParameterBaseObject {
[media: string]: MediaTypeObject;
};
}
-type NonArraySchemaObjectType = 'boolean' | 'object' | 'number' | 'string' | 'integer';
+export type NonArraySchemaObjectType = 'boolean' | 'object' | 'number' | 'string' | 'integer';
type ArraySchemaObjectType = 'array';
export type SchemaObject = ArraySchemaObject | NonArraySchemaObject;
interface ArraySchemaObject extends BaseSchemaObject {
diff --git a/src/lib/swagger/util.ts b/src/lib/swagger/util.ts
index 2e7db9b9..5e1e2e2d 100644
--- a/src/lib/swagger/util.ts
+++ b/src/lib/swagger/util.ts
@@ -1,4 +1,5 @@
import { OpenAPIV3 } from 'openapi-types';
+
import { replaceAll, titleCase } from '../util';
export const isHttpMethod = (method: string): boolean =>
@@ -42,10 +43,3 @@ export const createSlug = (menuSegments: string[]): string => {
export const createTitle = (menuSegments: string[]): string => {
return menuSegments.join(' ');
};
-
-/**
- * Turns an operation slug into a fully qualified local path to use in links
- */
-export const apiOperationPath = (slug: string): string => {
- return `/api/${slug}`;
-};
diff --git a/src/lib/url.ts b/src/lib/url.ts
new file mode 100644
index 00000000..e69de29b