UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

486 lines (473 loc) • 21.6 kB
/** * Renders path, query, header, and cookie parameter inputs, schema tree previews, constraints, and clickable example badges for <api-request>. */ import { html } from "lit"; import { unsafeHTML } from "lit/directives/unsafe-html.js"; import { live } from "lit/directives/live.js"; import { sanitizeHTML } from "../utils/sanitize.js"; import { marked } from "marked"; import { schemaToAST, getTypeInfo, normalizeExamples, getSchemaFromParam, generateExample, standardizeExample, anyExampleWithSummaryOrDescription, EMPTY_ARRAY, } from "~/utils/schema-utils"; import "~/components/schema-tree"; import "~/components/tag-input"; /** * A label followed by a wrapping list of values (Allowed values, Examples). * Wrapped lines hang under the first value; CSS draws the separators. */ export function valueListTemplate(label, items) { return html`<div class="value-list" part="value-list"> <span class="param-constraint-label value-list-label" part="value-list-label">${label}</span> <ul class="value-list-items" part="value-list-items" role="list"> ${items.map((item) => html`<li>${item}</li>`)} </ul> </div>`; } /** * Constraints as label/value pairs. The labels share one column, so every value starts at the same place, * whether there is one constraint or several. Limits (range, multiple of, length) and allowed values wrap * as value lists, hanging under their first value. */ export function constraintListTemplate(paramSchema, allowedItems, exampleItems) { const valueItems = (items) => html`<ul class="value-list-items" part="value-list-items" role="list"> ${items.map((item) => html`<li>${item}</li>`)} </ul>`; const row = (label, value) => html`<dt class="param-constraint-label" part="param-constraint-label">${label}</dt> <dd>${value}</dd>`; return html`<dl class="param-constraint" part="param-constraint"> ${paramSchema.default ? row("Default:", paramSchema.default) : ""} ${paramSchema.pattern ? row("Pattern:", paramSchema.pattern) : ""} ${ paramSchema.constrain ? row( "Limits:", valueItems( paramSchema.constrain .split("┃") .map((v) => v.trim()) .filter(Boolean), ), ) : "" } ${allowedItems ? row("Allowed:", valueItems(allowedItems)) : ""} ${exampleItems ? row("Examples:", valueItems(exampleItems)) : ""} </dl>`; } /** * Short examples join the constraint list (lined up with the other values) when there is no description to put them under. * Long-format examples, with a summary or description, always stay in the details. */ export function examplesBesideConstraints(description, hasConstraint, exampleList = []) { return ( !description?.trim() && Boolean(hasConstraint) && exampleList.length > 0 && !anyExampleWithSummaryOrDescription(exampleList) ); } export function renderExample(example, paramType, paramName) { return html` ${paramType === "array" ? "[" : ""} <button type="button" part="anchor anchor-param-example" class="value-chip" ?disabled="${this.allowTry !== "true"}" data-example-type="${paramType === "array" ? paramType : "string"}" data-example="${ example.value && Array.isArray(example.value) ? example.value?.join("~|~") : (typeof example.value === "object" ? JSON.stringify(example.value, null, 2) : example.value) || "" }" title="${ example.value && Array.isArray(example.value) ? example.value?.join("~|~") : (typeof example.value === "object" ? JSON.stringify(example.value, null, 2) : example.value) || "" }" @click="${(e) => { const inputEl = e.target .closest(".param-list") .querySelector(`[data-pname="${paramName}"]`); if (inputEl) { inputEl.value = e.target.dataset.exampleType === "array" ? e.target.dataset.example.split("~|~") : e.target.dataset.example; } }}" > ${example.printableValue || example.value} </button> ${paramType === "array" ? "] " : ""} `; } export function renderShortFormatExamples(examples, paramType, paramName) { return valueListTemplate( "Examples:", examples.map((x) => renderExample.call(this, x, paramType, paramName)), ); } export function renderLongFormatExamples(exampleList, paramType, paramName) { return html` <ul class="param-example-list" part="param-example-list" role="list"> ${exampleList.map( (v) => html`<li> ${renderExample.call(this, v, paramType, paramName)} ${v.summary?.length > 0 ? html`<span>&lpar;${v.summary}&rpar;</span>` : ""} ${v.description?.length > 0 ? html`<div class="m-markdown-small">${unsafeHTML(sanitizeHTML(marked(v.description)))}</div>` : ""} </li>`, )} </ul>`; } export function exampleListTemplate(paramName, paramType, exampleList = []) { return html` ${ exampleList.length > 0 ? anyExampleWithSummaryOrDescription(exampleList) ? html`<span class="param-constraint-label">Examples: </span> ${renderLongFormatExamples.call(this, exampleList, paramType, paramName)}` : renderShortFormatExamples.call(this, exampleList, paramType, paramName) : "" }`; } // A singular `example` is the value itself, even an object; only an `examples` map wraps its values in { value } const singleExample = (ex) => ex !== null && typeof ex === "object" && !Array.isArray(ex) ? { Example: { value: ex } } : standardizeExample(ex); export function inputParametersTemplate(paramType) { const filteredParams = this.parameters ? this.parameters.filter((param) => param.in === paramType) : []; if (filteredParams.length === 0) { return ""; } let title = ""; if (paramType === "path") { title = "Path parameters"; } else if (paramType === "query") { title = "Query-string parameters"; } else if (paramType === "header") { title = "Request headers"; } else if (paramType === "cookie") { title = "Cookies"; } const paramItems = []; for (const param of filteredParams) { const [declaredParamSchema, serializeStyle, mimeTypeElem] = getSchemaFromParam(param); if (!declaredParamSchema) { continue; } const paramSchema = getTypeInfo(declaredParamSchema); if (!paramSchema) { continue; } const schemaAsObj = schemaToAST(declaredParamSchema); let paramStyle = "form"; let paramExplode = true; let paramAllowReserved = false; if (paramType === "query" || paramType === "header" || paramType === "path") { if (param.style && "form spaceDelimited pipeDelimited".includes(param.style)) { paramStyle = param.style; } else if (serializeStyle) { paramStyle = serializeStyle; } if (typeof param.explode === "boolean") { paramExplode = param.explode; } if (typeof param.allowReserved === "boolean") { paramAllowReserved = param.allowReserved; } } // openapi 3.1.0 spec based examples (which must be Object(string : { value:any, summary?: string, description?: string}) const example = normalizeExamples( standardizeExample(param.examples) || singleExample(param.example) || singleExample(mimeTypeElem?.example) || standardizeExample(mimeTypeElem?.examples) || standardizeExample(paramSchema.examples) || standardizeExample(paramSchema.example), paramSchema.type, ); if ( !example.exampleVal && (paramSchema.type === "object" || paramSchema.type.split("┃").includes("object")) ) { example.exampleVal = generateExample( declaredParamSchema, serializeStyle || "json", {}, {}, this.callback === "true" || this.webhook === "true" ? true : false, this.callback === "true" || this.webhook === "true" ? false : true, "text", false, )[0]?.exampleValue || ""; } const hasConstraint = paramSchema.default || paramSchema.constrain || paramSchema.allowedValues || paramSchema.pattern; const inlineExamples = examplesBesideConstraints( param.description, hasConstraint, example.exampleList, ); paramItems.push(html` <li class="param ${param.deprecated ? "deprecated" : ""}" part="param" title="${param.deprecated ? "Deprecated" : ""}"> <div class="param-label" part="param-label"> <div class="param-name ${param.deprecated ? "deprecated" : ""}" part="param-name"> ${ param.deprecated ? html`<svg class="param-deprecated-icon" viewBox="0 0 10 10" width="10" height="10" aria-hidden="true"> <path d="M2 2L8 8M2 8L8 2" /> </svg>` : "" } ${param.required ? html`<span class="param-required" title="Required">*</span>` : ""} ${param.name} </div> <div class="param-type" part="param-type"> ${ paramSchema.type === "array" || paramSchema.type.split("┃").includes("array") ? `${paramSchema.arrayType || paramSchema.type}` : `${paramSchema.format && !paramSchema.type.includes("┃") ? paramSchema.format : paramSchema.contentMediaType && !paramSchema.type.includes("┃") ? paramSchema.contentMediaType : paramSchema.type}` } </div> </div> ${ this.allowTry === "true" ? html` <div class="param-input" part="param-input"> ${ paramSchema.type === "array" || (paramSchema.type.split("┃").includes("array") && !paramSchema.type.split("┃").includes("object")) ? html`<tag-input class="request-param" id="tag-input-request-param-${param.name}" style="width:100%" data-ptype="${paramType}" data-pname="${param.name}" data-item-type="${declaredParamSchema.items?.type || ""}" data-example="${Array.isArray(example.exampleVal) ? example.exampleVal.join("~|~") : example.exampleVal}" data-param-serialize-style="${paramStyle}" data-param-serialize-explode="${paramExplode}" data-param-allow-reserved="${paramAllowReserved}" data-x-fill-example="${param["x-fill-example"] || "yes"}" data-array="true" placeholder="add-multiple &#x21a9;" .value="${ param["x-fill-example"] === "no" ? EMPTY_ARRAY : live( this.fillRequestFieldsWithExample === "true" ? Array.isArray(example.exampleVal) ? example.exampleVal : [example.exampleVal] : EMPTY_ARRAY, ) }" @input="${(e) => { const requestPanelEl = this.getRequestPanel(e); this.liveCURLSyntaxUpdate(requestPanelEl); }}" @change="${(e) => { const requestPanelEl = this.getRequestPanel(e); this.liveCURLSyntaxUpdate(requestPanelEl); }}" > </tag-input>` : paramSchema.type === "object" || paramSchema.type.split("┃").includes("object") ? html`<div part="tab-panel" class="tab-panel col"> <div part="tab-btn-row" class="tab-buttons row" @click="${(e) => { if (e.target.tagName.toLowerCase() === "button") { const newState = { ...this.activeParameterSchemaTabs }; newState[param.name] = e.target.dataset.tab; this.activeParameterSchemaTabs = newState; } }}" > <button part="tab-btn" class="tab-btn ${this.activeParameterSchemaTabs[param.name] === "example" ? "active" : ""}" data-tab="example" > EXAMPLE </button> <button part="tab-btn" class="tab-btn ${this.activeParameterSchemaTabs[param.name] !== "example" ? "active" : ""}" data-tab="schema" > SCHEMA </button> </div> ${html`<div part="tab-content" class="tab-content col" data-tab="example" style="display:${ this.activeParameterSchemaTabs[param.name] === "example" ? "block" : "none" }; padding-left:5px; width:100%" > <textarea id="textarea-request-param-${param.name}" class="textarea request-param" part="textarea textarea-param" data-ptype="${paramType}-object" data-pname="${param.name}" data-example="${ typeof example.exampleVal === "object" ? JSON.stringify(example.exampleVal, null, 2) : example.exampleVal }" data-param-serialize-style="${paramStyle}" data-param-serialize-explode="${paramExplode}" data-param-allow-reserved="${paramAllowReserved}" data-x-fill-example="${param["x-fill-example"] || "yes"}" spellcheck="false" .value="${ param["x-fill-example"] === "no" ? "" : live( this.fillRequestFieldsWithExample === "true" ? typeof example.exampleVal === "object" ? JSON.stringify(example.exampleVal, null, 2) : example.exampleVal : "", ) }" style="resize:vertical; width:100%; height: ${"read focused".includes(this.renderStyle) ? "180px" : "120px"};" @input=${(e) => { const requestPanelEl = this.getRequestPanel(e); this.liveCURLSyntaxUpdate(requestPanelEl); }} ></textarea> </div>`} ${html`<div part="tab-content" class="tab-content col" data-tab="schema" style="display:${ this.activeParameterSchemaTabs[param.name] !== "example" ? "block" : "none" }; padding-left:5px; width:100%;" > <schema-tree class="json" style="display: block" .data="${schemaAsObj}" schema-expand-level="${this.schemaExpandLevel}" schema-description-expanded="${this.schemaDescriptionExpanded}" allow-schema-description-expand-toggle="${this.allowSchemaDescriptionExpandToggle}" schema-hide-read-only="${this.schemaHideReadOnly.includes(this.method)}" schema-hide-write-only="${this.schemaHideWriteOnly.includes(this.method)}" exportparts="wrap-request-btn:wrap-request-btn, btn:btn, btn-fill:btn-fill, btn-outline:btn-outline, btn-try:btn-try, btn-clear:btn-clear, btn-fill-example:btn-fill-example, btn-clear-resp:btn-clear-resp, file-input:file-input, textbox:textbox, textbox-param:textbox-param, textarea:textarea, textarea-param:textarea-param, anchor:anchor, anchor-param-example:anchor-param-example" > </schema-tree> </div>`} </div>` : html` <input type="${paramSchema.format === "password" ? "password" : "text"}" spellcheck="false" style="width:100%" id="input-request-param-${param.name}" class="request-param" part="textbox textbox-param" data-ptype="${paramType}" data-pname="${param.name}" data-example="${Array.isArray(example.exampleVal) ? example.exampleVal.join("~|~") : example.exampleVal}" data-param-allow-reserved="${paramAllowReserved}" data-x-fill-example="${param["x-fill-example"] || "yes"}" data-array="false" .value="${ param["x-fill-example"] === "no" ? "" : live( this.fillRequestFieldsWithExample === "true" ? example.exampleVal : "", ) }" @input=${(e) => { const requestPanelEl = this.getRequestPanel(e); this.liveCURLSyntaxUpdate(requestPanelEl); }} />` } </div>` : "" } ${ hasConstraint ? constraintListTemplate( paramSchema, paramSchema.allowedValues && String(paramSchema.allowedValues) .split("┃") .map( (v) => html`<button type="button" part="anchor anchor-param-constraint" class="value-chip" ?disabled="${this.allowTry !== "true"}" data-type="${paramSchema.type === "array" ? paramSchema.type : "string"}" data-enum="${v.trim()}" @click="${(e) => { const inputEl = e.target .closest(".param-list") .querySelector(`[data-pname="${param.name}"]`); if (inputEl) { if (e.target.dataset.type === "array") { inputEl.value = [e.target.dataset.enum]; } else { inputEl.value = e.target.dataset.enum; } } }}" > ${v} </button>`, ), inlineExamples && example.exampleList.map((x) => renderExample.call(this, x, paramSchema.type, param.name), ), ) : "" } <div class="param-details" part="param-details"> ${param.description ? html`<div class="m-markdown-small">${unsafeHTML(sanitizeHTML(marked(param.description)))}</div>` : ""} ${inlineExamples ? "" : exampleListTemplate.call(this, param.name, paramSchema.type, example.exampleList)} </div> </li> `); } return html` <h5 class="rapidoc-heading label-heading table-title top-gap" part="heading heading-5">${title}</h5> <div class="param-list-container"> <ul class="param-list" part="param-list" role="list"> ${paramItems} </ul> </div>`; }