Skip to content

Commit c32578f

Browse files
committed
Support form explode in request parameters. fix #262
1 parent 7af571c commit c32578f

2 files changed

Lines changed: 152 additions & 137 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ This package follows standard semver, `<major>.<minor>.<build>`. No breaking cha
1717
* Add id and name for autocomplete options.
1818
* Improve error on Access-Control-Allow-Private-Network.
1919
* Add `ph-no-capture` to all sensitive locations.
20+
* [Fix] Param explode for parameters when using `explode: true` and `style: form`, also aligns default behavior to match open api 3.1 specification.
2021

2122
## 2.1
2223
* Add `x-locale` vendor extension to specify the locale of the spec.

src/components/api-request.js

Lines changed: 151 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -139,143 +139,157 @@ export default class ApiRequest extends LitElement {
139139
continue;
140140
}
141141
const defaultVal = Array.isArray(paramSchema.default) ? paramSchema.default : `${paramSchema.default}`;
142-
let paramStyle = 'form';
143-
let paramExplode = true;
144-
if (paramLocation === 'query') {
145-
if (param.style && 'form spaceDelimited pipeDelimited'.includes(param.style)) {
146-
paramStyle = param.style;
147-
}
148-
if (typeof param.explode === 'boolean') {
149-
paramExplode = param.explode;
150-
}
151-
}
152-
153-
const displayAllowedValuesHints = (paramSchema.type === 'object' || paramSchema.type === 'array') && paramSchema.allowedValues;
154-
tableRows.push(html`
155-
<tr>
156-
<td colspan="1" style="width:160px; min-width:50px; vertical-align: top">
157-
<div class="param-name ${paramSchema.deprecated ? 'deprecated' : ''}" style="margin-top: 1rem;">
158-
${param.name}${!paramSchema.deprecated && param.required ? html`<span style='color:var(--red);'>*</span>` : ''}
159-
</div>
160-
<div class="param-type" style="margin-bottom: 1rem;">
161-
${paramSchema.type === 'array'
162-
? `${paramSchema.arrayType}`
163-
: `${paramSchema.format ? paramSchema.format : paramSchema.type}`
164-
}${!paramSchema.deprecated && param.required ? html`<span style='opacity: 0;'>*</span>` : ''}
165-
</div>
166-
</td>
167-
<td colspan="2" style="min-width:160px; vertical-align: top">
168-
${this.allowTry === 'true'
169-
? paramSchema.type === 'array' && html`
170-
<div style=" margin-top: 1rem; margin-bottom: 1rem;">
171-
<tag-input class="request-param"
172-
autocomplete="on"
173-
id = "request-param-${param.name}"
174-
style = "width:100%;"
175-
data-ptype = "${paramLocation}"
176-
data-pname = "${param.name}"
177-
data-default = "${Array.isArray(defaultVal) ? defaultVal.join('~|~') : defaultVal}"
178-
data-param-serialize-style = "${paramStyle}"
179-
data-param-serialize-explode = "${paramExplode}"
180-
data-array = "true"
181-
placeholder="add-multiple ↩"
182-
@change="${(e) => { this.storedParamValues[param.name] = e.detail.value; this.computeCurlSyntax(); }}"
183-
.value = "${this.storedParamValues[param.name] ?? (this.fillRequestWithDefault === 'true' && Array.isArray(defaultVal) ? defaultVal : defaultVal.split(','))}"></tag-input>
184-
</div>`
185-
|| paramSchema.type === 'object' && html`
186-
<textarea
187-
autocomplete="on"
188-
id = "request-param-${param.name}"
189-
@input="${() => { this.computeCurlSyntax(); }}"
190-
class = "textarea small request-param"
191-
part = "textarea small textarea-param"
192-
rows = 3
193-
data-ptype = "${paramLocation}"
194-
data-pname = "${param.name}"
195-
data-default = "${defaultVal}"
196-
data-param-serialize-style = "${paramStyle}"
197-
data-param-serialize-explode = "${paramExplode}"
198-
spellcheck = "false"
199-
placeholder="${paramSchema.example || defaultVal || ''}"
200-
style = "width:100%; margin-top: 1rem; margin-bottom: 1rem;"
201-
.value="${this.fillRequestWithDefault === 'true' ? defaultVal : ''}"></textarea>`
202-
|| paramSchema.allowedValues && html`
203-
<select aria-label="mime type" style="width:100%; margin-top: 1rem; margin-bottom: 1rem;"
204-
data-ptype="${paramLocation}"
205-
data-pname="${param.name}"
206-
.value="${this.fillRequestWithDefault === 'true' ? defaultVal : ''}"
207-
@change="${(e) => { this.storedParamValues[param.name] = e; this.computeCurlSyntax(); }}">
208-
${paramSchema.allowedValues.map((allowedValue) => html`
209-
<option value="${allowedValue}" ?selected = '${allowedValue === this.storedParamValues[param.name]}'>
210-
${allowedValue === null ? '-' : allowedValue}
211-
</option>`
212-
)}
213-
</select>`
214-
|| html`
215-
<input type="${paramSchema.format === 'password' ? 'password' : 'text'}" spellcheck="false" style="width:100%; margin-top: 1rem; margin-bottom: 1rem;"
216-
autocomplete="on"
217-
id="request-param-${param.name}"
218-
@input="${() => { this.computeCurlSyntax(); }}"
219-
placeholder="${paramSchema.example || defaultVal || ''}"
220-
class="request-param"
221-
part="textbox textbox-param"
222-
data-ptype="${paramLocation}"
223-
data-pname="${param.name}"
224-
data-default="${Array.isArray(defaultVal) ? defaultVal.join('~|~') : defaultVal}"
225-
data-array="false"
226-
@keyup="${this.requestParamFunction}"
227-
.value="${this.fillRequestWithDefault === 'true' ? defaultVal : ''}"
228-
/>`
229-
: ''}
230-
231-
${this.exampleListTemplate.call(this, param, paramSchema.type)}
232-
</td>
233-
${this.renderStyle === 'focused'
234-
? html`
235-
<td colspan="2" style="vertical-align: top">
236-
${param.description
237-
? html`
238-
<div class="param-description" style="margin-top: 1rem;">
239-
${unsafeHTML(toMarkdown(param.description))}
240-
</div>`
241-
: ''
242-
}
243-
${paramSchema.constraints.length || displayAllowedValuesHints || paramSchema.pattern
244-
? html`
245-
<div class="param-constraint" style="margin-top: 1rem;">
246-
${paramSchema.constraints.length ? html`<span style="font-weight:bold">Constraints: </span>${paramSchema.constraints.join(', ')}<br>` : ''}
247-
${paramSchema.pattern ? html`
248-
<div class="tooltip tooltip-replace" style="cursor: pointer; max-width: 100%; display: flex;">
249-
<div style="white-space:nowrap; font-weight:bold; margin-right: 2px;">Pattern: </div>
250-
<div style="white-space:nowrap; text-overflow:ellipsis; max-width:100%; overflow:hidden;">${paramSchema.pattern}</div>
251-
<br>
252-
<div class="tooltip-text" style="position: absolute; display:block;">${paramSchema.pattern}</div>
253-
</div>
254-
` : ''}
255-
${paramSchema.allowedValues?.map((v, i) => html`
256-
${i > 0 ? '|' : html`<span style="font-weight:bold">Allowed: </span>`}
257-
${html`
258-
<a part="anchor anchor-param-constraint" class = "${this.allowTry === 'true' ? '' : 'inactive-link'}"
259-
data-type="${paramSchema.type === 'array' ? 'array' : 'string'}"
260-
data-enum="${v?.trim()}"
261-
@click="${(e) => {
262-
const inputEl = e.target.closest('table').querySelector(`[data-pname="${param.name}"]`);
263-
if (inputEl) {
264-
inputEl.value = e.target.dataset.type === 'array' ? [e.target.dataset.enum] : e.target.dataset.enum;
265-
}
266-
}}"
267-
>
268-
${v === null ? '-' : v}
269-
</a>`
270-
}`)}
271-
</div>`
272-
: ''
273-
}
142+
// Set the default style: https://spec.openapis.org/oas/v3.1.0.html#fixed-fields-9
143+
const paramStyle = param.style ?? {
144+
query: 'form',
145+
path: 'simple',
146+
header: 'simple',
147+
cookie: 'form'
148+
}[paramLocation];
149+
150+
const paramExplode = param.explode ?? param.style === 'form';
151+
152+
const rowGenerator = ({ name: paramName, description: paramDescription, required: paramRequired }, generatedParamSchema) => {
153+
const displayAllowedValuesHints = (generatedParamSchema.type === 'object' || generatedParamSchema.type === 'array') && generatedParamSchema.allowedValues;
154+
return html`
155+
<tr>
156+
<td colspan="1" style="width:160px; min-width:50px; vertical-align: top">
157+
<div class="param-name ${generatedParamSchema.deprecated ? 'deprecated' : ''}" style="margin-top: 1rem;">
158+
${paramName}${!generatedParamSchema.deprecated && paramRequired ? html`<span style='color:var(--red);'>*</span>` : ''}
159+
</div>
160+
<div class="param-type" style="margin-bottom: 1rem;">
161+
${generatedParamSchema.type === 'array'
162+
? `${generatedParamSchema.arrayType}`
163+
: `${generatedParamSchema.format ? generatedParamSchema.format : generatedParamSchema.type}`
164+
}${!generatedParamSchema.deprecated && paramRequired ? html`<span style='opacity: 0;'>*</span>` : ''}
165+
</div>
274166
</td>
275-
</tr>`
276-
: ''
167+
<td colspan="2" style="min-width:160px; vertical-align: top">
168+
${this.allowTry === 'true'
169+
? generatedParamSchema.type === 'array' && html`
170+
<div style=" margin-top: 1rem; margin-bottom: 1rem;">
171+
<tag-input class="request-param"
172+
autocomplete="on"
173+
id = "request-param-${paramName}"
174+
style = "width:100%;"
175+
data-ptype = "${paramLocation}"
176+
data-pname = "${paramName}"
177+
data-default = "${Array.isArray(defaultVal) ? defaultVal.join('~|~') : defaultVal}"
178+
data-param-serialize-style = "${paramStyle}"
179+
data-param-serialize-explode = "${paramExplode}"
180+
data-array = "true"
181+
placeholder="add-multiple ↩"
182+
@change="${(e) => { this.storedParamValues[paramName] = e.detail.value; this.computeCurlSyntax(); }}"
183+
.value = "${this.storedParamValues[paramName] ?? (this.fillRequestWithDefault === 'true' && Array.isArray(defaultVal) ? defaultVal : defaultVal.split(','))}"></tag-input>
184+
</div>`
185+
|| generatedParamSchema.type === 'object' && html`
186+
<textarea
187+
autocomplete="on"
188+
id = "request-param-${paramName}"
189+
@input="${() => { this.computeCurlSyntax(); }}"
190+
class = "textarea small request-param"
191+
part = "textarea small textarea-param"
192+
rows = 3
193+
data-ptype = "${paramLocation}"
194+
data-pname = "${paramName}"
195+
data-default = "${defaultVal}"
196+
data-param-serialize-style = "${paramStyle}"
197+
data-param-serialize-explode = "${paramExplode}"
198+
spellcheck = "false"
199+
placeholder="${generatedParamSchema.example || defaultVal || ''}"
200+
style = "width:100%; margin-top: 1rem; margin-bottom: 1rem;"
201+
.value="${this.fillRequestWithDefault === 'true' ? defaultVal : ''}"></textarea>`
202+
|| generatedParamSchema.allowedValues && html`
203+
<select aria-label="mime type" style="width:100%; margin-top: 1rem; margin-bottom: 1rem;"
204+
data-ptype="${paramLocation}"
205+
data-pname="${paramName}"
206+
.value="${this.fillRequestWithDefault === 'true' ? defaultVal : ''}"
207+
@change="${(e) => { this.storedParamValues[paramName] = e; this.computeCurlSyntax(); }}">
208+
${generatedParamSchema.allowedValues.map((allowedValue) => html`
209+
<option value="${allowedValue}" ?selected = '${allowedValue === this.storedParamValues[paramName]}'>
210+
${allowedValue === null ? '-' : allowedValue}
211+
</option>`
212+
)}
213+
</select>`
214+
|| html`
215+
<input type="${generatedParamSchema.format === 'password' ? 'password' : 'text'}" spellcheck="false" style="width:100%; margin-top: 1rem; margin-bottom: 1rem;"
216+
autocomplete="on"
217+
id="request-param-${paramName}"
218+
@input="${() => { this.computeCurlSyntax(); }}"
219+
placeholder="${generatedParamSchema.example || defaultVal || ''}"
220+
class="request-param"
221+
part="textbox textbox-param"
222+
data-ptype="${paramLocation}"
223+
data-pname="${paramName}"
224+
data-default="${Array.isArray(defaultVal) ? defaultVal.join('~|~') : defaultVal}"
225+
data-array="false"
226+
@keyup="${this.requestParamFunction}"
227+
.value="${this.fillRequestWithDefault === 'true' ? defaultVal : ''}"
228+
/>`
229+
: ''}
230+
231+
${this.exampleListTemplate.call(this, param, generatedParamSchema.type)}
232+
</td>
233+
${this.renderStyle === 'focused'
234+
? html`
235+
<td colspan="2" style="vertical-align: top">
236+
${paramDescription
237+
? html`
238+
<div class="param-description" style="margin-top: 1rem;">
239+
${unsafeHTML(toMarkdown(paramDescription))}
240+
</div>`
241+
: ''
242+
}
243+
${generatedParamSchema.constraints.length || displayAllowedValuesHints || generatedParamSchema.pattern
244+
? html`
245+
<div class="param-constraint" style="margin-top: 1rem;">
246+
${generatedParamSchema.constraints.length ? html`<span style="font-weight:bold">Constraints: </span>${generatedParamSchema.constraints.join(', ')}<br>` : ''}
247+
${generatedParamSchema.pattern ? html`
248+
<div class="tooltip tooltip-replace" style="cursor: pointer; max-width: 100%; display: flex;">
249+
<div style="white-space:nowrap; font-weight:bold; margin-right: 2px;">Pattern: </div>
250+
<div style="white-space:nowrap; text-overflow:ellipsis; max-width:100%; overflow:hidden;">${generatedParamSchema.pattern}</div>
251+
<br>
252+
<div class="tooltip-text" style="position: absolute; display:block;">${generatedParamSchema.pattern}</div>
253+
</div>
254+
` : ''}
255+
${generatedParamSchema.allowedValues?.map((v, i) => html`
256+
${i > 0 ? '|' : html`<span style="font-weight:bold">Allowed: </span>`}
257+
${html`
258+
<a part="anchor anchor-param-constraint" class = "${this.allowTry === 'true' ? '' : 'inactive-link'}"
259+
data-type="${generatedParamSchema.type === 'array' ? 'array' : 'string'}"
260+
data-enum="${v?.trim()}"
261+
@click="${(e) => {
262+
const inputEl = e.target.closest('table').querySelector(`[data-pname="${paramName}"]`);
263+
if (inputEl) {
264+
inputEl.value = e.target.dataset.type === 'array' ? [e.target.dataset.enum] : e.target.dataset.enum;
265+
}
266+
}}"
267+
>
268+
${v === null ? '-' : v}
269+
</a>`
270+
}`)}
271+
</div>`
272+
: ''
273+
}
274+
</td>
275+
</tr>`
276+
: ''
277+
}
278+
`;
279+
};
280+
281+
let newRows = [];
282+
if (paramStyle === 'form' && paramExplode) {
283+
newRows = Object.keys(param.schema.properties).map(explodedParamKey => {
284+
const explodedParam = param.schema.properties[explodedParamKey];
285+
const explodedParamSchema = getTypeInfo(explodedParam, { includeNulls: this.includeNulls, enableExampleGeneration: true });
286+
return rowGenerator({ name: explodedParamKey, description: explodedParam.description, required: param.schema?.required?.includes(explodedParamKey) }, explodedParamSchema);
287+
});
288+
} else {
289+
newRows = rowGenerator(param, paramSchema);
277290
}
278-
`);
291+
292+
tableRows.push(newRows);
279293
}
280294

