Skip to content
Draft
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
1 change: 1 addition & 0 deletions src/json-schema-viewer.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'prismjs/components/prism-python';
import 'prismjs/components/prism-http';
import 'prismjs/components/prism-csharp';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-javadoclike';
import 'prismjs/components/prism-jsdoc';

// Styles
Expand Down
22 changes: 19 additions & 3 deletions src/styles/endpoint-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ export default css`
align-items: center;
cursor: pointer;
}
.m-endpoint > .endpoint-head:hover,
.m-endpoint > .endpoint-head.expanded {
border-color:var(--primary-color);
background-color:var(--light-primary-color, var(--light-bg));
}
.m-endpoint > .endpoint-head.put:hover,
.m-endpoint > .endpoint-head.put.expanded {
border-color:var(--orange);
Expand Down Expand Up @@ -78,14 +83,17 @@ export default css`
border-width:0px 1px 1px 5px;
border-style:solid;
box-shadow: 0px 4px 3px -3px rgba(0, 0, 0, 0.15);
border-color:var(--primary-color);
}
.m-endpoint .endpoint-body.delete{ border-color:var(--red); }
.m-endpoint .endpoint-body.put{ border-color:var(--orange); }
.m-endpoint .endpoint-body.post { border-color:var(--green); }
.m-endpoint .endpoint-body.get { border-color:var(--blue); }
.m-endpoint .endpoint-body.head,
.m-endpoint .endpoint-body.patch,
.m-endpoint .endpoint-body.options {
.m-endpoint .endpoint-body.options,
.m-endpoint .endpoint-body.trace,
.m-endpoint .endpoint-body.query {
border-color:var(--yellow);
}

Expand Down Expand Up @@ -121,6 +129,7 @@ export default css`
font-weight: bold;
text-transform:uppercase;
margin-right:5px;
border: 2px solid var(--primary-color);
}
.endpoint-head .method.delete{ border: 2px solid var(--red);}
.endpoint-head .method.put{ border: 2px solid var(--orange); }
Expand All @@ -129,7 +138,9 @@ export default css`
.endpoint-head .method.get.deprecated{ border: 2px solid var(--border-color); }
.endpoint-head .method.head,
.endpoint-head .method.patch,
.endpoint-head .method.options {
.endpoint-head .method.options,
.endpoint-head .method.trace,
.endpoint-head .method.query {
border: 2px solid var(--yellow);
}

Expand All @@ -154,9 +165,14 @@ api-response.view-mode {
border-style:dashed;
}

.view-mode-request {
border-color:var(--primary-color);
}
.head .view-mode-request,
.patch .view-mode-request,
.options .view-mode-request {
.options .view-mode-request,
.trace .view-mode-request,
.query .view-mode-request {
border-color:var(--yellow);
}
.put .view-mode-request {
Expand Down
5 changes: 4 additions & 1 deletion src/styles/font-styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,16 @@ export default css`
max-height: var(--resp-area-height, 400px);
color: var(--fg3);
}
.method-fg { color: var(--primary-color); }
.method-fg.put { color: var(--orange); }
.method-fg.post { color: var(--green); }
.method-fg.get { color: var(--blue); }
.method-fg.delete { color: var(--red); }
.method-fg.options,
.method-fg.head,
.method-fg.patch {
.method-fg.patch,
.method-fg.trace,
.method-fg.query {
color: var(--yellow);
}

Expand Down
224 changes: 132 additions & 92 deletions src/utils/spec-parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,50 @@ import OpenApiParser from '@apitools/openapi-parser';
import { marked } from 'marked';
import { invalidCharsRegEx, rapidocApiKey, sleep } from '~/utils/common-utils';

const fixedOperationFields = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace', 'query']; // this is also used for ordering endpoints by methods

function isFixedOperationField(methodName) {
return fixedOperationFields.includes(methodName.toLowerCase());
}

function getOperationEntries(pathItem) {
const entries = fixedOperationFields
.filter((methodName) => pathItem[methodName])
.map((methodName) => [methodName, pathItem[methodName]]);

Object.entries(pathItem.additionalOperations || {}).forEach(([methodName, operation]) => {
if (!isFixedOperationField(methodName) && operation && typeof operation === 'object') {
entries.push([methodName, operation]);
}
});

return entries;
}

function setOperationEntry(pathItem, methodName, operation) {
if (isFixedOperationField(methodName)) {
pathItem[methodName.toLowerCase()] = operation;
return;
}

pathItem.additionalOperations = {
...pathItem.additionalOperations,
[methodName]: operation,
};
}

function methodSortIndex(methodName) {
const index = fixedOperationFields.indexOf(methodName.toLowerCase());
return index === -1 ? fixedOperationFields.length : index;
}

function compareMethods(methodA, methodB) {
const indexA = methodSortIndex(methodA);
const indexB = methodSortIndex(methodB);

return indexA === indexB ? methodA.localeCompare(methodB) : indexA - indexB;
}

export default async function ProcessSpec(
specUrl,
generateMissingTags = false,
Expand Down Expand Up @@ -202,19 +246,19 @@ function filterPaths(openApiObject, matchPaths = '', matchType = '', removeEndpo
Object.entries(openApiObject.paths).forEach(([pathsKey, methods]) => {
const filteredMethods = {};

Object.entries(methods).forEach(([httpMethod, methodDetails]) => {
getOperationEntries(methods).forEach(([httpMethod, methodDetails]) => {
const badges = methodDetails['x-badges'];

// Filter by matchPaths
if (pathMatches(pathsKey, httpMethod)) {
if (badges && Array.isArray(badges)) {
// Filter out based on removePathsWithBadgeLabeledAs
if (!containsLabelToRemove(badges)) {
filteredMethods[httpMethod] = methodDetails;
setOperationEntry(filteredMethods, httpMethod, methodDetails);
}
} else {
// No badges present, include the method
filteredMethods[httpMethod] = methodDetails;
setOperationEntry(filteredMethods, httpMethod, methodDetails);
}
}
});
Expand Down Expand Up @@ -316,7 +360,6 @@ function getComponents(openApiSpec, sortSchemas = false) {
}

function groupByTags(openApiSpec, sortEndpointsBy, generateMissingTags = false, sortTags = false) {
const supportedMethods = ['get', 'put', 'post', 'delete', 'patch', 'head', 'options']; // this is also used for ordering endpoints by methods
const tags = openApiSpec.tags && Array.isArray(openApiSpec.tags) && openApiSpec.tags.length > 0
? openApiSpec.tags.map((v) => ({
show: true,
Expand Down Expand Up @@ -345,112 +388,109 @@ function groupByTags(openApiSpec, sortEndpointsBy, generateMissingTags = false,
parameters: pathsAndWebhooks[pathOrHookName].parameters || [],
};
const isWebhook = pathsAndWebhooks[pathOrHookName]._type === 'webhook'; // eslint-disable-line no-underscore-dangle
supportedMethods.forEach((methodName) => {
if (pathsAndWebhooks[pathOrHookName][methodName]) {
const pathOrHookObj = openApiSpec.paths[pathOrHookName][methodName];
// If path.methods are tagged, else generate it from path
const pathTags = pathOrHookObj.tags || [];
if (pathTags.length === 0) {
if (generateMissingTags) {
const pathOrHookNameKey = pathOrHookName.replace(/^\/+|\/+$/g, '');
const firstWordEndIndex = pathOrHookNameKey.indexOf('/');
if (firstWordEndIndex === -1) {
pathTags.push(pathOrHookNameKey);
} else {
// firstWordEndIndex -= 1;
pathTags.push(pathOrHookNameKey.substring(0, firstWordEndIndex));
}
getOperationEntries(pathsAndWebhooks[pathOrHookName]).forEach(([methodName, pathOrHookObj]) => {
// If path.methods are tagged, else generate it from path
const pathTags = pathOrHookObj.tags || [];
if (pathTags.length === 0) {
if (generateMissingTags) {
const pathOrHookNameKey = pathOrHookName.replace(/^\/+|\/+$/g, '');
const firstWordEndIndex = pathOrHookNameKey.indexOf('/');
if (firstWordEndIndex === -1) {
pathTags.push(pathOrHookNameKey);
} else {
pathTags.push('General ⦂');
// firstWordEndIndex -= 1;
pathTags.push(pathOrHookNameKey.substring(0, firstWordEndIndex));
}
} else {
pathTags.push('General ⦂');
}
}

pathTags.forEach((tag) => {
let tagObj;
let specTagsItem;
pathTags.forEach((tag) => {
let tagObj;
let specTagsItem;

if (openApiSpec.tags) {
specTagsItem = openApiSpec.tags.find((v) => (v.name.toLowerCase() === tag.toLowerCase()));
}
if (openApiSpec.tags) {
specTagsItem = openApiSpec.tags.find((v) => (v.name.toLowerCase() === tag.toLowerCase()));
}

tagObj = tags.find((v) => v.name === tag);
if (!tagObj) {
tagObj = {
show: true,
elementId: `tag--${tag.replace(invalidCharsRegEx, '-')}`,
name: tag,
description: specTagsItem?.description || '',
headers: specTagsItem?.description ? getHeadersFromMarkdown(specTagsItem.description) : [],
paths: [],
expanded: (specTagsItem ? specTagsItem['x-tag-expanded'] !== false : true),
};
tags.push(tagObj);
}
tagObj = tags.find((v) => v.name === tag);
if (!tagObj) {
tagObj = {
show: true,
elementId: `tag--${tag.replace(invalidCharsRegEx, '-')}`,
name: tag,
description: specTagsItem?.description || '',
headers: specTagsItem?.description ? getHeadersFromMarkdown(specTagsItem.description) : [],
paths: [],
expanded: (specTagsItem ? specTagsItem['x-tag-expanded'] !== false : true),
};
tags.push(tagObj);
}

// Generate a short summary which is broken
let shortSummary = (pathOrHookObj.summary || pathOrHookObj.description || `${methodName.toUpperCase()} ${pathOrHookName}`).trim();
if (shortSummary.length > 100) {
[shortSummary] = shortSummary.split(/[.|!|?]\s|[\r?\n]/); // take the first line (period or carriage return)
}
// Merge Common Parameters with This methods parameters
let finalParameters = [];
if (commonParams) {
if (pathOrHookObj.parameters) {
finalParameters = commonParams.filter((commonParam) => {
if (!pathOrHookObj.parameters.some((param) => (commonParam.name === param.name && commonParam.in === param.in))) {
return commonParam;
}
}).concat(pathOrHookObj.parameters);
} else {
finalParameters = commonParams.slice(0);
}
// Generate a short summary which is broken
let shortSummary = (pathOrHookObj.summary || pathOrHookObj.description || `${methodName.toUpperCase()} ${pathOrHookName}`).trim();
if (shortSummary.length > 100) {
[shortSummary] = shortSummary.split(/[.|!|?]\s|[\r?\n]/); // take the first line (period or carriage return)
}
// Merge Common Parameters with This methods parameters
let finalParameters = [];
if (commonParams) {
if (pathOrHookObj.parameters) {
finalParameters = commonParams.filter((commonParam) => {
if (!pathOrHookObj.parameters.some((param) => (commonParam.name === param.name && commonParam.in === param.in))) {
return commonParam;
}
}).concat(pathOrHookObj.parameters);
} else {
finalParameters = pathOrHookObj.parameters ? pathOrHookObj.parameters.slice(0) : [];
finalParameters = commonParams.slice(0);
}
} else {
finalParameters = pathOrHookObj.parameters ? pathOrHookObj.parameters.slice(0) : [];
}

// Filter callbacks to contain only objects.
if (pathOrHookObj.callbacks) {
for (const [callbackName, callbackConfig] of Object.entries(pathOrHookObj.callbacks)) {
const filteredCallbacks = Object.entries(callbackConfig).filter((entry) => typeof entry[1] === 'object') || [];
pathOrHookObj.callbacks[callbackName] = Object.fromEntries(filteredCallbacks);
}
// Filter callbacks to contain only objects.
if (pathOrHookObj.callbacks) {
for (const [callbackName, callbackConfig] of Object.entries(pathOrHookObj.callbacks)) {
const filteredCallbacks = Object.entries(callbackConfig).filter((entry) => typeof entry[1] === 'object') || [];
pathOrHookObj.callbacks[callbackName] = Object.fromEntries(filteredCallbacks);
}
}

// Update Responses
tagObj.paths.push({
show: true,
expanded: false,
isWebhook,
expandedAtLeastOnce: false,
summary: (pathOrHookObj.summary || ''),
description: (pathOrHookObj.description || ''),
externalDocs: pathOrHookObj.externalDocs,
shortSummary,
method: methodName,
path: pathOrHookName,
operationId: pathOrHookObj.operationId,
elementId: `${methodName}-${pathOrHookName.replace(invalidCharsRegEx, '-')}`,
servers: pathOrHookObj.servers ? commonPathProp.servers.concat(pathOrHookObj.servers) : commonPathProp.servers,
parameters: finalParameters,
requestBody: pathOrHookObj.requestBody,
responses: pathOrHookObj.responses,
callbacks: pathOrHookObj.callbacks,
deprecated: pathOrHookObj.deprecated,
security: pathOrHookObj.security,
// commonSummary: commonPathProp.summary,
// commonDescription: commonPathProp.description,
xBadges: pathOrHookObj['x-badges'] || undefined,
xCodeSamples: pathOrHookObj['x-codeSamples'] || pathOrHookObj['x-code-samples'] || '',
});
});// End of tag path create
}
// Update Responses
tagObj.paths.push({
show: true,
expanded: false,
isWebhook,
expandedAtLeastOnce: false,
summary: (pathOrHookObj.summary || ''),
description: (pathOrHookObj.description || ''),
externalDocs: pathOrHookObj.externalDocs,
shortSummary,
method: methodName,
path: pathOrHookName,
operationId: pathOrHookObj.operationId,
elementId: `${methodName}-${pathOrHookName.replace(invalidCharsRegEx, '-')}`,
servers: pathOrHookObj.servers ? commonPathProp.servers.concat(pathOrHookObj.servers) : commonPathProp.servers,
parameters: finalParameters,
requestBody: pathOrHookObj.requestBody,
responses: pathOrHookObj.responses,
callbacks: pathOrHookObj.callbacks,
deprecated: pathOrHookObj.deprecated,
security: pathOrHookObj.security,
// commonSummary: commonPathProp.summary,
// commonDescription: commonPathProp.description,
xBadges: pathOrHookObj['x-badges'] || undefined,
xCodeSamples: pathOrHookObj['x-codeSamples'] || pathOrHookObj['x-code-samples'] || '',
});
});// End of tag path create
}); // End of Methods
}

const tagsWithSortedPaths = tags.filter((tag) => tag.paths && tag.paths.length > 0);
tagsWithSortedPaths.forEach((tag) => {
if (sortEndpointsBy === 'method') {
tag.paths.sort((a, b) => supportedMethods.indexOf(a.method).toString().localeCompare(supportedMethods.indexOf(b.method)));
tag.paths.sort((a, b) => compareMethods(a.method, b.method));
} else if (sortEndpointsBy === 'summary') {
tag.paths.sort((a, b) => (a.shortSummary).localeCompare(b.shortSummary));
} else if (sortEndpointsBy === 'path') {
Expand Down