diff --git a/.changeset/cold-wombats-chew.md b/.changeset/cold-wombats-chew.md new file mode 100644 index 000000000..439633767 --- /dev/null +++ b/.changeset/cold-wombats-chew.md @@ -0,0 +1,5 @@ +--- +'@gitbook/react-openapi': minor +--- + +Add in HTTP example code blocks diff --git a/packages/react-openapi/src/code-samples.test.ts b/packages/react-openapi/src/code-samples.test.ts new file mode 100644 index 000000000..6d2aa1302 --- /dev/null +++ b/packages/react-openapi/src/code-samples.test.ts @@ -0,0 +1,51 @@ +import { it, expect } from 'bun:test'; +import { parseHostAndPath } from './code-samples'; + +it('should parse host and path on url strings properly', () => { + const testUrls = [ + '//example.com/path', + '//sub.example.com', + '//example:8080/v1/test', + 'ftp://domain.com', + '//example.com/com.example', + 'https://example.com/path.com/another.com', + 'example.com/firstPath/secondPath', + ]; + + expect(testUrls.map(parseHostAndPath)).toEqual([ + { + host: 'example.com', + path: '/path', + }, + + { + host: 'sub.example.com', + path: '/', + }, + + { + host: 'example:8080', + path: '/v1/test', + }, + + { + host: 'domain.com', + path: '/', + }, + + { + host: 'example.com', + path: '/com.example', + }, + + { + host: 'example.com', + path: '/path.com/another.com', + }, + + { + host: 'example.com', + path: '/firstPath/secondPath', + }, + ]); +}); diff --git a/packages/react-openapi/src/code-samples.ts b/packages/react-openapi/src/code-samples.ts index 23e4bb60b..fd921ce8a 100644 --- a/packages/react-openapi/src/code-samples.ts +++ b/packages/react-openapi/src/code-samples.ts @@ -84,6 +84,46 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [ return code; }, }, + { + id: 'http', + label: 'HTTP', + syntax: 'bash', + generate: ({ method, url, headers = {}, body }: CodeSampleInput) => { + const { host, path } = parseHostAndPath(url); + + if (body) { + // if we had a body add a content length header + const bodyContent = body ? JSON.stringify(body) : ''; + // handle unicode chars with a text encoder + const encoder = new TextEncoder(); + + headers = { + ...headers, + 'Content-Length': encoder.encode(bodyContent).length.toString(), + }; + } + + if (!headers.hasOwnProperty('Accept')) { + headers.Accept = '*/*'; + } + + const headerString = headers + ? Object.entries(headers) + .map(([key, value]) => + key.toLowerCase() !== 'host' ? `${key}: ${value}` : ``, + ) + .join('\n') + '\n' + : ''; + + const bodyString = body ? `\n${JSON.stringify(body, null, 2)}` : ''; + + const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1 +Host: ${host} +${headerString}${bodyString}`; + + return httpRequest; + }, + }, ]; function indent(code: string, spaces: number) { @@ -93,3 +133,24 @@ function indent(code: string, spaces: number) { .map((line) => (line ? indent + line : '')) .join('\n'); } + +export function parseHostAndPath(url: string) { + try { + const urlObj = new URL(url); + const path = urlObj.pathname || '/'; + return { host: urlObj.host, path }; + } catch (e) { + // If the URL was invalid do our best to parse the URL. + // Check for the protocol part and pull it off to grab the host + const fullUrl = url.match(/\/\//) ? url.split('//')[1] : url; + + // separate paths from the first element (host) + const parts = fullUrl.split('/'); + // pull off the host (mutates) + const host = parts.shift(); + // add a leading slash and join the paths again + const path = '/' + parts.join('/'); + + return { host, path }; + } +}