From 8405fa01011f8bae3fa4f99ea924f43aa6c9bf24 Mon Sep 17 00:00:00 2001 From: Mahtis Michel Date: Wed, 10 Mar 2021 20:02:34 +0100 Subject: [PATCH] feat: request snippets plugin (#6910) --- docs/usage/configuration.md | 4 +- src/core/components/curl.jsx | 4 +- src/core/components/live-response.jsx | 9 +- src/core/curlify.js | 75 ------ src/core/index.js | 26 ++- src/core/plugins/request-snippets/fn.js | 217 ++++++++++++++++++ src/core/plugins/request-snippets/index.js | 16 ++ .../request-snippets/request-snippets.jsx | 127 ++++++++++ .../plugins/request-snippets/selectors.js | 45 ++++ src/core/plugins/spec/actions.js | 8 +- src/core/presets/base.js | 4 +- src/core/syntax-highlighting.js | 4 + src/core/utils.js | 19 -- test/e2e-cypress/helpers/multiple-examples.js | 20 +- .../tests/features/multiple-examples-core.js | 60 ++--- .../features/oas3-request-body-required.js | 2 +- test/mocha/components/live-response.jsx | 14 +- test/unit/core/curlify.js | 93 +++----- 18 files changed, 537 insertions(+), 210 deletions(-) delete mode 100644 src/core/curlify.js create mode 100644 src/core/plugins/request-snippets/fn.js create mode 100644 src/core/plugins/request-snippets/index.js create mode 100644 src/core/plugins/request-snippets/request-snippets.jsx create mode 100644 src/core/plugins/request-snippets/selectors.js diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md index 8d3f45910cf..16c4383b499 100644 --- a/docs/usage/configuration.md +++ b/docs/usage/configuration.md @@ -65,6 +65,8 @@ Parameter name | Docker variable | Description `syntaxHighlight.activate` | _Unavailable_ | `Boolean=true`. Whether syntax highlighting should be activated or not. `syntaxHighlight.theme` | _Unavailable_ | `String=["agate"*, "arta", "monokai", "nord", "obsidian", "tomorrow-night"]`. [Highlight.js](https://highlightjs.org/static/demo/) syntax coloring theme to use. (Only these 6 styles are available.) `tryItOutEnabled` | `TRY_IT_OUT_ENABLED` | `Boolean=false`. Controls whether the "Try it out" section should be enabled by default. +`requestSnippets` | _Unavailable_ | `Object`. This is the default configuration section for the the requestSnippets plugin.
requestSnippets: {
  generators: {
    "curl_bash": {
      title: "cURL (bash)",
      syntax: "bash"
    },
    "curl_powershell": {
      title: "cURL (PowerShell)",
      syntax: "powershell"
    },
    "curl_cmd": {
      title: "cURL (CMD)",
      syntax: "bash"
    },
    "node_native": {
      title: "Node.js (Native)",
      syntax: "javascript"
    },
  },
  defaultExpanded: true,
  languagesMask: null, // e.g. only show curl bash = \["curl_bash"\]
}, + ##### Network @@ -168,4 +170,4 @@ SPEC="{ \"openapi\": \"3.0.0\" }" ```sh SUPPORTED_SUBMIT_METHODS=['get', 'post'] URLS=[ { url: 'http://petstore.swagger.io/v2/swagger.json', name: 'Petstore' } ] -``` \ No newline at end of file +``` diff --git a/src/core/components/curl.jsx b/src/core/components/curl.jsx index c453e6a0681..44f3b15e657 100644 --- a/src/core/components/curl.jsx +++ b/src/core/components/curl.jsx @@ -1,9 +1,9 @@ import React from "react" import PropTypes from "prop-types" -import curlify from "core/curlify" import { CopyToClipboard } from "react-copy-to-clipboard" import {SyntaxHighlighter, getStyle} from "core/syntax-highlighting" import get from "lodash/get" +import { requestSnippetGenerator_curl_bash } from "../plugins/request-snippets/fn" export default class Curl extends React.Component { static propTypes = { @@ -13,7 +13,7 @@ export default class Curl extends React.Component { render() { let { request, getConfigs } = this.props - let curl = curlify(request) + let curl = requestSnippetGenerator_curl_bash(request) const config = getConfigs() diff --git a/src/core/components/live-response.jsx b/src/core/components/live-response.jsx index 4bb9d8cda05..11375b55b80 100644 --- a/src/core/components/live-response.jsx +++ b/src/core/components/live-response.jsx @@ -49,7 +49,7 @@ export default class LiveResponse extends React.Component { render() { const { response, getComponent, getConfigs, displayRequestDuration, specSelectors, path, method } = this.props - const { showMutatedRequest } = getConfigs() + const { showMutatedRequest, requestSnippetsEnabled } = getConfigs() const curlRequest = showMutatedRequest ? specSelectors.mutatedRequestFor(path, method) : specSelectors.requestFor(path, method) const status = response.get("status") @@ -62,7 +62,6 @@ export default class LiveResponse extends React.Component { const headersKeys = Object.keys(headers) const contentType = headers["content-type"] || headers["Content-Type"] - const Curl = getComponent("curl") const ResponseBody = getComponent("responseBody") const returnObject = headersKeys.map(key => { var joinedHeaders = Array.isArray(headers[key]) ? headers[key].join() : headers[key] @@ -70,10 +69,14 @@ export default class LiveResponse extends React.Component { }) const hasHeaders = returnObject.length !== 0 const Markdown = getComponent("Markdown", true) + const RequestSnippets = getComponent("RequestSnippets", true) + const Curl = getComponent("curl") return (
- { curlRequest && } + { curlRequest && (requestSnippetsEnabled === true || requestSnippetsEnabled === "true" + ? + : ) } { url &&

Request URL

diff --git a/src/core/curlify.js b/src/core/curlify.js deleted file mode 100644 index 0735fdccd07..00000000000 --- a/src/core/curlify.js +++ /dev/null @@ -1,75 +0,0 @@ -import win from "./window" -import { Map } from "immutable" - -/** - * if duplicate key name existed from FormData entries, - * we mutated the key name by appending a hashIdx - * @param {String} k - possibly mutated key name - * @return {String} - src key name - */ -const extractKey = (k) => { - const hashIdx = "_**[]" - if (k.indexOf(hashIdx) < 0) { - return k - } - return k.split(hashIdx)[0].trim() -} - -export default function curl( request ){ - let curlified = [] - let isMultipartFormDataRequest = false - let headers = request.get("headers") - curlified.push( "curl" ) - - if (request.get("curlOptions")) { - curlified.push(...request.get("curlOptions")) - } - - curlified.push( "-X", request.get("method") ) - curlified.push( `"${request.get("url")}"`) - - if ( headers && headers.size ) { - for( let p of request.get("headers").entries() ){ - let [ h,v ] = p - curlified.push( "-H " ) - curlified.push( `"${h}: ${v.replace(/\$/g, "\\$")}"` ) - isMultipartFormDataRequest = isMultipartFormDataRequest || /^content-type$/i.test(h) && /^multipart\/form-data$/i.test(v) - } - } - - if ( request.get("body") ){ - if (isMultipartFormDataRequest && ["POST", "PUT", "PATCH"].includes(request.get("method"))) { - for( let [ k,v ] of request.get("body").entrySeq()) { - let extractedKey = extractKey(k) - curlified.push( "-F" ) - if (v instanceof win.File) { - curlified.push(`"${extractedKey}=@${v.name}${v.type ? `;type=${v.type}` : ""}"` ) - } else { - curlified.push(`"${extractedKey}=${v}"` ) - } - } - } else { - curlified.push( "-d" ) - let reqBody = request.get("body") - if (!Map.isMap(reqBody)) { - curlified.push( JSON.stringify( request.get("body") ).replace(/\\n/g, "").replace(/\$/g, "\\$") ) - } else { - let curlifyToJoin = [] - for (let [k, v] of request.get("body").entrySeq()) { - let extractedKey = extractKey(k) - if (v instanceof win.File) { - curlifyToJoin.push(`"${extractedKey}":{"name":"${v.name}"${v.type ? `,"type":"${v.type}"` : ""}}`) - } else { - curlifyToJoin.push(`"${extractedKey}":${JSON.stringify(v).replace(/\\n/g, "").replace("$", "\\$")}`) - } - } - curlified.push(`{${curlifyToJoin.join()}}`) - } - } - } else if(!request.get("body") && request.get("method") === "POST") { - curlified.push( "-d" ) - curlified.push( "\"\"" ) - } - - return curlified.join( " " ) -} diff --git a/src/core/index.js b/src/core/index.js index 72a32d3ff03..32493a92819 100644 --- a/src/core/index.js +++ b/src/core/index.js @@ -53,6 +53,29 @@ export default function SwaggerUI(opts) { showExtensions: false, showCommonExtensions: false, withCredentials: undefined, + requestSnippetsEnabled: false, + requestSnippets: { + generators: { + "curl_bash": { + title: "cURL (bash)", + syntax: "bash" + }, + "curl_powershell": { + title: "cURL (PowerShell)", + syntax: "powershell" + }, + "curl_cmd": { + title: "cURL (CMD)", + syntax: "bash" + }, + "node_native": { + title: "Node.js (Native)", + syntax: "javascript" + }, + }, + defaultExpanded: true, + languagesMask: null, // e.g. only show curl bash = ["curl_bash"] + }, supportedSubmitMethods: [ "get", "put", @@ -107,7 +130,8 @@ export default function SwaggerUI(opts) { spec: { spec: "", url: constructorConfig.url - } + }, + requestSnippets: constructorConfig.requestSnippets }, constructorConfig.initialState) } diff --git a/src/core/plugins/request-snippets/fn.js b/src/core/plugins/request-snippets/fn.js new file mode 100644 index 00000000000..f692d2d37b3 --- /dev/null +++ b/src/core/plugins/request-snippets/fn.js @@ -0,0 +1,217 @@ +import win from "../../window" +import { Map } from "immutable" +import Url from "url-parse" + +/** + * if duplicate key name existed from FormData entries, + * we mutated the key name by appending a hashIdx + * @param {String} k - possibly mutated key name + * @return {String} - src key name + */ +const extractKey = (k) => { + const hashIdx = "_**[]" + if (k.indexOf(hashIdx) < 0) { + return k + } + return k.split(hashIdx)[0].trim() +} + +const escapeShell = (str) => { + if (str === "-d ") { + return str + } + // eslint-disable-next-line no-useless-escape + if (!/^[_\/-]/g.test(str)) + return ("'" + str + .replace(/'/g, "'\\''") + "'") + else + return str +} + +const escapeCMD = (str) => { + str = str + .replace(/\^/g, "^^") + .replace(/\\"/g, "\\\\\"") + .replace(/"/g, "\"\"") + .replace(/\n/g, "^\n") + if (str === "-d ") { + return str + .replace(/-d /g, "-d ^\n") + } + // eslint-disable-next-line no-useless-escape + if (!/^[_\/-]/g.test(str)) + return "\"" + str + "\"" + else + return str +} + +const escapePowershell = (str) => { + if (str === "-d ") { + return str + } + if (/\n/.test(str)) { + return "@\"\n" + str.replace(/"/g, "\\\"").replace(/`/g, "``").replace(/\$/, "`$") + "\n\"@" + } + // eslint-disable-next-line no-useless-escape + if (!/^[_\/-]/g.test(str)) + return "'" + str + .replace(/"/g, "\"\"") + .replace(/'/g, "''") + "'" + else + return str +} + +function getStringBodyOfMap(request) { + let curlifyToJoin = [] + for (let [k, v] of request.get("body").entrySeq()) { + let extractedKey = extractKey(k) + if (v instanceof win.File) { + curlifyToJoin.push(` "${extractedKey}": {\n "name": "${v.name}"${v.type ? `,\n "type": "${v.type}"` : ""}\n }`) + } else { + curlifyToJoin.push(` "${extractedKey}": ${JSON.stringify(v, null, 2).replace(/(\r\n|\r|\n)/g, "\n ")}`) + } + } + return `{\n${curlifyToJoin.join(",\n")}\n}` +} + +const curlify = (request, escape, newLine, ext = "") => { + let isMultipartFormDataRequest = false + let curlified = "" + const addWords = (...args) => curlified += " " + args.map(escape).join(" ") + const addWordsWithoutLeadingSpace = (...args) => curlified += args.map(escape).join(" ") + const addNewLine = () => curlified += ` ${newLine}` + const addIndent = (level = 1) => curlified += " ".repeat(level) + let headers = request.get("headers") + curlified += "curl" + ext + + if (request.has("curlOptions")) { + addWords(...request.get("curlOptions")) + } + + addWords("-X", request.get("method")) + + addNewLine() + addIndent() + addWordsWithoutLeadingSpace(`${request.get("url")}`) + + if (headers && headers.size) { + for (let p of request.get("headers").entries()) { + addNewLine() + addIndent() + let [h, v] = p + addWordsWithoutLeadingSpace("-H", `${h}: ${v}`) + isMultipartFormDataRequest = isMultipartFormDataRequest || /^content-type$/i.test(h) && /^multipart\/form-data$/i.test(v) + } + } + + if (request.get("body")) { + if (isMultipartFormDataRequest && ["POST", "PUT", "PATCH"].includes(request.get("method"))) { + for (let [k, v] of request.get("body").entrySeq()) { + let extractedKey = extractKey(k) + addNewLine() + addIndent() + addWordsWithoutLeadingSpace("-F") + if (v instanceof win.File) { + addWords(`${extractedKey}=@${v.name}${v.type ? `;type=${v.type}` : ""}`) + } else { + addWords(`${extractedKey}=${v}`) + } + } + } else { + addNewLine() + addIndent() + addWordsWithoutLeadingSpace("-d ") + let reqBody = request.get("body") + if (!Map.isMap(reqBody)) { + if (typeof reqBody !== "string") { + reqBody = JSON.stringify(reqBody) + } + addWordsWithoutLeadingSpace(reqBody) + } else { + addWordsWithoutLeadingSpace(getStringBodyOfMap(request)) + } + } + } else if (!request.get("body") && request.get("method") === "POST") { + addNewLine() + addIndent() + addWordsWithoutLeadingSpace("-d ''") + } + + return curlified +} + +// eslint-disable-next-line camelcase +export const requestSnippetGenerator_curl_powershell = (request) => { + return curlify(request, escapePowershell, "`\n", ".exe") +} + +// eslint-disable-next-line camelcase +export const requestSnippetGenerator_curl_bash = (request) => { + return curlify(request, escapeShell, "\\\n") +} + +// eslint-disable-next-line camelcase +export const requestSnippetGenerator_curl_cmd = (request) => { + return curlify(request, escapeCMD, "^\n") +} + +// eslint-disable-next-line camelcase +export const requestSnippetGenerator_node_native = (request) => { + const url = new Url(request.get("url")) + let isMultipartFormDataRequest = false + const headers = request.get("headers") + if(headers && headers.size) { + request.get("headers").map((val, key) => { + isMultipartFormDataRequest = isMultipartFormDataRequest || /^content-type$/i.test(key) && /^multipart\/form-data$/i.test(val) + }) + } + const packageStr = url.protocol === "https:" ? "https" : "http" + let reqBody = request.get("body") + if (request.get("body")) { + if (isMultipartFormDataRequest && ["POST", "PUT", "PATCH"].includes(request.get("method"))) { + return "throw new Error(\"Currently unsupported content-type: /^multipart\\/form-data$/i\");" + } else { + if (!Map.isMap(reqBody)) { + if (typeof reqBody !== "string") { + reqBody = JSON.stringify(reqBody) + } + } else { + reqBody = getStringBodyOfMap(request) + } + } + } else if (!request.get("body") && request.get("method") === "POST") { + reqBody = "" + } + + const stringBody = "`" + (reqBody || "") + .replace(/\\n/g, "\n") + .replace(/`/g, "\\`") + + "`" + + return `const http = require("${packageStr}"); + +const options = { + "method": "${request.get("method")}", + "hostname": "${url.host}", + "port": ${url.port || "null"}, + "path": "${url.pathname}"${headers && headers.size ? `, + "headers": { + ${request.get("headers").map((val, key) => `"${key}": "${val}"`).valueSeq().join(",\n ")} + }` : ""} +}; + +const req = http.request(options, function (res) { + const chunks = []; + + res.on("data", function (chunk) { + chunks.push(chunk); + }); + + res.on("end", function () { + const body = Buffer.concat(chunks); + console.log(body.toString()); + }); +}); +${reqBody ? `\nreq.write(${stringBody});` : ""} +req.end();` +} diff --git a/src/core/plugins/request-snippets/index.js b/src/core/plugins/request-snippets/index.js new file mode 100644 index 00000000000..cbda08f996b --- /dev/null +++ b/src/core/plugins/request-snippets/index.js @@ -0,0 +1,16 @@ +import * as fn from "./fn" +import * as selectors from "./selectors" +import { RequestSnippets } from "./request-snippets" +export default () => { + return { + components: { + RequestSnippets + }, + fn, + statePlugins: { + requestSnippets: { + selectors + } + } + } +} diff --git a/src/core/plugins/request-snippets/request-snippets.jsx b/src/core/plugins/request-snippets/request-snippets.jsx new file mode 100644 index 00000000000..12bb9b695b7 --- /dev/null +++ b/src/core/plugins/request-snippets/request-snippets.jsx @@ -0,0 +1,127 @@ +import React from "react" +import { CopyToClipboard } from "react-copy-to-clipboard" +import PropTypes from "prop-types" +import get from "lodash/get" +import {SyntaxHighlighter, getStyle} from "core/syntax-highlighting" + +export class RequestSnippets extends React.Component { + constructor() { + super() + this.state = { + activeLanguage: this.props?.requestSnippetsSelectors?.getSnippetGenerators()?.keySeq().first(), + expanded: this.props?.requestSnippetsSelectors?.getDefaultExpanded(), + } + } + + static propTypes = { + request: PropTypes.object.isRequired, + requestSnippetsSelectors: PropTypes.object.isRequired, + getConfigs: PropTypes.object.isRequired, + requestSnippetsActions: PropTypes.object.isRequired, + } + render() { + const {request, getConfigs, requestSnippetsSelectors } = this.props + const snippetGenerators = requestSnippetsSelectors.getSnippetGenerators() + const activeLanguage = this.state.activeLanguage || snippetGenerators.keySeq().first() + const activeGenerator = snippetGenerators.get(activeLanguage) + const snippet = activeGenerator.get("fn")(request) + const onGenChange = (key) => { + const needsChange = activeLanguage !== key + if(needsChange) { + this.setState({ + activeLanguage: key + }) + } + } + const style = { + cursor: "pointer", + lineHeight: 1, + display: "inline-flex", + backgroundColor: "rgb(250, 250, 250)", + paddingBottom: "0", + paddingTop: "0", + border: "1px solid rgb(51, 51, 51)", + borderRadius: "4px 4px 0 0", + boxShadow: "none", + borderBottom: "none" + } + const activeStyle = { + cursor: "pointer", + lineHeight: 1, + display: "inline-flex", + backgroundColor: "rgb(51, 51, 51)", + boxShadow: "none", + border: "1px solid rgb(51, 51, 51)", + paddingBottom: "0", + paddingTop: "0", + borderRadius: "4px 4px 0 0", + marginTop: "-5px", + marginRight: "-5px", + marginLeft: "-5px", + zIndex: "9999", + borderBottom: "none" + } + const getBtnStyle = (key) => { + if (key === activeLanguage) { + return activeStyle + } + return style + } + const config = getConfigs() + + const SnippetComponent = config?.syntaxHighlight?.activated + ? + {snippet} + + : + + + const expanded = this.state.expanded === undefined ? this.props?.requestSnippetsSelectors?.getDefaultExpanded() : this.state.expanded + return ( +
+
+

this.setState({expanded: !expanded})} + >Snippets

+ +
+ { + expanded &&
+
+ { + snippetGenerators.map((gen, key) => { + return (
onGenChange(key)}> +

{gen.get("title")}

+
) + }) + } +
+
+ +
+
+ {SnippetComponent} +
+
+ } +
+ + ) + } +} diff --git a/src/core/plugins/request-snippets/selectors.js b/src/core/plugins/request-snippets/selectors.js new file mode 100644 index 00000000000..396f7295209 --- /dev/null +++ b/src/core/plugins/request-snippets/selectors.js @@ -0,0 +1,45 @@ +import { createSelector } from "reselect" +import { Map } from "immutable" + +const state = state => state || Map() + +export const getGenerators = createSelector( + state, + state => { + const languageKeys = state + .get("languages") + const generators = state + .get("generators", Map()) + if(!languageKeys) { + return generators + } + return generators + .filter((v, key) => languageKeys.includes(key)) + } +) + +export const getSnippetGenerators = (state) => ({ fn }) => { + const getGenFn = (key) => fn[`requestSnippetGenerator_${key}`] + return getGenerators(state) + .map((gen, key) => { + const genFn = getGenFn(key) + if(typeof genFn !== "function") { + return null + } + + return gen.set("fn", genFn) + }) + .filter(v => v) +} + +export const getActiveLanguage = createSelector( + state, + state => state + .get("activeLanguage") +) + +export const getDefaultExpanded = createSelector( + state, + state => state + .get("defaultExpanded") +) diff --git a/src/core/plugins/spec/actions.js b/src/core/plugins/spec/actions.js index 147de2aabf8..5e296c64571 100644 --- a/src/core/plugins/spec/actions.js +++ b/src/core/plugins/spec/actions.js @@ -5,7 +5,7 @@ import serializeError from "serialize-error" import isString from "lodash/isString" import debounce from "lodash/debounce" import set from "lodash/set" -import { isJSONObject, paramToValue, isEmptyValue } from "core/utils" +import { paramToValue, isEmptyValue } from "core/utils" // Actions conform to FSA (flux-standard-actions) // {type: string,payload: Any|Error, meta: obj, error: bool} @@ -426,9 +426,7 @@ export const executeRequest = (req) => const requestBody = oas3Selectors.requestBodyValue(pathName, method) const requestBodyInclusionSetting = oas3Selectors.requestBodyInclusionSetting(pathName, method) - if(isJSONObject(requestBody)) { - req.requestBody = JSON.parse(requestBody) - } else if(requestBody && requestBody.toJS) { + if(requestBody && requestBody.toJS) { req.requestBody = requestBody .map( (val) => { @@ -445,7 +443,7 @@ export const executeRequest = (req) => ) || requestBodyInclusionSetting.get(key) ) .toJS() - } else{ + } else { req.requestBody = requestBody } } diff --git a/src/core/presets/base.js b/src/core/presets/base.js index 73565cdf7cd..95e7329dd07 100644 --- a/src/core/presets/base.js +++ b/src/core/presets/base.js @@ -3,6 +3,7 @@ import layout from "core/plugins/layout" import spec from "core/plugins/spec" import view from "core/plugins/view" import samples from "core/plugins/samples" +import requestSnippets from "core/plugins/request-snippets" import logs from "core/plugins/logs" import swaggerJs from "core/plugins/swagger-js" import auth from "core/plugins/auth" @@ -191,6 +192,7 @@ export default function() { downloadUrlPlugin, deepLinkingPlugin, filter, - onComplete + onComplete, + requestSnippets ] } diff --git a/src/core/syntax-highlighting.js b/src/core/syntax-highlighting.js index e3e760c5f1b..25e7c31185b 100644 --- a/src/core/syntax-highlighting.js +++ b/src/core/syntax-highlighting.js @@ -6,6 +6,8 @@ import xml from "react-syntax-highlighter/dist/esm/languages/hljs/xml" import bash from "react-syntax-highlighter/dist/esm/languages/hljs/bash" import yaml from "react-syntax-highlighter/dist/esm/languages/hljs/yaml" import http from "react-syntax-highlighter/dist/esm/languages/hljs/http" +import powershell from "react-syntax-highlighter/dist/esm/languages/hljs/powershell" +import javascript from "react-syntax-highlighter/dist/esm/languages/hljs/javascript" import agate from "react-syntax-highlighter/dist/esm/styles/hljs/agate" import arta from "react-syntax-highlighter/dist/esm/styles/hljs/arta" @@ -20,6 +22,8 @@ SyntaxHighlighter.registerLanguage("xml", xml) SyntaxHighlighter.registerLanguage("yaml", yaml) SyntaxHighlighter.registerLanguage("http", http) SyntaxHighlighter.registerLanguage("bash", bash) +SyntaxHighlighter.registerLanguage("powershell", powershell) +SyntaxHighlighter.registerLanguage("javascript", javascript) const styles = {agate, arta, monokai, nord, obsidian, "tomorrow-night": tomorrowNight} export const availableStyles = Object.keys(styles) diff --git a/src/core/utils.js b/src/core/utils.js index 59be73f56f9..853bf68297e 100644 --- a/src/core/utils.js +++ b/src/core/utils.js @@ -32,25 +32,6 @@ const DEFAULT_RESPONSE_KEY = "default" export const isImmutable = (maybe) => Im.Iterable.isIterable(maybe) -export function isJSONObject (str) { - try { - var o = JSON.parse(str) - - // Handle non-exception-throwing cases: - // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking, - // but... JSON.parse(null) returns null, and typeof null === "object", - // so we must check for that, too. Thankfully, null is falsey, so this suffices: - if (o && typeof o === "object") { - return o - } - } - catch (e) { - // do nothing - } - - return false -} - export function objectify (thing) { if(!isObject(thing)) return {} diff --git a/test/e2e-cypress/helpers/multiple-examples.js b/test/e2e-cypress/helpers/multiple-examples.js index 2d024c0db45..88a45043f97 100644 --- a/test/e2e-cypress/helpers/multiple-examples.js +++ b/test/e2e-cypress/helpers/multiple-examples.js @@ -208,7 +208,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the curl body // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleA.serializedValue || exampleA.value}"`) + .contains(`-d '${exampleA.serializedValue || exampleA.value}'`) }) it("should set default static and Try-It-Out values based on choosing the second member in static mode", () => { @@ -234,7 +234,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the request URL // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleB.serializedValue || exampleB.value}"`) + .contains(`-d '${exampleB.serializedValue || exampleB.value}'`) }) it("should set default static and Try-It-Out values based on choosing the second member in Try-It-Out mode", () => { @@ -257,7 +257,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the request URL // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleB.serializedValue || exampleB.value}"`) + .contains(`-d '${exampleB.serializedValue || exampleB.value}'`) // Switch to static docs .get(".try-out__btn") .click() @@ -323,7 +323,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the request URL // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleB.serializedValue || exampleB.value}"`) + .contains(`-d '${exampleB.serializedValue || exampleB.value}'`) }) it("should use the first example for the media type when changing the media type without prior interactions with the value", () => { @@ -349,7 +349,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the request URL // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleA.serializedValue || exampleA.value}"`) + .contains(`-d '${exampleA.serializedValue || exampleA.value}'`) }) it("static mode toggling: mediaType -> example -> mediaType -> example", () => { @@ -489,7 +489,7 @@ function RequestBodyPrimitiveTestCases({ // TODO: use an interceptor instead of curl .get(".curl") .contains( - `-d "${customUserInputExpectedCurlSubstring || customUserInput}"` + `-d '${customUserInputExpectedCurlSubstring || customUserInput}'` ) // Choose exampleB @@ -508,7 +508,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the curl body // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleB.serializedValue || exampleB.value}"`) + .contains(`-d '${exampleB.serializedValue || exampleB.value}'`) // Ensure the modified value is still accessible .get(".opblock-section-request-body .examples-select > select") @@ -530,7 +530,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the curl body // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleB.serializedValue || exampleB.value}"`) + .contains(`-d '${exampleB.serializedValue || exampleB.value}'`) // Ensure the modified value is still accessible .get(".opblock-section-request-body .examples-select > select") @@ -552,7 +552,7 @@ function RequestBodyPrimitiveTestCases({ // Assert on the curl body // TODO: use an interceptor instead of curl .get(".curl") - .contains(`-d "${exampleA.serializedValue || exampleA.value}"`) + .contains(`-d '${exampleA.serializedValue || exampleA.value}'`) // Ensure the modified value is still the same value .get(".opblock-section-request-body .examples-select > select") @@ -571,7 +571,7 @@ function RequestBodyPrimitiveTestCases({ // TODO: use an interceptor instead of curl .get(".curl") .contains( - `-d "${customUserInputExpectedCurlSubstring || customUserInput}"` + `-d '${customUserInputExpectedCurlSubstring || customUserInput}'` ) }) diff --git a/test/e2e-cypress/tests/features/multiple-examples-core.js b/test/e2e-cypress/tests/features/multiple-examples-core.js index 333b797df13..6eef6267198 100644 --- a/test/e2e-cypress/tests/features/multiple-examples-core.js +++ b/test/e2e-cypress/tests/features/multiple-examples-core.js @@ -2,7 +2,7 @@ * @prettier */ - const { +const { ParameterPrimitiveTestCases, RequestBodyPrimitiveTestCases, ResponsePrimitiveTestCases, @@ -256,7 +256,7 @@ describe("OpenAPI 3.0 Multiple Examples - core features", () => { .get("#operations-default-post_Array") .click() .get(".json-schema-form-item > input") - .then(inputs => { + .then((inputs) => { expect(inputs.map((i, el) => el.value).toArray()).to.deep.equal([ "a", "b", @@ -276,7 +276,7 @@ describe("OpenAPI 3.0 Multiple Examples - core features", () => { .get(".parameters-col_description .examples-select > select") .select("ArrayExampleB") .get(".json-schema-form-item > input") - .then(inputs => { + .then((inputs) => { expect(inputs.map((i, el) => el.value).toArray()).to.deep.equal([ "1", "2", @@ -317,7 +317,7 @@ describe("OpenAPI 3.0 Multiple Examples - core features", () => { .type("5") // Assert against the input fields .get(".json-schema-form-item > input") - .then(inputs => { + .then((inputs) => { expect(inputs.map((i, el) => el.value).toArray()).to.deep.equal([ "1", "2", @@ -351,7 +351,7 @@ describe("OpenAPI 3.0 Multiple Examples - core features", () => { .select("ArrayExampleB") // Assert against the input fields .get(".json-schema-form-item > input") - .then(inputs => { + .then((inputs) => { expect(inputs.map((i, el) => el.value).toArray()).to.deep.equal([ "1", "2", @@ -367,7 +367,7 @@ describe("OpenAPI 3.0 Multiple Examples - core features", () => { .select("__MODIFIED__VALUE__") // Assert that our modified value is back .get(".json-schema-form-item > input") - .then(inputs => { + .then((inputs) => { expect(inputs.map((i, el) => el.value).toArray()).to.deep.equal([ "1", "2", @@ -566,41 +566,43 @@ describe("OpenAPI 3.0 Multiple Examples - core features", () => { }) }) describe("in a Request Body", () => { + const exampleA = JSON.stringify( + { + firstName: "Kyle", + lastName: "Shockey", + email: "kyle.shockey@smartbear.com", + }, + null, + 2 + ) + const exampleB = JSON.stringify( + { + name: "Abbey", + type: "kitten", + color: "calico", + gender: "female", + age: "11 weeks", + }, + null, + 2 + ) RequestBodyPrimitiveTestCases({ operationDomId: "#operations-default-post_Object", primaryMediaType: "application/json", // ↓ not a typo, Cypress requires escaping { when using `cy.type` customUserInput: `{{} "openapiIsCool": true }`, customExpectedUrlSubstring: "?openapiIsCool=true", - customUserInputExpectedCurlSubstring: `{\\"openapiIsCool\\":true}`, + customUserInputExpectedCurlSubstring: `{ "openapiIsCool": true }`, exampleA: { key: "ObjectExampleA", - serializedValue: `{\\"firstName\\":\\"Kyle\\",\\"lastName\\":\\"Shockey\\",\\"email\\":\\"kyle.shockey@smartbear.com\\"}`, - value: JSON.stringify( - { - firstName: "Kyle", - lastName: "Shockey", - email: "kyle.shockey@smartbear.com", - }, - null, - 2 - ), + serializedValue: exampleA, + value: exampleA, summary: "A user's contact info", }, exampleB: { key: "ObjectExampleB", - serializedValue: `{\\"name\\":\\"Abbey\\",\\"type\\":\\"kitten\\",\\"color\\":\\"calico\\",\\"gender\\":\\"female\\",\\"age\\":\\"11 weeks\\"}`, - value: JSON.stringify( - { - name: "Abbey", - type: "kitten", - color: "calico", - gender: "female", - age: "11 weeks", - }, - null, - 2 - ), + serializedValue: exampleB, + value: exampleB, summary: "A wonderful kitten's info", }, }) diff --git a/test/e2e-cypress/tests/features/oas3-request-body-required.js b/test/e2e-cypress/tests/features/oas3-request-body-required.js index 62390c62e31..d514f587fe2 100644 --- a/test/e2e-cypress/tests/features/oas3-request-body-required.js +++ b/test/e2e-cypress/tests/features/oas3-request-body-required.js @@ -95,7 +95,7 @@ describe("OpenAPI 3.0 Validation for Required Request Body and Request Body Fiel .get(".responses-wrapper .curl-command") .should("exist") .get(".responses-wrapper .curl-command span") - .should("contains.text", "\" \"") + .should("contains.text", "' '") }) }) diff --git a/test/mocha/components/live-response.jsx b/test/mocha/components/live-response.jsx index 0beef9fc123..b75de4bf6ab 100644 --- a/test/mocha/components/live-response.jsx +++ b/test/mocha/components/live-response.jsx @@ -3,9 +3,9 @@ import React from "react" import { fromJSOrdered } from "core/utils" import expect, { createSpy } from "expect" import { shallow } from "enzyme" -import Curl from "components/curl" import LiveResponse from "components/live-response" import ResponseBody from "components/response-body" +import { RequestSnippets } from "core/plugins/request-snippets/request-snippets" describe("", function () { let request = fromJSOrdered({ @@ -36,7 +36,7 @@ describe("", function () { ] tests.forEach(function (test) { - it("passes " + test.expected.request + " to Curl when showMutatedRequest = " + test.showMutatedRequest, function () { + it("passes " + test.expected.request + " to RequestSnippets when showMutatedRequest = " + test.showMutatedRequest, function () { // Given @@ -54,7 +54,7 @@ describe("", function () { let requestForSpy = createSpy().andReturn(request) let components = { - curl: Curl, + RequestSnippets: RequestSnippets, responseBody: ResponseBody } @@ -69,7 +69,7 @@ describe("", function () { return components[c] }, displayRequestDuration: true, - getConfigs: () => ({ showMutatedRequest: test.showMutatedRequest }) + getConfigs: () => ({ showMutatedRequest: test.showMutatedRequest, requestSnippetsEnabled: true }) } // When @@ -79,9 +79,9 @@ describe("", function () { expect(mutatedRequestForSpy.calls.length).toEqual(test.expected.mutatedRequestForCalls) expect(requestForSpy.calls.length).toEqual(test.expected.requestForCalls) - const curl = wrapper.find(Curl) - expect(curl.length).toEqual(1) - expect(curl.props().request).toBe(requests[test.expected.request]) + const snippets = wrapper.find("RequestSnippets") + expect(snippets.length).toEqual(1) + expect(snippets.props().request).toBe(requests[test.expected.request]) const expectedUrl = requests[test.expected.request].get("url") expect(wrapper.find("div.request-url pre.microlight").text()).toEqual(expectedUrl) diff --git a/test/unit/core/curlify.js b/test/unit/core/curlify.js index 620cd92749b..aa168d07a7c 100644 --- a/test/unit/core/curlify.js +++ b/test/unit/core/curlify.js @@ -1,18 +1,19 @@ import Im from "immutable" -import curl from "core/curlify" +import { requestSnippetGenerator_curl_bash as curl } from "core/plugins/request-snippets/fn.js" import win from "core/window" describe("curlify", function () { it("prints a curl statement with custom content-type", function () { + const body = JSON.stringify({ + id: 0, + name: "doggie", + status: "available" + }, null, 2) let req = { url: "http://example.com", method: "POST", - body: { - id: 0, - name: "doggie", - status: "available" - }, + body, headers: { Accept: "application/json", "content-type": "application/json" @@ -21,7 +22,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"Accept: application/json\" -H \"content-type: application/json\" -d {\"id\":0,\"name\":\"doggie\",\"status\":\"available\"}") + expect(curlified).toEqual(`curl -X 'POST' \\\n 'http://example.com' \\\n -H 'Accept: application/json' \\\n -H 'content-type: application/json' \\\n -d '${body}'`) }) it("does add a empty data param if no request body given", function () { @@ -32,7 +33,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -d \"\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -d ''") }) it("does not change the case of header in curl", function () { @@ -46,7 +47,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"conTenT Type: application/Moar\" -d \"\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'conTenT Type: application/Moar' \\\n -d ''") }) it("prints a curl statement with an array of query params", function () { @@ -57,7 +58,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X GET \"http://swaggerhub.com/v1/one?name=john|smith\"") + expect(curlified).toEqual("curl -X 'GET' \\\n 'http://swaggerhub.com/v1/one?name=john|smith'") }) it("prints a curl statement with an array of query params and auth", function () { @@ -71,24 +72,25 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X GET \"http://swaggerhub.com/v1/one?name=john|smith\" -H \"authorization: Basic Zm9vOmJhcg==\"") + expect(curlified).toEqual("curl -X 'GET' \\\n 'http://swaggerhub.com/v1/one?name=john|smith' \\\n -H 'authorization: Basic Zm9vOmJhcg=='") }) it("prints a curl statement with html", function () { + const body = { + description: "Test" + } let req = { url: "http://swaggerhub.com/v1/one?name=john|smith", method: "GET", headers: { accept: "application/json" }, - body: { - description: "Test" - } + body } let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X GET \"http://swaggerhub.com/v1/one?name=john|smith\" -H \"accept: application/json\" -d {\"description\":\"Test\"}") + expect(curlified).toEqual(`curl -X 'GET' \\\n 'http://swaggerhub.com/v1/one?name=john|smith' \\\n -H 'accept: application/json' \\\n -d '${JSON.stringify(body, null, 2)}'`) }) it("handles post body with html", function () { @@ -105,7 +107,12 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://swaggerhub.com/v1/one?name=john|smith\" -H \"accept: application/json\" -d {\"description\":\"Test\"}") + expect(curlified).toEqual(`curl -X 'POST' \\ + 'http://swaggerhub.com/v1/one?name=john|smith' \\ + -H 'accept: application/json' \\ + -d '{ + "description": "Test" +}'`) }) it("handles post body with special chars", function () { @@ -120,7 +127,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://swaggerhub.com/v1/one?name=john|smith\" -d {\"description\":\"@prefix nif: .@prefix itsrdf: .\"}") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://swaggerhub.com/v1/one?name=john|smith' \\\n -d '{\n \"description\": \"@prefix nif: .\\n@prefix itsrdf: .\"\n}'") }) it("handles delete form with parameters", function () { @@ -134,7 +141,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X DELETE \"http://example.com\" -H \"accept: application/x-www-form-urlencoded\"") + expect(curlified).toEqual("curl -X 'DELETE' \\\n 'http://example.com' \\\n -H 'accept: application/x-www-form-urlencoded'") }) it("should print a curl with formData", function () { @@ -150,7 +157,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"name=Sahar\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'content-type: multipart/form-data' \\\n -F 'id=123' \\\n -F 'name=Sahar'") }) it("should print a curl with formData that extracts array representation with hashIdx", function () { @@ -170,7 +177,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"fruits[]=apple\" -F \"fruits[]=banana\" -F \"fruits[]=grape\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'content-type: multipart/form-data' \\\n -F 'id=123' \\\n -F 'fruits[]=apple' \\\n -F 'fruits[]=banana' \\\n -F 'fruits[]=grape'") }) it("should print a curl with formData and file", function () { @@ -190,7 +197,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"file=@file.txt;type=text/plain\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'content-type: multipart/form-data' \\\n -F 'id=123' \\\n -F 'file=@file.txt;type=text/plain'") }) it("should print a curl without form data type if type is unknown", function () { @@ -210,7 +217,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"file=@file.txt\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'content-type: multipart/form-data' \\\n -F 'id=123' \\\n -F 'file=@file.txt'") }) it("prints a curl post statement from an object", function () { @@ -227,7 +234,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"accept: application/json\" -d {\"id\":10101}") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'accept: application/json' \\\n -d '{\n \"id\": 10101\n}'") }) it("prints a curl post statement from a string containing a single quote", function () { @@ -242,7 +249,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"accept: application/json\" -d \"{\\\"id\\\":\\\"foo'bar\\\"}\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'accept: application/json' \\\n -d '{\"id\":\"foo'\\''bar\"}'") }) describe("given multiple entries with file", function () { @@ -267,7 +274,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"x-custom-name: multipart/form-data\" -H \"content-type: multipart/form-data\" -F \"id=123\" -F \"file=@file.txt;type=text/plain\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'x-custom-name: multipart/form-data' \\\n -H 'content-type: multipart/form-data' \\\n -F 'id=123' \\\n -F 'file=@file.txt;type=text/plain'") }) }) @@ -292,7 +299,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"content-type: multipart/form-data\" -H \"x-custom-name: any-value\" -F \"id=123\" -F \"file=@file.txt;type=text/plain\"") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'content-type: multipart/form-data' \\\n -H 'x-custom-name: any-value' \\\n -F 'id=123' \\\n -F 'file=@file.txt;type=text/plain'") }) }) }) @@ -315,7 +322,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"x-custom-name: multipart/form-data\" -d {\"id\":\"123\",\"file\":{\"name\":\"file.txt\",\"type\":\"text/plain\"}}") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'x-custom-name: multipart/form-data' \\\n -d '{\n \"id\": \"123\",\n \"file\": {\n \"name\": \"file.txt\",\n \"type\": \"text/plain\"\n }\n}'") }) it("shoud print a proper curl as -d , no file type provided", function () { @@ -335,36 +342,10 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"x-custom-name: multipart/form-data\" -d {\"id\":\"123\",\"file\":{\"name\":\"file.txt\"}}") + expect(curlified).toEqual("curl -X 'POST' \\\n 'http://example.com' \\\n -H 'x-custom-name: multipart/form-data' \\\n -d '{\n \"id\": \"123\",\n \"file\": {\n \"name\": \"file.txt\"\n }\n}'") }) }) - it("should escape dollar signs in headers and request body", function () { - let req = { - url: "http://example.com", - method: "POST", - headers: { "X-DOLLAR": "token/123$" }, - body: "CREATE ($props)" - } - - let curlified = curl(Im.fromJS(req)) - - expect(curlified).toEqual("curl -X POST \"http://example.com\" -H \"X-DOLLAR: token/123\\$\" -d \"CREATE (\\$props)\"") - }) - - it("should escape multiple dollar signs", function () { - let req = { - url: "http://example.com", - method: "POST", - headers: { }, - body: "RETURN $x + $y" - } - - let curlified = curl(Im.fromJS(req)) - - expect(curlified).toEqual("curl -X POST \"http://example.com\" -d \"RETURN \\$x + \\$y\"") - }) - it("should include curlOptions from the request in the curl command", function () { let req = { url: "http://example.com", @@ -375,7 +356,7 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -g -X GET \"http://example.com\" -H \"X-DOLLAR: token/123\\$\"") + expect(curlified).toEqual("curl -g -X 'GET' \\\n 'http://example.com' \\\n -H 'X-DOLLAR: token/123$'") }) it("should include multiple curlOptions from the request in the curl command", function () { @@ -388,6 +369,6 @@ describe("curlify", function () { let curlified = curl(Im.fromJS(req)) - expect(curlified).toEqual("curl -g --limit-rate 20k -X GET \"http://example.com\" -H \"X-DOLLAR: token/123\\$\"") + expect(curlified).toEqual("curl -g --limit-rate 20k -X 'GET' \\\n 'http://example.com' \\\n -H 'X-DOLLAR: token/123$'") }) })