281295
return html`
@@ -734,7 +748,7 @@ export default class ApiRequest extends LitElement {
734748
} else if (paramSerializeStyle === 'pipeDelimited') {
735749
fetchUrl.searchParams.append(el.dataset.pname, values.join('|').replace(/^\||\|$/g, ''));
736750
} else {
737-
if (paramSerializeExplode === 'true') { // eslint-disable-line no-lonely-if
751+
if (paramSerializeExplode === 'true' || paramSerializeExplode === true) { // eslint-disable-line no-lonely-if
738752
values.forEach((v) => { fetchUrl.searchParams.append(el.dataset.pname, v); });
739753
} else {
740754
fetchUrl.searchParams.append(el.dataset.pname, values.join(',').replace(/^,|,$/g, ''));
@@ -759,7 +773,7 @@ export default class ApiRequest extends LitElement {
759773
} else if (paramSerializeStyle === 'pipeDelimited') {
760774
fetchUrl.searchParams.append(key, queryParamObj[key].join('|'));
761775
} else {
762-
if (paramSerializeExplode === 'true') { // eslint-disable-line no-lonely-if
776+
if (paramSerializeExplode === 'true' || paramSerializeExplode === true) { // eslint-disable-line no-lonely-if
763777
queryParamObj[key].forEach((v) => {
764778
fetchUrl.searchParams.append(key, v);
765779
});

0 commit comments

Comments
 (0)