Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions src/components/swagger-viewer/curl-command-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Box from '@mui/material/Box';
import Button from '@mui/material/Button';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useTranslations } from 'next-intl';

type CurlCommandPreviewProps = {
command: string;
onCopy: () => void;
};

export function CurlCommandPreview({ command, onCopy }: CurlCommandPreviewProps) {
const t = useTranslations('swaggerViewer');

return (
<Paper sx={{ p: 1.5 }} variant="outlined">
<Stack spacing={1}>
<Stack
direction="row"
spacing={1}
sx={{ alignItems: 'center', justifyContent: 'space-between' }}
>
<Typography sx={{ fontWeight: 600 }} variant="caption">
{t('curlCommandLabel')}
</Typography>
<Button onClick={onCopy} size="small" type="button" variant="text">
{t('copyCurlButton')}
</Button>
</Stack>
<Box
component="pre"
sx={{
bgcolor: 'action.hover',
borderRadius: 1,
fontSize: '0.75rem',
m: 0,
overflow: 'auto',
p: 1,
whiteSpace: 'pre-wrap',
}}
>
{command}
</Box>
</Stack>
</Paper>
);
}
166 changes: 166 additions & 0 deletions src/components/swagger-viewer/endpoint-details.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import type { ReactNode } from 'react';

import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { useTranslations } from 'next-intl';

import type {
SwaggerEndpointParameter,
SwaggerEndpointRequestBody,
SwaggerEndpointResponse,
} from '@/utils/swagger-editor/get-swagger-endpoints';

export function DetailSection({ children, title }: { children: ReactNode; title: string }) {
return (
<Box>
<Typography sx={{ fontWeight: 700, mb: 1 }} variant="body2">
{title}
</Typography>
<Stack spacing={1}>{children}</Stack>
</Box>
);
}

export function EmptyDetails({ children }: { children: ReactNode }) {
return (
<Typography color="text.secondary" variant="body2">
{children}
</Typography>
);
}

export function ParameterDetails({ parameter }: { parameter: SwaggerEndpointParameter }) {
const t = useTranslations('swaggerViewer');

return (
<Paper sx={{ p: 1.5 }} variant="outlined">
<Stack spacing={1}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Chip label={parameter.in} size="small" />
<Typography sx={{ fontFamily: 'monospace', fontWeight: 700 }} variant="body2">
{parameter.name}
</Typography>
<Chip
color={parameter.required ? 'error' : 'default'}
label={parameter.required ? t('requiredLabel') : t('optionalLabel')}
size="small"
variant="outlined"
/>
</Stack>
{parameter.description && (
<Typography color="text.secondary" variant="body2">
{parameter.description}
</Typography>
)}
<SchemaText schema={parameter.schema} />
</Stack>
</Paper>
);
}

export function RequestBodyDetails({ requestBody }: { requestBody: SwaggerEndpointRequestBody }) {
const t = useTranslations('swaggerViewer');

return (
<Paper sx={{ p: 1.5 }} variant="outlined">
<Stack spacing={1}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Chip
color={requestBody.required ? 'error' : 'default'}
label={requestBody.required ? t('requiredLabel') : t('optionalLabel')}
size="small"
variant="outlined"
/>
<ContentTypes contentTypes={requestBody.contentTypes} />
</Stack>
{requestBody.description && (
<Typography color="text.secondary" variant="body2">
{requestBody.description}
</Typography>
)}
<SchemaText schema={requestBody.schema} />
<Examples examples={requestBody.examples} />
</Stack>
</Paper>
);
}

export function ResponseDetails({ response }: { response: SwaggerEndpointResponse }) {
return (
<Paper sx={{ p: 1.5 }} variant="outlined">
<Stack spacing={1}>
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flexWrap: 'wrap' }}>
<Chip color="primary" label={response.statusCode} size="small" variant="outlined" />
<ContentTypes contentTypes={response.contentTypes} />
</Stack>
{response.description && (
<Typography color="text.secondary" variant="body2">
{response.description}
</Typography>
)}
<SchemaText schema={response.schema} />
<Examples examples={response.examples} />
</Stack>
</Paper>
);
}

