diff --git a/src/json-schema-viewer.js b/src/json-schema-viewer.js index 3e5b7cdc..7edabfa1 100644 --- a/src/json-schema-viewer.js +++ b/src/json-schema-viewer.js @@ -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 diff --git a/src/styles/endpoint-styles.js b/src/styles/endpoint-styles.js index 255d5296..9a6df9f5 100644 --- a/src/styles/endpoint-styles.js +++ b/src/styles/endpoint-styles.js @@ -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); @@ -78,6 +83,7 @@ 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); } @@ -85,7 +91,9 @@ export default css` .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); } @@ -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); } @@ -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); } @@ -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 { diff --git a/src/styles/font-styles.js b/src/styles/font-styles.js index 8eddcdd0..ff0df87b 100644 --- a/src/styles/font-styles.js +++ b/src/styles/font-styles.js @@ -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); } diff --git a/src/utils/spec-parser.js b/src/utils/spec-parser.js index 8b6d1c7d..c074ca98 100644 --- a/src/utils/spec-parser.js +++ b/src/utils/spec-parser.js @@ -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, @@ -202,7 +246,7 @@ 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 @@ -210,11 +254,11 @@ function filterPaths(openApiObject, matchPaths = '', matchType = '', removeEndpo 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); } } }); @@ -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, @@ -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') {