UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

604 lines (560 loc) • 20.6 kB
import { css, LitElement } from "lit"; import { customElement, property } from "lit/decorators.js"; import { scheduleHighlight } from "~/utils/highlighter"; // Styles import FontStyles from "~/styles/font-styles"; import InputStyles from "~/styles/input-styles"; import FlexStyles from "~/styles/flex-styles"; import AuthStyles from "~/styles/auth-styles"; import ParamListStyles from "~/styles/param-list-styles"; import MicrolighterStyles from "~/styles/microlighter-styles"; import TabStyles from "~/styles/tab-styles"; import NavStyles from "~/styles/nav-styles"; import InfoStyles from "~/styles/info-styles"; import EndpointStyles from "~/styles/endpoint-styles"; import ThemeTokens, { DEFAULT_THEME, applyCustomTheme, isValidHexColor, normalizeTheme, } from "~/styles/theme-tokens"; import { rapidocApiKey } from "~/utils/common-utils"; import ProcessSpec from "~/utils/spec-parser"; import { enableMockServer, disableMockServer, updateMockConfig } from "~/utils/mock-interceptor"; import mainBodyTemplate from "~/templates/main-body-template"; import { applyApiKey, onClearAllApiKeys } from "~/templates/security-scheme-template"; import { setApiServer } from "~/templates/server-template"; import "~/utils/markdown-utils"; import { AutoColorSchemeController } from "~/utils/auto-color-scheme"; const paramsConverter = { fromAttribute: (attr: string | null) => { if (!attr) { return null; } const trimmed = attr.trim(); if (trimmed.startsWith("{") && trimmed.endsWith("}")) { try { return JSON.parse(trimmed); } catch { return trimmed; } } return trimmed; }, toAttribute: (prop: any) => { if (typeof prop === "object" && prop !== null) { return JSON.stringify(prop); } return prop; }, }; @customElement("rapi-doc-mini") export default class RapiDocMini extends LitElement { // Spec @property({ type: String, attribute: "spec-url" }) specUrl = ""; @property({ type: String, attribute: "sort-endpoints-by" }) sortEndpointsBy = "path"; // UI Layouts @property({ type: String }) layout = "row"; @property({ type: String, attribute: "paths-expanded" }) pathsExpanded: any = "false"; @property({ type: String, attribute: "default-schema-tab" }) defaultSchemaTab = "example"; @property({ type: String, attribute: "response-area-height" }) responseAreaHeight = "300px"; @property({ type: String, attribute: "show-summary-when-collapsed" }) showSummaryWhenCollapsed = "true"; @property({ type: String, attribute: "fill-request-fields-with-example" }) fillRequestFieldsWithExample = "true"; @property({ type: String, attribute: "persist-auth" }) persistAuth = "false"; // Schema Styles @property({ type: String, attribute: "schema-style" }) schemaStyle = "tree"; @property({ type: Number, attribute: "schema-expand-level" }) schemaExpandLevel = 99999; @property({ type: String, attribute: "schema-description-expanded" }) schemaDescriptionExpanded = "false"; // API Server @property({ type: String, attribute: "api-key-name" }) apiKeyName = ""; @property({ type: String, attribute: "api-key-location" }) apiKeyLocation = "header"; @property({ type: String, attribute: "api-key-value" }) apiKeyValue = "-"; @property({ type: String, attribute: "default-api-server" }) defaultApiServerUrl = ""; @property({ type: String, attribute: "server-url" }) serverUrl = ""; @property({ type: String, attribute: "oauth-receiver" }) oauthReceiver = "oauth-receiver.html"; @property({ type: Object, attribute: "additional-authorize-params", converter: paramsConverter }) additionalAuthorizeParams: any; @property({ type: Object, attribute: "additional-token-params", converter: paramsConverter }) additionalTokenParams: any; // Mock Server @property({ type: String, attribute: "mock-server" }) mockServer = "false"; // A string of codes for every operation, or a map by operation (`{ "POST /orders": "503, 200", "*": "200" }`) @property({ type: String, attribute: "mock-server-status-code" }) mockServerStatusCode: | string | Record<string, string> = "200"; @property({ type: String, attribute: "mock-server-status-strategy" }) mockServerStatusStrategy = "example"; @property({ type: Number, attribute: "mock-server-delay" }) mockServerDelay = 0; @property({ type: String, attribute: "mock-server-log" }) mockServerLog = "false"; @property({ type: String, attribute: "allow-try" }) allowTry = "true"; @property({ type: String, attribute: "show-curl-before-try" }) showCurlBeforeTry = "false"; // Main Colors and Font @property({ type: String, reflect: true }) theme = DEFAULT_THEME; @property({ type: String, attribute: "primary-color" }) primaryColor = ""; @property({ type: String, attribute: "color-scheme", reflect: true }) colorScheme = "dark"; // color-scheme="auto": follows the page's theme (see utils/auto-color-scheme.js) _autoColorScheme = new AutoColorSchemeController(this); @property({ type: String, reflect: true }) radius = "sm"; @property({ type: String, reflect: true }) scale = "default"; // Fetch Options @property({ type: String, attribute: "fetch-credentials" }) fetchCredentials = ""; // Filters @property({ type: String, attribute: "match-paths" }) matchPaths = ""; @property({ type: String, attribute: "match-type" }) matchType = "includes"; @property({ type: String, attribute: "remove-endpoints-with-badge-label-as" }) removeEndpointsWithBadgeLabelAs = ""; // Internal Properties @property({ type: Boolean }) loading = false; isMini: boolean; _specLoadSeq: number; updateRoute: string; renderStyle: string; showHeader: string; allowAdvancedSearch: string; resolvedSpec: any; selectedServer: any; loadFailed: boolean; sortTags: any; generateMissingTags: any; sortSchemas: any; allowSchemaDescriptionExpandToggle: any; constructor() { super(); this.isMini = true; // Spec loads carry a token; only the latest load may apply its result (see loadSpec) this._specLoadSeq = 0; this.updateRoute = "false"; this.renderStyle = "view"; this.showHeader = "false"; this.allowAdvancedSearch = "false"; this.loadFailed = false; } static get styles() { return [ ThemeTokens, FontStyles, InputStyles, FlexStyles, AuthStyles, ParamListStyles, EndpointStyles, MicrolighterStyles, TabStyles, NavStyles, InfoStyles, css` :host { all: initial; /* Same crisp text rendering as <rapi-doc> (see layout-styles.js) */ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; display: flex; flex-direction: column; min-width: 360px; width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; letter-spacing: normal; color: var(--foreground); background: var(--background); font-family: var(--font-sans); container-type: inline-size; } @container (min-width: 768px) { .only-large-screen { display: block; } .only-large-screen-flex { display: flex; } } `, ]; } // Startup connectedCallback() { super.connectedCallback(); if ( !this.showSummaryWhenCollapsed || !"true, false,".includes(`${this.showSummaryWhenCollapsed},`) ) { this.showSummaryWhenCollapsed = "true"; } if (!this.layout || !"row, column,".includes(`${this.layout},`)) { this.layout = "row"; } if (!this.schemaStyle || !"tree, table,".includes(`${this.schemaStyle},`)) { this.schemaStyle = "tree"; } if (this.primaryColor && isValidHexColor(this.primaryColor)) { this.theme = this.primaryColor; } const normalizedTheme = normalizeTheme(this.theme); this.theme = normalizedTheme.theme; this.setAttribute("theme", this.theme); if (!this.colorScheme || !"dark, light, system,".includes(`${this.colorScheme},`)) { this.colorScheme = "dark"; } if (!this.radius || !"none, sm, md, lg, full,".includes(`${this.radius},`)) { this.radius = "sm"; } if (!this.scale || !"sm, default, lg,".includes(`${this.scale},`)) { this.scale = "default"; } if (!this.getAttribute("theme")) { this.setAttribute("theme", this.theme); } if (!this.getAttribute("color-scheme")) { this.setAttribute("color-scheme", this.colorScheme); } if (!this.getAttribute("radius")) { this.setAttribute("radius", this.radius); } if (!this.getAttribute("scale")) { this.setAttribute("scale", this.scale); } applyCustomTheme(this, this.theme); if ( !this.defaultSchemaTab || !"example, schema, model,".includes(`${this.defaultSchemaTab},`) ) { this.defaultSchemaTab = "example"; } else if (this.defaultSchemaTab === "model") { this.defaultSchemaTab = "schema"; } this.pathsExpanded = this.pathsExpanded === "true"; if (!this.schemaExpandLevel || this.schemaExpandLevel < 1) { this.schemaExpandLevel = 99999; } if ( !this.schemaDescriptionExpanded || !"true, false,".includes(`${this.schemaDescriptionExpanded},`) ) { this.schemaDescriptionExpanded = "false"; } if ( !this.fillRequestFieldsWithExample || !"true, false,".includes(`${this.fillRequestFieldsWithExample},`) ) { this.fillRequestFieldsWithExample = "true"; } if (!this.persistAuth || !"true, false,".includes(`${this.persistAuth},`)) { this.persistAuth = "false"; } if (!this.responseAreaHeight) { this.responseAreaHeight = "300px"; } if (!this.allowTry || !"true, false,".includes(`${this.allowTry},`)) { this.allowTry = "true"; } if (!this.apiKeyValue) { this.apiKeyValue = "-"; } if (!this.apiKeyLocation) { this.apiKeyLocation = "header"; } if (!this.apiKeyName) { this.apiKeyName = ""; } if (!this.oauthReceiver) { this.oauthReceiver = "oauth-receiver.html"; } if (!this.sortTags || !"true, false,".includes(`${this.sortTags},`)) { this.sortTags = "false"; } if (!this.sortEndpointsBy || !"method, path, summary,".includes(`${this.sortEndpointsBy},`)) { this.sortEndpointsBy = "path"; } if (!this.matchType || !"includes regex".includes(this.matchType)) { this.matchType = "includes"; } if (!this.matchPaths) { this.matchPaths = ""; } if (!this.removeEndpointsWithBadgeLabelAs) { this.removeEndpointsWithBadgeLabelAs = ""; } if ( !this.allowSchemaDescriptionExpandToggle || !"true, false,".includes(`${this.allowSchemaDescriptionExpandToggle},`) ) { this.allowSchemaDescriptionExpandToggle = "true"; } if ( !this.fetchCredentials || !"omit, same-origin, include,".includes(`${this.fetchCredentials},`) ) { this.fetchCredentials = ""; } } disconnectedCallback() { if (this.mockServer === "true") { disableMockServer(this); } super.disconnectedCallback(); } render() { return mainBodyTemplate.call(this, true, this.pathsExpanded); } get config() { return { renderStyle: this.renderStyle, schemaStyle: this.schemaStyle, defaultSchemaTab: this.defaultSchemaTab, schemaExpandLevel: this.schemaExpandLevel, schemaDescriptionExpanded: this.schemaDescriptionExpanded, allowSchemaDescriptionExpandToggle: this.allowSchemaDescriptionExpandToggle, schemaHideReadOnly: this.schemaHideReadOnly, schemaHideWriteOnly: this.schemaHideWriteOnly, fillRequestFieldsWithExample: this.fillRequestFieldsWithExample, allowTry: this.allowTry, showCurlBeforeTry: this.showCurlBeforeTry, fetchCredentials: this.fetchCredentials, }; } updated(changedProperties) { super.updated?.(changedProperties); scheduleHighlight(this.shadowRoot); if (changedProperties.has("theme")) { const normalizedTheme = normalizeTheme(this.theme); if (this.theme !== normalizedTheme.theme) { this.theme = normalizedTheme.theme; this.setAttribute("theme", this.theme); } applyCustomTheme(this, this.theme); } if ( changedProperties.has("primaryColor") && this.primaryColor && isValidHexColor(this.primaryColor) ) { this.theme = normalizeTheme(this.primaryColor).theme; this.setAttribute("theme", this.theme); applyCustomTheme(this, this.theme); } if (changedProperties.has("mockServer")) { if (this.mockServer === "true" && this.resolvedSpec) { enableMockServer(this.resolvedSpec, { owner: this, statusCode: this.mockServerStatusCode, statusStrategy: this.mockServerStatusStrategy, delay: this.mockServerDelay, log: this.mockServerLog, }); } else if (this.mockServer !== "true") { disableMockServer(this); } } if ( changedProperties.has("mockServerStatusCode") || changedProperties.has("mockServerStatusStrategy") || changedProperties.has("mockServerDelay") || changedProperties.has("mockServerLog") ) { updateMockConfig({ owner: this, statusCode: this.mockServerStatusCode, statusStrategy: this.mockServerStatusStrategy, delay: this.mockServerDelay, log: this.mockServerLog, }); } } attributeChangedCallback(name, oldVal, newVal) { if (name === "spec-url") { if (oldVal !== newVal) { // put it at the end of event-loop to load all the attributes window.setTimeout(async () => { await this.loadSpec(newVal); }, 0); } } if ( name === "match-paths" || name === "match-type" || name === "remove-endpoints-with-badge-label-as" ) { if (oldVal !== newVal) { window.setTimeout(async () => { await this.loadSpec(this.specUrl); }, 0); } } if (name === "api-key-name" || name === "api-key-location" || name === "api-key-value") { let updateSelectedApiKey = false; let apiKeyName = ""; let apiKeyLocation = ""; let apiKeyValue = ""; if (name === "api-key-name") { if (this.getAttribute("api-key-location") && this.getAttribute("api-key-value")) { apiKeyName = newVal; apiKeyLocation = this.getAttribute("api-key-location"); apiKeyValue = this.getAttribute("api-key-value"); updateSelectedApiKey = true; } } else if (name === "api-key-location") { if (this.getAttribute("api-key-name") && this.getAttribute("api-key-value")) { apiKeyLocation = newVal; apiKeyName = this.getAttribute("api-key-name"); apiKeyValue = this.getAttribute("api-key-value"); updateSelectedApiKey = true; } } else if (name === "api-key-value") { if (this.getAttribute("api-key-name") && this.getAttribute("api-key-location")) { apiKeyValue = newVal; apiKeyLocation = this.getAttribute("api-key-location"); apiKeyName = this.getAttribute("api-key-name"); updateSelectedApiKey = true; } } if (updateSelectedApiKey) { // While the spec is loading, resolvedSpec is a placeholder without securitySchemes if (this.resolvedSpec?.securitySchemes) { const rapiDocApiKey = this.resolvedSpec.securitySchemes.find( (v: any) => v.securitySchemeId === rapidocApiKey, ); if (!rapiDocApiKey) { this.resolvedSpec.securitySchemes.push({ securitySchemeId: rapidocApiKey, description: "api-key provided in rapidoc element attributes", type: "apiKey", name: apiKeyName, in: apiKeyLocation, value: apiKeyValue, finalKeyValue: apiKeyValue, }); } else { rapiDocApiKey.name = apiKeyName; rapiDocApiKey.in = apiKeyLocation; rapiDocApiKey.value = apiKeyValue; rapiDocApiKey.finalKeyValue = apiKeyValue; } this.requestUpdate(); } } } super.attributeChangedCallback(name, oldVal, newVal); } onSpecUrlChange() { this.setAttribute("spec-url", (this.shadowRoot.getElementById("spec-url") as any).value); } // Public Method async loadSpec(specUrl) { if (!specUrl) { return; } // Each load gets a token; a load whose token is no longer current never applies its result const token = ++this._specLoadSeq; try { this.resolvedSpec = { specLoadError: false, isSpecLoading: true, tags: [], }; this.loading = true; this.loadFailed = false; this.requestUpdate(); const spec = await ProcessSpec.call( this, specUrl, this.generateMissingTags === "true", this.sortTags === "true", this.sortSchemas === "true", this.getAttribute("sort-endpoints-by"), this.getAttribute("api-key-name"), this.getAttribute("api-key-location"), this.getAttribute("api-key-value"), this.getAttribute("server-url"), this.matchPaths, this.matchType, this.removeEndpointsWithBadgeLabelAs, ); if (token !== this._specLoadSeq) { return; // a newer load started while this one was parsing } this.loading = false; this.afterSpecParsedAndValidated(spec, token); } catch (err: any) { if (token !== this._specLoadSeq) { return; } this.loading = false; this.loadFailed = true; this.resolvedSpec = null; console.error(`RapiDoc: Unable to resolve the API spec.. ${err.message}`); } } // Public Method - to update security-scheme of type http setHttpUserNameAndPassword(securitySchemeId, username, password) { return applyApiKey.call(this, securitySchemeId, username, password); } // Public Method - to update security-scheme of type apiKey or OAuth. // options.note: where the key came from (e.g. "Using your sign-in"), shown in place of the scheme's form setApiKey(securitySchemeId, apiKeyValue, options: { note?: string } = {}) { return applyApiKey.call(this, securitySchemeId, "", "", apiKeyValue, options?.note || ""); } // Public Method removeAllSecurityKeys() { return onClearAllApiKeys.call(this); } // Public Method setApiServer(apiServerUrl) { // return apiServerUrl; return setApiServer.call(this, apiServerUrl); } async afterSpecParsedAndValidated(spec, token = this._specLoadSeq) { this.resolvedSpec = spec; this.selectedServer = undefined; if (this.defaultApiServerUrl) { if (this.defaultApiServerUrl === this.serverUrl) { this.selectedServer = { url: this.serverUrl, computedUrl: this.serverUrl, }; } else if (this.resolvedSpec.servers) { this.selectedServer = this.resolvedSpec.servers.find( (v: any) => v.url === this.defaultApiServerUrl, ); } } if (!this.selectedServer) { if (this.resolvedSpec.servers) { this.selectedServer = this.resolvedSpec.servers[0]; } } this.requestUpdate(); while (!(await this.updateComplete)); if (token !== this._specLoadSeq) { return; // a newer load took over while this one was rendering; it fires its own spec-loaded } const specLoadedEvent = new CustomEvent("spec-loaded", { detail: spec }); this.dispatchEvent(specLoadedEvent); if (this.mockServer === "true") { enableMockServer(this.resolvedSpec, { owner: this, statusCode: this.mockServerStatusCode, statusStrategy: this.mockServerStatusStrategy, delay: this.mockServerDelay, log: this.mockServerLog, }); } } // Called by anchor tags created using markdown handleHref(e) { if (e.target.tagName.toLowerCase() === "a") { if (e.target.getAttribute("href").startsWith("#")) { const gotoEl = this.shadowRoot.getElementById( e.target.getAttribute("href").replace("#", ""), ); if (gotoEl) { gotoEl.scrollIntoView({ behavior: "auto", block: "start" }); } } } } }