function ContentTypes({ contentTypes }: { contentTypes: string[] }) {
if (contentTypes.length === 0) {
return null;
}

return (
<Stack direction="row" spacing={1} sx={{ flexWrap: 'wrap' }}>
{contentTypes.map((contentType) => (
<Chip key={contentType} label={contentType} size="small" variant="outlined" />
))}
</Stack>
);
}

function Examples({ examples }: { examples: string[] }) {
const t = useTranslations('swaggerViewer');

if (examples.length === 0) {
return null;
}

return (
<Stack spacing={0.75}>
<Typography sx={{ fontWeight: 600 }} variant="caption">
{t('examplesLabel')}
</Typography>
{examples.map((example, index) => (
<Box
component="pre"
key={`${example}-${index}`}
sx={{
bgcolor: 'action.hover',
borderRadius: 1,
fontSize: '0.75rem',
m: 0,
overflow: 'auto',
p: 1,
whiteSpace: 'pre-wrap',
}}
>
{example}
</Box>
))}
</Stack>
);
}

function SchemaText({ schema }: { schema: string }) {
const t = useTranslations('swaggerViewer');

return (
<Typography color="text.secondary" variant="body2">
{t('schemaLabel')}: {schema || t('schemaNotSpecified')}
</Typography>
);
}
118 changes: 118 additions & 0 deletions src/components/swagger-viewer/swagger-viewer.helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import type {
SwaggerEndpoint,
SwaggerEndpointParameter,
} from '@/utils/swagger-editor/get-swagger-endpoints';

export type TryItOutRequest = {
body: string;
headers: Record<string, string>;
method: string;
url: string;
};

export function buildCurlCommand(request: TryItOutRequest) {
const parts = ['curl', '-X', request.method, shellQuote(request.url)];

for (const [key, value] of Object.entries(request.headers)) {
parts.push('-H', shellQuote(`${key}: ${value}`));
}

if (request.body) {
parts.push('--data-raw', shellQuote(request.body));
}

return parts.join(' ');
}

export function buildTryItOutRequest(
endpoint: SwaggerEndpoint,
serverUrl: string,
parameterValues: Record<string, string>,
body: string,
) {
try {
const url = new URL(
joinUrl(serverUrl, applyPathParameters(endpoint.path, endpoint.parameters, parameterValues)),
);
const headers: Record<string, string> = {};
const cookieValues: string[] = [];

for (const parameter of endpoint.parameters) {
const value = parameterValues[getParameterKey(parameter)] ?? '';

if (!value) {
continue;
}

if (parameter.in === 'query') {
url.searchParams.set(parameter.name, value);
}

if (parameter.in === 'header') {
headers[parameter.name] = value;
}

if (parameter.in === 'cookie') {
cookieValues.push(`${parameter.name}=${value}`);
}
}

if (cookieValues.length > 0) {
headers.Cookie = cookieValues.join('; ');
}

if (endpoint.requestBody?.contentTypes[0]) {
headers['Content-Type'] = endpoint.requestBody.contentTypes[0];
}

return {
body,
headers,
method: endpoint.method,
url: url.toString(),
};
} catch {
return null;
}
}

export function getParameterKey(parameter: SwaggerEndpointParameter) {
return `${parameter.in}:${parameter.name}`;
}

export function getTryItOutErrorKey(errorCode: unknown) {
switch (errorCode) {
case 'blockedUrl':
return 'tryItOutBlockedUrl';
case 'invalidPayload':
return 'tryItOutInvalidPayload';
case 'invalidUrl':
return 'tryItOutInvalidUrl';
case 'timeout':
return 'tryItOutTimeout';
default:
return 'tryItOutFailed';
}
}

function applyPathParameters(
path: string,
parameters: SwaggerEndpointParameter[],
parameterValues: Record<string, string>,
) {
return parameters
.filter((parameter) => parameter.in === 'path')
.reduce((currentPath, parameter) => {
const value = parameterValues[getParameterKey(parameter)] ?? '';

return currentPath.replaceAll(`{${parameter.name}}`, encodeURIComponent(value));
}, path);
}

function joinUrl(serverUrl: string, path: string) {
return `${serverUrl.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
}

function shellQuote(value: string) {
return `'${value.replaceAll("'", "'\\''")}'`;
}
Loading