rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
604 lines (560 loc) • 20.6 kB
text/typescript
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;
},
};
export default class RapiDocMini extends LitElement {
// Spec
specUrl = "";
sortEndpointsBy = "path";
// UI Layouts
layout = "row";
pathsExpanded: any = "false";
defaultSchemaTab = "example";
responseAreaHeight = "300px";
showSummaryWhenCollapsed =
"true";
fillRequestFieldsWithExample = "true";
persistAuth = "false";
// Schema Styles
schemaStyle = "tree";
schemaExpandLevel = 99999;
schemaDescriptionExpanded =
"false";
// API Server
apiKeyName = "";
apiKeyLocation = "header";
apiKeyValue = "-";
defaultApiServerUrl = "";
serverUrl = "";
oauthReceiver = "oauth-receiver.html";
additionalAuthorizeParams: any;
additionalTokenParams: any;
// Mock Server
mockServer = "false";
// A string of codes for every operation, or a map by operation (`{ "POST /orders": "503, 200", "*": "200" }`)
mockServerStatusCode:
| string
| Record<string, string> = "200";
mockServerStatusStrategy =
"example";
mockServerDelay = 0;
mockServerLog = "false";
allowTry = "true";
showCurlBeforeTry = "false";
// Main Colors and Font
theme = DEFAULT_THEME;
primaryColor = "";
colorScheme = "dark";
// color-scheme="auto": follows the page's theme (see utils/auto-color-scheme.js)
_autoColorScheme = new AutoColorSchemeController(this);
radius = "sm";
scale = "default";
// Fetch Options
fetchCredentials = "";
// Filters
matchPaths = "";
matchType = "includes";
removeEndpointsWithBadgeLabelAs = "";
// Internal Properties
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;
}
(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" });
}
}
}
}
}