rapidoc
Version:
RapiDoc - Open API spec viewer with built in console
632 lines (584 loc) • 24.3 kB
JavaScript
import { LitElement, html } from 'lit-element';
import JsonTree from '@/components/json-tree';
import SchemaTree from '@/components/schema-tree';
import TagInput from '@/components/tag-input';
import vars from '@/styles/vars';
import TableStyles from '@/styles/table-styles';
import FlexStyles from '@/styles/flex-styles';
import InputStyles from '@/styles/input-styles';
import FontStyles from '@/styles/font-styles';
import CommonStyles from '@/styles/common-styles';
import { schemaToModel, getTypeInfo, generateExample, removeCircularReferences} from '@/utils/common-utils';
import marked from 'marked';
import {unsafeHTML} from 'lit-html/directives/unsafe-html.js';
export default class ApiRequest extends LitElement {
render() {
return html`
${TableStyles}
${InputStyles}
${FontStyles}
${FlexStyles}
${CommonStyles}
<style>
.title{
font-family:var(--font-regular);
font-size:var(--title-font-size);
font-weight:bold;
margin-bottom:16px;
}
.param-name,
.param-type{
margin: 1px 0;
text-align: right;
line-height: 12px;
}
.param-name{
color: var(--fg);
font-family: var(--font-mono);
}
.param-type{
color: var(--light-fg);
font-family: var(--font-regular);
}
.param-constraint{
min-width:100px;
}
.param-constraint:empty{
display:none;
}
.top-gap{margin-top:24px;}
.tab-buttons{
height:30px;
border-bottom: 1px solid var(--light-border-color) ;
align-items: stretch;
}
.tab-btn{
border:none;
background-color:transparent;
cursor:pointer;
outline:none;
font-size:12px;
margin-right:16px;
padding:1px;
}
.tab-btn.active{
border-bottom: 3px solid var(--primary-color);
font-weight:bold;
color:var(--primary-color);
}
.tab-btn:hover{
color:var(--primary-color);
}
.tab-content{
margin:-1px 0 0 0;
}
.link{
font-size:12px;
text-decoration: underline;
color:var(--link-color);
font-family:var(--font-mono);
margin-bottom:2px;
}
.textarea {
min-height:180px;
padding:5px;
}
.response-message.error{
color:var(--error-color);
font-weight:bold;
text-overflow: ellipsis;
}
.response-message.success{
color:var(--success-color);
font-weight:bold;
text-overflow: ellipsis;
}
@media only screen and (min-width: 768px){
.textarea {
padding:16px;
}
}
</style>
<div class="col regular-font request-panel">
<div class="title">REQUEST</div>
${this.inputParametersTemplate('path')}
${this.inputParametersTemplate('query')}
${this.requestBodyTemplate()}
${this.inputParametersTemplate('header')}
${this.inputParametersTemplate('cookie')}
${this.allowTry==='false'?'':html`${this.apiCallTemplate()}`}
</div>
`
}
constructor() {
super();
this.responseMessage = '';
this.responseStatus = 'success';
this.responseHeaders = '';
this.responseText = '';
this.responseUrl = '';
this.curlSyntax = '';
}
static get properties() {
return {
server : { type: String },
apiKeyName : { type: String, attribute: 'api-key-name' },
apiKeyValue : { type: String, attribute: 'api-key-value' },
apiKeyLocation: { type: String, attribute: 'api-key-location' },
method : { type: String },
path : { type: String },
parameters : { type: Array },
request_body : { type: Object },
parser : { type: Object },
responseMessage: { type: String, attribute:false },
responseText : { type: String, attribute:false },
responseHeaders: { type: String, attribute:false },
responseStatus : { type: String, attribute:false },
responseUrl : { type: String, attribute:false },
allowTry : { type: String, attribute: 'allow-try' },
};
}
inputParametersTemplate(paramType){
let title ="";
let filteredParams= this.parameters? this.parameters.filter(param => param.in === paramType):[];
if (filteredParams.length == 0 ){
return "";
}
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 tableRows = [];
for (const param of filteredParams) {
if (!param.schema){
continue;
}
let paramSchema = getTypeInfo(param.schema);
let inputVal='';
if (param.example=='0'){
inputVal='0'
}
else{
inputVal = paramSchema.default;
}
tableRows.push(html`
<tr>
<td style="min-width:100px;">
<div class="param-name">
${param.required?html`<span style='color:orangered'>*</span>`:``}${param.name}
</div>
<div class="param-type">
${paramSchema.type==='array' ? `${paramSchema.arrayType}`
:`${paramSchema.type}${paramSchema.format?`\u00a0(${paramSchema.format})`:''}`}
</div>
</td>
<td style="min-width:100px;">
${paramSchema.type === 'array'?html`
<tag-input class="request-param" style="width:100%;font-size:13px;background:var(--input-bg);line-height:13px;"
data-ptype="${paramType}"
data-pname="${param.name}"
data-array="true"
placeholder="add-multiple\u23ce"
></tag-input>`
:html`<input type="text" style="width:100%" class="request-param"
data-pname="${param.name}"
data-ptype="${paramType}"
data-array="false"
value="${inputVal}">`
}
</td>
<td>
<div class="param-constraint">
${paramSchema.constrain?html`${paramSchema.constrain}<br/>`:``}
${paramSchema.allowedValues?html`${paramSchema.allowedValues}`:``}
</div>
</td>
</tr>
${param.description?html`
<tr>
<td style="border:none">
</td>
<td colspan="2" style="border:none; margin-top:0; padding:0 5px;">
<span class="m-markdown-small">${unsafeHTML(marked(param.description))}</span>
</td>
</tr>`
:``}
`)
}
return html`
<div class="table-title top-gap">${title}</div>
<div style="display:block; overflow-x:auto; max-width:100%;">
<table class="m-table" style="width:100%; word-break:break-word;;">
${tableRows}
</table>
</div>`
}
requestBodyTemplate(){
if(!this.request_body){
return '';
}
if (Object.keys(this.request_body).length == 0){
return '';
}
let mimeReqCount=0;
let shortMimeTypes={};
let bodyDescrHtml = this.request_body.description? html`<div class="m-markdown"> ${unsafeHTML(marked(this.request_body.description))}</div>`:'';
let textareaExampleHtml='';
let formDataHtml='';
const formDataTableRows = [];
let isFormDataPresent = false;
let reqSchemaTree="";
let content = this.request_body.content;
for(let mimeReq in content ) {
// do not change shortMimeTypes values, they are referenced in other places
if (mimeReq.includes('json')){shortMimeTypes[mimeReq]='json';}
else if (mimeReq.includes('xml')){shortMimeTypes[mimeReq]='xml';}
else if (mimeReq.includes('text/plain')){shortMimeTypes[mimeReq]='text';}
else if (mimeReq.includes('form-urlencoded')){shortMimeTypes[mimeReq]='form-urlencoded';}
else if (mimeReq.includes('multipart/form-data')){shortMimeTypes[mimeReq]='multipart-form-data';}
let mimeReqObj = content[mimeReq];
let reqExample="";
if (mimeReq.includes('json') || mimeReq.includes('xml') || mimeReq.includes('text/plain')){
try {
//Remove Circular references from RequestBody json-schema
mimeReqObj.schema = JSON.parse(JSON.stringify(mimeReqObj.schema, removeCircularReferences()));
}
catch{
console.error("Unable to resolve circular refs in schema", mimeReqObj.schema);
return;
}
reqSchemaTree = schemaToModel(mimeReqObj.schema,{});
reqExample = generateExample(mimeReqObj.examples, mimeReqObj.example, mimeReqObj.schema, mimeReq, "text");
textareaExampleHtml = textareaExampleHtml + `
<textarea
class="textarea mono request-body-param ${shortMimeTypes[mimeReq]}"
data-ptype="${mimeReq}"
style="display:${shortMimeTypes[mimeReq]==='json'?'block':'none'};
">${reqExample[0].exampleValue}</textarea>`
}
else if (mimeReq.includes('form') || mimeReq.includes('multipart-form')){
isFormDataPresent = true;
for (const fieldName in mimeReqObj.schema.properties) {
const fieldSchema = mimeReqObj.schema.properties[fieldName];
const fieldType = fieldSchema.type;
const arrayType = fieldSchema.type==='array'?fieldSchema.items.type:'';
formDataTableRows.push(html`
<tr>
<td style="min-width:100px;">
<div class="param-name">${fieldName}</div>
<div class="param-type">
${fieldType==='array' ? `${fieldType} of ${arrayType}`
:`${fieldType} ${fieldSchema.format?`\u00a0(${fieldSchema.format})`:''}`}
</div>
</td>
<td style="min-width:100px;">
${fieldType === 'array'?html`
<tag-input class="request-form-param" style="width:100%;font-size:13px;background:var(--input-bg);line-height:13px;"
data-ptype="${fieldType}"
data-pname="${fieldName}"
data-array="true"
placeholder="add-multiple\u23ce"
></tag-input>`
:html`<input
type="${fieldSchema.format==='binary'?'file':'text'}"
style="width:100%" class="request-form-param"
data-pname="${fieldName}"
data-ptype="${fieldType}"
data-array="false" />`
}
</td>
<td>
<div class="param-constraint"></div>
</td>
</tr>
${fieldSchema.description?html`
<tr>
<td style="border:none"></td>
<td colspan="2" style="border:none; margin-top:0; padding:0 5px;">
<span class="m-markdown-small">${unsafeHTML(marked(fieldSchema.description))}</span>
</td>
</tr>`
:``}
`);
}
formDataHtml = html`
<form class="${shortMimeTypes[mimeReq]}" onsubmit="event.preventDefault();">
<table style="width: 100%" class="m-table">
${formDataTableRows}
</table>
</form>`;
}
mimeReqCount++;
}
return html`
<div class="table-title top-gap ${isFormDataPresent?'form_data':'body_data'} "> ${isFormDataPresent?'FORM':'BODY'} DATA ${this.request_body.required?'(required)':''} </div>
${bodyDescrHtml}
${isFormDataPresent?html`${formDataHtml}`
:html`
<div class="tab-panel col" style="border-width:0; min-height:200px">
<div id="tab_buttons" class="tab-buttons row" @click="${this.activateTab}">
<button class="tab-btn active" content_id="tab_example">EXAMPLE </button>
<button class="tab-btn" content_id="tab_model">MODEL</button>
<div style="flex:1"> </div>
<div style="color:var(--light-fg); align-self:center; font-size:12px; margin-top:8px;">
${mimeReqCount==1?`
${Object.keys(shortMimeTypes)[0]}
`:html`
${Object.keys(shortMimeTypes).map(k => html`
${shortMimeTypes[k]==='json'?html`
<input type='radio' name='request_body_type' value='${shortMimeTypes[k]}' @change="${this.onMimeTypeChange}" checked style='margin:0 0 0 8px'/>
`
:html`
<input type='radio' name='request_body_type' value='${shortMimeTypes[k]}' @change="${this.onMimeTypeChange}" style='margin:0 0 0 8px'/>
`}
${shortMimeTypes[k]}`
)}
`}
</div>
</div>
<div id="tab_example" class="tab-content col" style="flex:1; ">
${unsafeHTML(textareaExampleHtml)}
</div>
<div id="tab_model" class="tab-content col" style="flex:1;display:none">
<schema-tree class="border" style="padding:16px;" .data="${reqSchemaTree}"></schema-tree>
</div>
</div>`
}`
}
apiCallTemplate(){
return html`
<div style="display:flex; align-items: center; margin:16px 0; font-size:12px;">
<div style="display:flex; flex-direction:column; margin:0; width:calc(100% - 60px);">
<div style="display:flex;flex-direction:row;overflow:hidden;"> <div style="font-weight:bold;">API_Server: </div>
${this.server?html`${this.server}`
: html`<div style="font-weight:bold;color:var(--error-color)">Not Set</div>`}
</div>
<div style="display:flex;flex-direction:row;overflow:hidden;line-height:16px;color:var(--fg2)">
${this.apiKeyValue && this.apiKeyName ? html`
<div style="font-weight:bold;color:var(--success-color)">Authentication: </div>
send <div style="font-family:var(--font-mono); color:var(--fg)"> '${this.apiKeyName}' </div>
in<div style="font-family:var(--font-mono); color:var(--fg)"> '${this.apiKeyLocation}' </div>
with value<div style="font-family:var(--font-mono); color:var(--fg)"> '${this.apiKeyValue.substring(0,3)+"***" }' </div>`
:html`<div style="color:var(--light-fg)">No Authentication Token provided</div>`}
</div>
</div>
<button class="m-btn" style="padding: 6px 0px;width:60px" @click="${this.onTryClick}">TRY</button>
</div>
${this.responseMessage===''?'':html`
<div class="row" style="font-size:12px; margin:5px 0">
<div class="response-message ${this.responseStatus}">Response Status: ${this.responseMessage}</div>
<div style="flex:1"></div>
<button class="m-btn" style="padding: 6px 0px;width:60px" @click="${this.clearResponseData}">CLEAR</button>
</div>
<div class="tab-panel col" style="border-width:0; min-height:200px">
<div id="tab_buttons" class="tab-buttons row" @click="${this.activateTab}">
<button class="tab-btn active" content_id="tab_response_text"> RESPONSE TEXT</button>
<button class="tab-btn" content_id="tab_response_headers"> RESPONSE HEADERS</button>
<button class="tab-btn" content_id="tab_curl">CURL</button>
</div>
<div id="tab_response_text" class="tab-content col" style="flex:1; ">
<textarea class="mono" style="min-height:180px; padding:16px;">${this.responseText}</textarea>
</div>
<div id="tab_response_headers" class="tab-content col" style="flex:1;display:none">
<textarea class="mono" style="min-height:180px; padding:16px; white-space:nowrap;">${this.responseHeaders}</textarea>
</div>
<div id="tab_curl" class="tab-content col" style="flex:1;display:none">
<code style="min-height:180px; padding:16px;font-size:12px; border:1px solid var(--input-border-color);overflow: scroll;word-break: break-word;">${this.curlSyntax}</code>
</div>
</div>`}
`
}
activateTab(e){
if (e.target.classList.contains("active") || e.target.classList.contains("tab-btn")===false){
return;
}
let activeTabBtn = e.currentTarget.parentNode.querySelector('.tab-btn.active');
let clickedTabBtn = e.target;
activeTabBtn.classList.remove("active");
e.target.classList.add("active");
let showContentEl = this.shadowRoot.getElementById(clickedTabBtn.attributes.content_id.value);
let allContentEls = e.currentTarget.parentNode.querySelectorAll('.tab-content');
if (showContentEl){
showContentEl.style.display="flex";
allContentEls.forEach(function(v){
if (v.attributes.id.value !== clickedTabBtn.attributes.content_id.value){
v.style.display="none";
}
})
}
}
onMimeTypeChange(e){
let textareaEls = e.target.closest('.tab-panel').querySelectorAll(`textarea.request-body-param`);
[...textareaEls].map(function(el){
el.style.display = el.classList.contains(e.target.value)?"block":"none";
});
}
onTryClick(e){
let me = this;
let curl="", curlHeaders="", curlData="", curlForm="";
let requestPanelEl = e.target.closest(".request-panel");
let pathParamEls = [...requestPanelEl.querySelectorAll(".request-param[data-ptype='path']")];
let queryParamEls = [...requestPanelEl.querySelectorAll(".request-param[data-ptype='query']")];
let headerParamEls = [...requestPanelEl.querySelectorAll(".request-param[data-ptype='header']")];
let formParamEls = [...requestPanelEl.querySelectorAll(".request-form-param")];
let bodyParamEls = [...requestPanelEl.querySelectorAll(".request-body-param")];
let fetchUrl = me.path;
let fetchOptions={
'mode' : "cors",
'method' : this.method.toUpperCase(),
'headers': {},
}
//Generate URL using Path Params
pathParamEls.map(function(el){
fetchUrl = fetchUrl.replace("{"+el.dataset.pname+"}", el.value);
});
//Submit Query Params
if (queryParamEls.length>0){
let queryParam = new URLSearchParams("");
queryParamEls.map(function(el){
if (el.dataset.array==='false'){
if (el.value !== ''){
queryParam.append(el.dataset.pname, el.value);
}
}
else {
let vals = el.getValues();
for(let v of vals){
queryParam.append(el.dataset.pname, v);
}
}
})
fetchUrl = `${fetchUrl}?${queryParam.toString()}`;
}
// Add authentication Query-Param if provided
if (this.apiKeyValue && this.apiKeyName && this.apiKeyLocation==='query'){
fetchUrl = `${fetchUrl}&${this.apiKeyName}=${this.apiKeyValue}`;
}
//Final URL for API call
fetchUrl = `${this.server.replace(/\/$/, "")}${fetchUrl}`;
curl=`curl -X ${this.method.toUpperCase()} "${fetchUrl}" `;
//Submit Header Params
headerParamEls.map(function(el){
if (el.value){
fetchOptions.headers[el.dataset.pname] = el.value;
curlHeaders = curlHeaders + ` -H "${fetchOptions.headers[el.dataset.pname]}: ${el.value}"`;
}
});
// Add Authentication Header if provided
if (this.apiKeyValue && this.apiKeyName && this.apiKeyLocation==='header'){
fetchOptions.headers[this.apiKeyName] = this.apiKeyValue;
curlHeaders = curlHeaders + ` -H "${this.apiKeyName}: ${this.apiKeyValue}"`;
}
//Submit Form Params (url-encoded or form-data)
if (formParamEls.length>=1){
let formEl = requestPanelEl.querySelector("form");
const formUrlParams = new URLSearchParams();
const formDataParams = new FormData();
formParamEls.map(function(el){
if (el.dataset.array==='false'){
if (el.type !== 'file'){
if (el.value !== ''){
formUrlParams.append(el.dataset.pname, el.value);
formDataParams.append(el.dataset.pname, el.value);
curlForm = curlForm + ` -F "${el.dataset.pname}=${el.value}"`;
}
}
else {
if (el.files[0]){
formUrlParams.append(el.dataset.pname, el.files[0]);
formDataParams.append(el.dataset.pname, el.files[0]);
curlForm = curlForm + ` -F "${el.dataset.pname}=@${el.value}"`;
}
}
}
else{
let vals = el.getValues();
for(let v of vals){
formUrlParams.append(el.dataset.pname, v);
formDataParams.append(el.dataset.pname, v);
curlForm = curlForm + ` -F "${el.dataset.pname}=${v}"`;
}
}
});
if (formEl.classList.contains("form-urlencoded")){
fetchOptions.headers['Content-Type'] = 'application/x-www-form-urlencoded; charset=utf-8'
curlHeaders = curlHeaders + ` -H "Content-Type: application/x-www-form-urlencoded"`;
fetchOptions.body = formUrlParams;
}
else {
//fetchOptions.headers['Content-Type'] = 'multipart/form-data' // Dont set content type for fetch, coz the browser must auto-generate boundry value too
curlHeaders = curlHeaders + ` -H "Content-Type: multipart/form-data"`;
fetchOptions.body = formDataParams;
}
}
//Submit Body Params (json/xml/text)
if (bodyParamEls.length>=1){
if (bodyParamEls.length===1){
fetchOptions.headers['Content-Type'] = bodyParamEls[0].dataset.ptype;
curlHeaders = curlHeaders + ` -H "Content-Type: ${bodyParamEls[0].dataset.ptype}"`;
fetchOptions.body=bodyParamEls[0].value;
curlData = ` -d ${JSON.stringify(bodyParamEls[0].value.replace(/(\r\n|\n|\r)/gm,"") )}`;
}
else{
let mimeTypeRadioEl = e.target.closest(".request-panel").querySelector("input[name='request_body_type']:checked");
let selectedBody = mimeTypeRadioEl===null?'json':mimeTypeRadioEl.value;
let bodyData='';
if (selectedBody === 'json'){
bodyData = requestPanelEl.querySelector(".request-body-param.json").value;
fetchOptions.headers['Content-Type'] = 'application/json; charset=utf-8';
curlHeaders = curlHeaders + ` -H "Content-Type: application/json"`;
}
else if (selectedBody === 'xml'){
bodyData = requestPanelEl.querySelector(".request-body-param.xml").value;
fetchOptions.headers['Content-Type'] = 'application/xml; charset=utf-8';
curlHeaders = curlHeaders + ` -H "Content-Type: application/xml"`;
}
else if (selectedBody === 'text'){
bodyData = requestPanelEl.querySelector(".request-body-param.text").value;
fetchOptions.headers['Content-Type'] = 'text/plain; charset=utf-8';
curlHeaders = curlHeaders + ` -H "Content-Type: text/plain"`;
}
fetchOptions.body=bodyData;
curlData = ` -d ${JSON.stringify(bodyData.replace(/(\r\n|\n|\r)/gm,""))}`;
}
}
me.responseUrl = '';
me.responseHeaders = '';
me.responseText = '';
me.curlSyntax = '';
me.responseStatus = 'success';
me.responseMessage = ''
fetch(fetchUrl,fetchOptions).then(function(resp){
me.curlSyntax = `${curl} ${curlHeaders} ${curlData} ${curlForm}`;
me.responseStatus = resp.ok ? 'success':'error';
me.responseMessage = `${resp.statusText}:${resp.status}`;
me.responseUrl = resp.url;
resp.headers.forEach(function(hdrVal, hdr) {
me.responseHeaders = me.responseHeaders + `${hdr.trim()}: ${hdrVal}`+"\n";
});
let contentType = resp.headers.get("content-type");
if(contentType && contentType.includes("json")) {
resp.json().then(function(respObj) {
me.responseText = JSON.stringify(respObj,null,2);
})
}
else{
resp.text().then(function(respText) {
me.responseText = respText;
})
}
})
.catch(function(err){
me.responseMessage = err.message + " (CORS or Network Issue)";
});
}
clearResponseData(){
this.responseUrl = '';
this.responseHeaders = '';
this.responseText = '';
this.responseStatus = 'success';
this.responseMessage = ''
}
}
// Register the element with the browser
customElements.define('api-request', ApiRequest);