mirror of
https://github.com/GitbookIO/gitbook.git
synced 2026-09-12 05:48:57 +00:00
Adapt OpenAPI code samples to prefill API key using visitor data (#3666)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@gitbook/react-openapi": patch
|
||||
"gitbook": patch
|
||||
---
|
||||
|
||||
Adapt OpenAPI code samples to prefill API key using visitor data
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { DocumentBlockCode, JSONDocument } from '@gitbook/api';
|
||||
import type { JSONDocument } from '@gitbook/api';
|
||||
import { useId } from 'react';
|
||||
|
||||
import { CodeBlock } from './CodeBlock';
|
||||
import { convertCodeStringToBlock } from './utils';
|
||||
|
||||
/**
|
||||
* Plain code block with syntax highlighting.
|
||||
@@ -11,31 +12,7 @@ export function PlainCodeBlock(props: { code: string; syntax: string }) {
|
||||
const { code, syntax } = props;
|
||||
const id = useId();
|
||||
|
||||
const block: DocumentBlockCode = {
|
||||
key: id,
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: {
|
||||
syntax,
|
||||
},
|
||||
nodes: code.split('\n').map((line) => ({
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{
|
||||
object: 'leaf',
|
||||
text: line,
|
||||
marks: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})),
|
||||
};
|
||||
const block = convertCodeStringToBlock({ key: id, code, syntax });
|
||||
|
||||
const document: JSONDocument = {
|
||||
object: 'document',
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import { convertCodeStringToBlock } from './utils';
|
||||
|
||||
describe('convertCodeStringToBlock', () => {
|
||||
it('converts plain code string without placeholders', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test1',
|
||||
code: 'console.log("hello");',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test1',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'console.log("hello");', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('converts a single placeholder into an expression node', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test2',
|
||||
code: 'const config = { API_KEY: "$$__X-GITBOOK-PREFILL[visitor.claims.apiKey ?? "YOUR_API_KEY"]__$$" }',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test2',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{ object: 'leaf', text: 'const config = { API_KEY: "', marks: [] },
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.apiKey ?? "YOUR_API_KEY"' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: '" }', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple placeholders in one line', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test3',
|
||||
code: 'let a = $$__X-GITBOOK-PREFILL[valA]__$$, b = $$__X-GITBOOK-PREFILL[valB]__$$;',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test3',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'let a = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'valA' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ', b = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'valB' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ';', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple placeholders across different lines', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test4',
|
||||
code: [
|
||||
'const name = "$$__X-GITBOOK-PREFILL[visitor.claims.userName]__$$";',
|
||||
'const age = $$__X-GITBOOK-PREFILL[visitor.claims.userAge]__$$;',
|
||||
'console.log(name, age);',
|
||||
].join('\n'),
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test4',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'const name = "', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.userName' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: '";', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: 'const age = ', marks: [] }],
|
||||
},
|
||||
{
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression: 'visitor.claims.userAge' },
|
||||
isVoid: true,
|
||||
},
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: ';', marks: [] }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [
|
||||
{
|
||||
object: 'text',
|
||||
leaves: [
|
||||
{ object: 'leaf', text: 'console.log(name, age);', marks: [] },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns an empty code block for empty string', () => {
|
||||
const result = convertCodeStringToBlock({
|
||||
key: 'test5',
|
||||
code: '',
|
||||
syntax: 'javascript',
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
key: 'test5',
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax: 'javascript' },
|
||||
nodes: [
|
||||
{
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { DocumentBlockCode, DocumentBlockCodeLine } from '@gitbook/api';
|
||||
|
||||
const PREFILL_WITH_EXPR_REGEX = /\$\$__X-GITBOOK-PREFILL\[(.+?)\]__\$\$/g;
|
||||
|
||||
/**
|
||||
* Convert a raw code string into a `DocumentBlockCode` object representation.
|
||||
*
|
||||
* Any placeholder of the form `$$__X-GITBOOK-PREFILL[<expression>]__$$` inside the code
|
||||
* string is transformed into a DocumentInlineExpression node with its `data.expression` set to the
|
||||
* extracted `<expression>`.
|
||||
*/
|
||||
export function convertCodeStringToBlock(args: {
|
||||
key: string;
|
||||
code: string;
|
||||
syntax: string;
|
||||
}): DocumentBlockCode {
|
||||
const { key, code, syntax } = args;
|
||||
const lines = code.split('\n').map<DocumentBlockCodeLine>((line) => {
|
||||
const nodes: DocumentBlockCodeLine['nodes'] = [];
|
||||
let lastIndex = 0;
|
||||
|
||||
for (const match of line.matchAll(PREFILL_WITH_EXPR_REGEX)) {
|
||||
const [placeholder, expression] = match;
|
||||
const start = match.index ?? 0;
|
||||
|
||||
if (start > lastIndex) {
|
||||
nodes.push({
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: line.slice(lastIndex, start), marks: [] }],
|
||||
});
|
||||
}
|
||||
|
||||
if (expression) {
|
||||
nodes.push({
|
||||
object: 'inline',
|
||||
type: 'expression',
|
||||
data: { expression },
|
||||
isVoid: true,
|
||||
});
|
||||
}
|
||||
|
||||
lastIndex = start + placeholder.length;
|
||||
}
|
||||
|
||||
if (lastIndex < line.length) {
|
||||
nodes.push({
|
||||
object: 'text',
|
||||
leaves: [{ object: 'leaf', text: line.slice(lastIndex), marks: [] }],
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
object: 'block',
|
||||
type: 'code-line',
|
||||
data: {},
|
||||
nodes,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
key,
|
||||
object: 'block',
|
||||
type: 'code',
|
||||
data: { syntax },
|
||||
nodes: lines,
|
||||
};
|
||||
}
|
||||
@@ -454,7 +454,7 @@ export function serveVisitorClaimsDataRequest(request: NextRequest, siteRequestU
|
||||
});
|
||||
|
||||
if (!visitorToken && !Object.keys(unsignedClaims).length) {
|
||||
return NextResponse.json({});
|
||||
return NextResponse.json({ visitor: { claims: { unsigned: {} } } });
|
||||
}
|
||||
|
||||
const visitorClaims = {
|
||||
|
||||
@@ -5,12 +5,16 @@ import {
|
||||
} from './OpenAPICodeSampleInteractive';
|
||||
import { OpenAPICodeSampleBody } from './OpenAPICodeSampleSelector';
|
||||
import { ScalarApiButton } from './ScalarApiButton';
|
||||
import { type CodeSampleGenerator, codeSampleGenerators } from './code-samples';
|
||||
import { type CodeSampleGenerator, codeSampleGenerators, parseHostAndPath } from './code-samples';
|
||||
import { type OpenAPIContext, getOpenAPIClientContext } from './context';
|
||||
import { generateMediaTypeExamples, generateSchemaExample } from './generateSchemaExample';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import type { OpenAPIOperationData } from './types';
|
||||
import { getDefaultServerURL } from './util/server';
|
||||
import {
|
||||
resolvePrefillCodePlaceholderFromSecurityScheme,
|
||||
resolveURLWithPrefillCodePlaceholdersFromServer,
|
||||
} from './util/tryit-prefill';
|
||||
import { checkIsReference, extractOperationSecurityInfo } from './utils';
|
||||
|
||||
const CUSTOM_CODE_SAMPLES_KEYS = ['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const;
|
||||
@@ -100,10 +104,15 @@ function generateCodeSamples(props: {
|
||||
? data.operation.requestBody
|
||||
: undefined;
|
||||
|
||||
const url =
|
||||
getDefaultServerURL(data.servers) +
|
||||
data.path +
|
||||
(searchParams.size ? `?${searchParams.toString()}` : '');
|
||||
const defaultServerUrl = getDefaultServerURL(data.servers);
|
||||
let serverUrlPath = defaultServerUrl ? parseHostAndPath(defaultServerUrl).path : '';
|
||||
serverUrlPath = serverUrlPath === '/' ? '' : serverUrlPath;
|
||||
const serverUrl = data.servers[0]
|
||||
? resolveURLWithPrefillCodePlaceholdersFromServer(data.servers[0], defaultServerUrl)
|
||||
: defaultServerUrl;
|
||||
const serverUrlOrigin = serverUrl.replaceAll(serverUrlPath, '');
|
||||
const path =
|
||||
serverUrlPath + data.path + (searchParams.size ? `?${searchParams.toString()}` : '');
|
||||
|
||||
const genericHeaders = {
|
||||
...getSecurityHeaders({
|
||||
@@ -124,7 +133,7 @@ function generateCodeSamples(props: {
|
||||
mediaType,
|
||||
element: context.renderCodeBlock({
|
||||
code: generator.generate({
|
||||
url,
|
||||
url: { origin: serverUrlOrigin, path },
|
||||
method: data.method,
|
||||
body: undefined,
|
||||
headers: mediaTypeHeaders,
|
||||
@@ -137,7 +146,7 @@ function generateCodeSamples(props: {
|
||||
example,
|
||||
element: context.renderCodeBlock({
|
||||
code: generator.generate({
|
||||
url,
|
||||
url: { origin: serverUrlOrigin, path },
|
||||
method: data.method,
|
||||
body: example.value,
|
||||
headers: mediaTypeHeaders,
|
||||
@@ -174,7 +183,7 @@ function generateCodeSamples(props: {
|
||||
label: generator.label,
|
||||
body: context.renderCodeBlock({
|
||||
code: generator.generate({
|
||||
url,
|
||||
url: { origin: serverUrlOrigin, path },
|
||||
method: data.method,
|
||||
body: undefined,
|
||||
headers: genericHeaders,
|
||||
@@ -306,13 +315,17 @@ function getSecurityHeaders(args: {
|
||||
switch (security.type) {
|
||||
case 'http': {
|
||||
let scheme = security.scheme;
|
||||
let format = security.bearerFormat ?? 'YOUR_SECRET_TOKEN';
|
||||
const format = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: security,
|
||||
defaultPlaceholderValue: scheme?.includes('basic')
|
||||
? 'username:password'
|
||||
: 'YOUR_SECRET_TOKEN',
|
||||
});
|
||||
|
||||
if (scheme?.includes('bearer')) {
|
||||
scheme = 'Bearer';
|
||||
} else if (scheme?.includes('basic')) {
|
||||
scheme = 'Basic';
|
||||
format = 'username:password';
|
||||
} else if (scheme?.includes('token')) {
|
||||
scheme = 'Token';
|
||||
}
|
||||
@@ -324,14 +337,18 @@ function getSecurityHeaders(args: {
|
||||
if (security.in !== 'header') {
|
||||
break;
|
||||
}
|
||||
|
||||
const name = security.name ?? 'Authorization';
|
||||
headers[name] = 'YOUR_API_KEY';
|
||||
|
||||
headers[name] = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: security,
|
||||
defaultPlaceholderValue: 'YOUR_API_KEY',
|
||||
});
|
||||
break;
|
||||
}
|
||||
case 'oauth2': {
|
||||
headers.Authorization = 'Bearer YOUR_OAUTH2_TOKEN';
|
||||
headers.Authorization = `Bearer ${resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: security,
|
||||
defaultPlaceholderValue: 'YOUR_OAUTH2_TOKEN',
|
||||
})}`;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
@@ -339,7 +356,6 @@ function getSecurityHeaders(args: {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ import { OpenAPICopyButton } from './OpenAPICopyButton';
|
||||
import { OpenAPISchemaName } from './OpenAPISchemaName';
|
||||
import type { OpenAPIClientContext } from './context';
|
||||
import { t } from './translate';
|
||||
import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types';
|
||||
import type { OpenAPISecuritySchemeWithRequired } from './types';
|
||||
import type { OpenAPIOperationData } from './types';
|
||||
import { createStateKey, extractOperationSecurityInfo, resolveDescription } from './utils';
|
||||
|
||||
/**
|
||||
@@ -62,7 +63,10 @@ export function OpenAPISecurities(props: {
|
||||
);
|
||||
}
|
||||
|
||||
function getLabelForType(security: OpenAPISecurityWithRequired, context: OpenAPIClientContext) {
|
||||
function getLabelForType(
|
||||
security: OpenAPISecuritySchemeWithRequired,
|
||||
context: OpenAPIClientContext
|
||||
) {
|
||||
switch (security.type) {
|
||||
case 'apiKey':
|
||||
return (
|
||||
|
||||
@@ -50,6 +50,8 @@ it('should parse host and path on url strings properly', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
const exampleEndpointUrl = new URL('https://example.com/path');
|
||||
|
||||
describe('curL code sample generator', () => {
|
||||
const generator = codeSampleGenerators.find((g) => g.id === 'curl');
|
||||
|
||||
@@ -58,7 +60,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format application/x-www-form-urlencoded body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -77,7 +79,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format application/json body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -96,7 +98,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format application/xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -113,7 +115,7 @@ describe('curL code sample generator', () => {
|
||||
it('should convert json to xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -130,7 +132,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format application/graphql body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
@@ -147,7 +149,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format text/csv body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
},
|
||||
@@ -164,7 +166,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format application/pdf body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
},
|
||||
@@ -181,7 +183,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format text/plain body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
@@ -198,7 +200,7 @@ describe('curL code sample generator', () => {
|
||||
it('should format multipart/form-data body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -223,7 +225,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format application/x-www-form-urlencoded body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -242,7 +244,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format application/json body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -261,7 +263,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format application/xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -278,7 +280,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should convert json to xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -295,7 +297,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format application/graphql body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
@@ -312,7 +314,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format text/csv body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
},
|
||||
@@ -329,7 +331,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format application/pdf body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
},
|
||||
@@ -346,7 +348,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format text/plain body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
@@ -363,7 +365,7 @@ describe('javascript code sample generator', () => {
|
||||
it('should format multipart/form-data body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -388,7 +390,7 @@ describe('python code sample generator', () => {
|
||||
it('should format application/x-www-form-urlencoded body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -407,7 +409,7 @@ describe('python code sample generator', () => {
|
||||
it('should format application/json body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'POST',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -429,7 +431,7 @@ describe('python code sample generator', () => {
|
||||
it('should format application/xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -446,7 +448,7 @@ describe('python code sample generator', () => {
|
||||
it('should convert json to xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -463,7 +465,7 @@ describe('python code sample generator', () => {
|
||||
it('should format application/graphql body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
@@ -480,7 +482,7 @@ describe('python code sample generator', () => {
|
||||
it('should format text/csv body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
},
|
||||
@@ -497,7 +499,7 @@ describe('python code sample generator', () => {
|
||||
it('should format application/pdf body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
},
|
||||
@@ -514,7 +516,7 @@ describe('python code sample generator', () => {
|
||||
it('should format text/plain body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
@@ -531,7 +533,7 @@ describe('python code sample generator', () => {
|
||||
it('should format multipart/form-data body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
@@ -556,7 +558,7 @@ describe('http code sample generator', () => {
|
||||
it('should format application/x-www-form-urlencoded body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
@@ -575,7 +577,7 @@ describe('http code sample generator', () => {
|
||||
it('should format application/json body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
@@ -594,7 +596,7 @@ describe('http code sample generator', () => {
|
||||
it('should format application/xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -611,7 +613,7 @@ describe('http code sample generator', () => {
|
||||
it('should convert json to xml body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/xml',
|
||||
},
|
||||
@@ -628,7 +630,7 @@ describe('http code sample generator', () => {
|
||||
it('should format application/graphql body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/graphql',
|
||||
},
|
||||
@@ -645,7 +647,7 @@ describe('http code sample generator', () => {
|
||||
it('should format text/csv body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/csv',
|
||||
},
|
||||
@@ -662,7 +664,7 @@ describe('http code sample generator', () => {
|
||||
it('should format application/pdf body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'application/pdf',
|
||||
},
|
||||
@@ -679,7 +681,7 @@ describe('http code sample generator', () => {
|
||||
it('should format text/plain body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'text/plain',
|
||||
},
|
||||
@@ -696,7 +698,7 @@ describe('http code sample generator', () => {
|
||||
it('should format multipart/form-data body properly', () => {
|
||||
const input: CodeSampleInput = {
|
||||
method: 'GET',
|
||||
url: 'https://example.com/path',
|
||||
url: { origin: exampleEndpointUrl.origin, path: exampleEndpointUrl.pathname },
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
},
|
||||
|
||||
@@ -16,7 +16,10 @@ import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
|
||||
export interface CodeSampleInput {
|
||||
method: string;
|
||||
url: string;
|
||||
url: {
|
||||
origin: string;
|
||||
path: string;
|
||||
};
|
||||
headers?: Record<string, string>;
|
||||
body?: any;
|
||||
}
|
||||
@@ -33,9 +36,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
||||
id: 'http',
|
||||
label: 'HTTP',
|
||||
syntax: 'http',
|
||||
generate: ({ method, url, headers = {}, body }: CodeSampleInput) => {
|
||||
const { host, path } = parseHostAndPath(url);
|
||||
|
||||
generate: ({ method, url: { origin, path }, headers = {}, body }: CodeSampleInput) => {
|
||||
if (body) {
|
||||
// if we had a body add a content length header
|
||||
const bodyContent = body ? stringifyOpenAPI(body) : '';
|
||||
@@ -69,7 +70,7 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
|
||||
const bodyString = body ? `\n${body}` : '';
|
||||
|
||||
const httpRequest = `${method.toUpperCase()} ${decodeURI(path)} HTTP/1.1
|
||||
Host: ${host}
|
||||
Host: ${origin.replaceAll(/https*:\/\//g, '')}
|
||||
${headerString}${bodyString}`;
|
||||
|
||||
return httpRequest;
|
||||
@@ -79,7 +80,7 @@ ${headerString}${bodyString}`;
|
||||
id: 'curl',
|
||||
label: 'cURL',
|
||||
syntax: 'bash',
|
||||
generate: ({ method, url, headers, body }) => {
|
||||
generate: ({ method, url: { origin, path }, headers, body }) => {
|
||||
const separator = ' \\\n';
|
||||
|
||||
const lines: string[] = ['curl -L'];
|
||||
@@ -88,7 +89,7 @@ ${headerString}${bodyString}`;
|
||||
lines.push(`--request ${method.toUpperCase()}`);
|
||||
}
|
||||
|
||||
lines.push(`--url '${url}'`);
|
||||
lines.push(`--url '${origin}${path}'`);
|
||||
|
||||
if (body) {
|
||||
const bodyContent = BodyGenerators.getCurlBody(body, headers);
|
||||
@@ -120,7 +121,7 @@ ${headerString}${bodyString}`;
|
||||
id: 'javascript',
|
||||
label: 'JavaScript',
|
||||
syntax: 'javascript',
|
||||
generate: ({ method, url, headers, body }) => {
|
||||
generate: ({ method, url: { origin, path }, headers, body }) => {
|
||||
let code = '';
|
||||
|
||||
if (body) {
|
||||
@@ -134,7 +135,7 @@ ${headerString}${bodyString}`;
|
||||
}
|
||||
}
|
||||
|
||||
code += `const response = await fetch('${url}', {
|
||||
code += `const response = await fetch('${origin}${path}', {
|
||||
method: '${method.toUpperCase()}',\n`;
|
||||
|
||||
if (headers && Object.keys(headers).length > 0) {
|
||||
@@ -155,7 +156,7 @@ ${headerString}${bodyString}`;
|
||||
id: 'python',
|
||||
label: 'Python',
|
||||
syntax: 'python',
|
||||
generate: ({ method, url, headers, body }) => {
|
||||
generate: ({ method, url: { origin, path }, headers, body }) => {
|
||||
const contentType = headers?.['Content-Type'];
|
||||
let code = `${isJSON(contentType) ? 'import json\n' : ''}import requests\n\n`;
|
||||
|
||||
@@ -171,7 +172,7 @@ ${headerString}${bodyString}`;
|
||||
}
|
||||
|
||||
code += `response = requests.${method.toLowerCase()}(\n`;
|
||||
code += indent(`"${url}",\n`, 4);
|
||||
code += indent(`"${origin}${path}",\n`, 4);
|
||||
|
||||
if (headers && Object.keys(headers).length > 0) {
|
||||
code += indent(`headers=${stringifyOpenAPI(headers)},\n`, 4);
|
||||
|
||||
@@ -17,7 +17,7 @@ export type OpenAPIServerWithCustomProperties = Omit<OpenAPIV3.ServerObject, 'va
|
||||
};
|
||||
} & OpenAPICustomPrefillProperties;
|
||||
|
||||
export type OpenAPISecurityWithRequired = OpenAPIV3.SecuritySchemeObject &
|
||||
export type OpenAPISecuritySchemeWithRequired = OpenAPIV3.SecuritySchemeObject &
|
||||
OpenAPICustomPrefillProperties & { required?: boolean };
|
||||
|
||||
export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
@@ -31,7 +31,7 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
|
||||
operation: OpenAPIV3.OperationObject<OpenAPICustomOperationProperties>;
|
||||
|
||||
/** Securities that should be used for this operation */
|
||||
securities: [string, OpenAPISecurityWithRequired][];
|
||||
securities: [string, OpenAPISecuritySchemeWithRequired][];
|
||||
}
|
||||
|
||||
export interface OpenAPIWebhookData extends OpenAPICustomSpecProperties {
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { describe, expect, it } from 'bun:test';
|
||||
import type { PrefillInputContextData } from '../OpenAPIPrefillContextProvider';
|
||||
import type { OpenAPIOperationData } from '../types';
|
||||
import { resolveTryItPrefillForOperation } from './tryit-prefill';
|
||||
import {
|
||||
resolvePrefillCodePlaceholderFromSecurityScheme,
|
||||
resolveTryItPrefillForOperation,
|
||||
resolveURLWithPrefillCodePlaceholdersFromServer,
|
||||
} from './tryit-prefill';
|
||||
|
||||
describe('resolveTryItPrefillForOperation', () => {
|
||||
describe('prefill authentication info', () => {
|
||||
@@ -309,3 +313,166 @@ describe('resolveTryItPrefillForOperation', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolvePrefillCodePlaceholderFromSecurityScheme (integration style)', () => {
|
||||
it('should return placeholder for bearer token scheme', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.apiToken }}',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.apiToken)]__$$');
|
||||
});
|
||||
|
||||
it('should return placeholder for basic auth scheme', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'http',
|
||||
scheme: 'basic',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.basicAuth }}',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.basicAuth)]__$$');
|
||||
});
|
||||
|
||||
it('should build placeholder for apiKey scheme', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'apiKey',
|
||||
in: 'header',
|
||||
name: 'X-API-KEY',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.apiKey }}',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('$$__X-GITBOOK-PREFILL[(visitor.claims.apiKey)]__$$');
|
||||
});
|
||||
|
||||
it('should return placeholder with default value if provided', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.missing }}',
|
||||
},
|
||||
defaultPlaceholderValue: 'YOUR_API_TOKEN',
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`$$__X-GITBOOK-PREFILL[(visitor.claims.missing) ?? 'YOUR_API_TOKEN']__$$`
|
||||
);
|
||||
});
|
||||
|
||||
it('should concatenate text and expression in prefill', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
'x-gitbook-prefill': 'Bearer {{ visitor.claims.apiToken }}',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('$$__X-GITBOOK-PREFILL[("Bearer " + visitor.claims.apiToken)]__$$');
|
||||
});
|
||||
|
||||
it('should handle multiple expressions in prefill', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'http',
|
||||
scheme: 'basic',
|
||||
'x-gitbook-prefill': '{{ visitor.claims.username }}:{{ visitor.claims.password }}',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
'$$__X-GITBOOK-PREFILL[(visitor.claims.username + ":" + visitor.claims.password)]__$$'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return empty default value if no prefill property exists', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
},
|
||||
defaultPlaceholderValue: 'YOUR_API_TOKEN',
|
||||
});
|
||||
|
||||
expect(result).toBe('YOUR_API_TOKEN');
|
||||
});
|
||||
|
||||
it('should return empty string if no prefill property exists', () => {
|
||||
const result = resolvePrefillCodePlaceholderFromSecurityScheme({
|
||||
security: {
|
||||
type: 'http',
|
||||
scheme: 'bearer',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveURLWithPrefillCodePlaceholdersFromServer', () => {
|
||||
it('should return a simple URL when no prefills are present', () => {
|
||||
const result = resolveURLWithPrefillCodePlaceholdersFromServer({
|
||||
url: 'https://api.example.com/v1',
|
||||
});
|
||||
|
||||
expect(result).toBe('https://api.example.com/v1');
|
||||
});
|
||||
|
||||
it('should replace a variable with its default when no prefill is set', () => {
|
||||
const result = resolveURLWithPrefillCodePlaceholdersFromServer({
|
||||
url: 'https://{region}.example.com',
|
||||
variables: {
|
||||
region: { default: 'us-east-1' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe('https://us-east-1.example.com');
|
||||
});
|
||||
|
||||
it('should return a placeholder for variable-level prefill only', () => {
|
||||
const result = resolveURLWithPrefillCodePlaceholdersFromServer({
|
||||
url: 'https://{region}.example.com',
|
||||
variables: {
|
||||
region: { default: 'us-east-1', 'x-gitbook-prefill': '{{ user.region }}' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
`$$__X-GITBOOK-PREFILL[(\`https://\${(user.region ?? 'us-east-1')}.example.com\`)]__$$`
|
||||
);
|
||||
});
|
||||
|
||||
it('should wrap full URL when URL-level prefill exists', () => {
|
||||
const result = resolveURLWithPrefillCodePlaceholdersFromServer({
|
||||
url: 'https://api.example.com/v1',
|
||||
'x-gitbook-prefill': '{{ user.baseUrl }}',
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
"$$__X-GITBOOK-PREFILL[(user.baseUrl ?? 'https://api.example.com/v1')]__$$"
|
||||
);
|
||||
});
|
||||
|
||||
it('should combine variable-level and URL-level prefills correctly', () => {
|
||||
const result = resolveURLWithPrefillCodePlaceholdersFromServer({
|
||||
url: 'https://{region}.example.com/{version}',
|
||||
'x-gitbook-prefill': '{{ user.baseUrl }}',
|
||||
variables: {
|
||||
region: { default: 'us-east-1', 'x-gitbook-prefill': '{{ user.region }}' },
|
||||
version: { default: 'v1' },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toBe(
|
||||
"$$__X-GITBOOK-PREFILL[(user.baseUrl ?? `https://${(user.region ?? 'us-east-1')}.example.com/v1`)]__$$"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import { ExpressionRuntime, parseTemplate } from '@gitbook/expr';
|
||||
import { ExpressionRuntime, type TemplatePart, parseTemplate } from '@gitbook/expr';
|
||||
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
|
||||
import type { ApiClientConfiguration } from '@scalar/types';
|
||||
import type { PrefillInputContextData } from '../OpenAPIPrefillContextProvider';
|
||||
import type { OpenAPIOperationData } from '../types';
|
||||
import type {
|
||||
OpenAPIOperationData,
|
||||
OpenAPISecuritySchemeWithRequired,
|
||||
OpenAPIServerWithCustomProperties,
|
||||
} from '../types';
|
||||
|
||||
export interface TryItPrefillConfiguration {
|
||||
authentication?: ApiClientConfiguration['authentication'];
|
||||
servers?: ApiClientConfiguration['servers'];
|
||||
}
|
||||
|
||||
export const PREFILL_CUSTOM_PROPERTY = 'x-gitbook-prefill';
|
||||
|
||||
/**
|
||||
* Resolve the Scalar API client prefill configuration for a given OpenAPI operation.
|
||||
*/
|
||||
@@ -68,8 +74,8 @@ function resolveTryItPrefillAuthForOperationSecurities(args: {
|
||||
const prefillAuthConfig: ApiClientConfiguration['authentication']['securitySchemes'] = {};
|
||||
|
||||
for (const [schemeName, security] of Object.values(securities)) {
|
||||
const tryitPrefillAuthValue = security['x-gitbook-prefill']
|
||||
? resolveTryItPrefillExpression(security['x-gitbook-prefill'])
|
||||
const tryitPrefillAuthValue = security[PREFILL_CUSTOM_PROPERTY]
|
||||
? resolveTryItPrefillExpression(security[PREFILL_CUSTOM_PROPERTY])
|
||||
: undefined;
|
||||
|
||||
if (!tryitPrefillAuthValue) {
|
||||
@@ -121,7 +127,7 @@ function resolveTryItPrefillServersForOperationServers(args: {
|
||||
|
||||
for (const server of servers) {
|
||||
// Url-level prefill
|
||||
const tryItPrefillServerUrlExpr = server['x-gitbook-prefill'];
|
||||
const tryItPrefillServerUrlExpr = server[PREFILL_CUSTOM_PROPERTY];
|
||||
const tryItPrefillServerUrlValue = tryItPrefillServerUrlExpr
|
||||
? resolveTryItPrefillExpression(tryItPrefillServerUrlExpr)
|
||||
: undefined;
|
||||
@@ -133,7 +139,8 @@ function resolveTryItPrefillServersForOperationServers(args: {
|
||||
// Variable-level prefill
|
||||
if (server.variables) {
|
||||
for (const [varName, variable] of Object.entries(server.variables)) {
|
||||
const { 'x-gitbook-prefill': tryItPrefillVarExpr, ...variableProps } = variable;
|
||||
const { [PREFILL_CUSTOM_PROPERTY]: tryItPrefillVarExpr, ...variableProps } =
|
||||
variable;
|
||||
|
||||
const tryItPrefillVarValue = tryItPrefillVarExpr
|
||||
? resolveTryItPrefillExpression(tryItPrefillVarExpr)
|
||||
@@ -158,3 +165,102 @@ function resolveTryItPrefillServersForOperationServers(args: {
|
||||
|
||||
return resolvedServers.length > 0 ? resolvedServers : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a X-GITBOOK-PREFILL placeholder based on the prefill custom property in the provided security scheme.
|
||||
*/
|
||||
export function resolvePrefillCodePlaceholderFromSecurityScheme(args: {
|
||||
security: OpenAPISecuritySchemeWithRequired;
|
||||
defaultPlaceholderValue?: string;
|
||||
}) {
|
||||
const { security, defaultPlaceholderValue } = args;
|
||||
const prefillExprParts = extractPrefillExpressionPartsFromSecurityScheme(security);
|
||||
|
||||
if (prefillExprParts.length === 0) {
|
||||
return defaultPlaceholderValue ?? '';
|
||||
}
|
||||
const prefillExpr = templatePartsToExpression(prefillExprParts);
|
||||
|
||||
return toPrefillCodePlaceholder(prefillExpr, defaultPlaceholderValue);
|
||||
}
|
||||
|
||||
function extractPrefillExpressionPartsFromSecurityScheme(
|
||||
security: OpenAPISecuritySchemeWithRequired
|
||||
): TemplatePart[] {
|
||||
const expression = security[PREFILL_CUSTOM_PROPERTY];
|
||||
|
||||
if (!expression || expression.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return parseTemplate(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a server URL with X-GITBOOK-PREFILL placeholders based on the prefill custom properties in the provided security scheme.
|
||||
*/
|
||||
export function resolveURLWithPrefillCodePlaceholdersFromServer(
|
||||
server: OpenAPIServerWithCustomProperties,
|
||||
defaultServerUrl?: string
|
||||
): string {
|
||||
const serverVariables = server.variables ?? {};
|
||||
const variableExprs: Record<string, string> = {};
|
||||
let hasVariablePrefill = false;
|
||||
|
||||
for (const [name, variable] of Object.entries(serverVariables ?? {})) {
|
||||
if (variable[PREFILL_CUSTOM_PROPERTY]) {
|
||||
hasVariablePrefill = true;
|
||||
const exprString = templatePartsToExpression(
|
||||
parseTemplate(variable[PREFILL_CUSTOM_PROPERTY])
|
||||
);
|
||||
variableExprs[name] = `(${exprString} ?? '${variable.default ?? ''}')`;
|
||||
} else {
|
||||
variableExprs[name] = String(variable.default) ?? '';
|
||||
}
|
||||
}
|
||||
|
||||
let interpolatedUrl = server.url ?? '';
|
||||
interpolatedUrl = interpolatedUrl.replace(/{([^}]+)}/g, (_, varName: string) => {
|
||||
const expr = variableExprs[varName];
|
||||
if (serverVariables[varName]?.[PREFILL_CUSTOM_PROPERTY]) {
|
||||
return `\${${expr ?? `'${varName}'`}}`;
|
||||
}
|
||||
return expr ?? `{${varName}}`;
|
||||
});
|
||||
|
||||
const interpolatedUrlTemplate = hasVariablePrefill ? `\`${interpolatedUrl}\`` : interpolatedUrl;
|
||||
|
||||
const urlLevelExpr = server[PREFILL_CUSTOM_PROPERTY];
|
||||
if (urlLevelExpr) {
|
||||
const exprString = templatePartsToExpression(parseTemplate(urlLevelExpr));
|
||||
const defaultValue = hasVariablePrefill
|
||||
? interpolatedUrlTemplate
|
||||
: `'${interpolatedUrlTemplate}'`;
|
||||
return toPrefillCodePlaceholder(`${exprString} ?? ${defaultValue}`, defaultServerUrl);
|
||||
}
|
||||
|
||||
if (hasVariablePrefill) {
|
||||
return toPrefillCodePlaceholder(interpolatedUrlTemplate, defaultServerUrl);
|
||||
}
|
||||
|
||||
return interpolatedUrl;
|
||||
}
|
||||
|
||||
function templatePartsToExpression(parts: ReturnType<typeof parseTemplate>) {
|
||||
return parts
|
||||
.map((part) => {
|
||||
switch (part.type) {
|
||||
case 'text':
|
||||
return `"${part.value}"`;
|
||||
case 'expression':
|
||||
return part.value;
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.join(' + ');
|
||||
}
|
||||
|
||||
function toPrefillCodePlaceholder(expression: string, defaultValue?: string) {
|
||||
return `$$__X-GITBOOK-PREFILL[(${expression})${defaultValue ? ` ?? '${defaultValue}'` : ''}]__$$`;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser'
|
||||
import type { OpenAPIUniversalContext } from './context';
|
||||
import { stringifyOpenAPI } from './stringifyOpenAPI';
|
||||
import { tString } from './translate';
|
||||
import type { OpenAPIOperationData, OpenAPISecurityWithRequired } from './types';
|
||||
import type { OpenAPIOperationData, OpenAPISecuritySchemeWithRequired } from './types';
|
||||
|
||||
export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
|
||||
return typeof input === 'object' && !!input && '$ref' in input;
|
||||
@@ -258,7 +258,7 @@ export function getSchemaTitle(schema: OpenAPIV3.SchemaObject): string {
|
||||
export type OperationSecurityInfo = {
|
||||
key: string;
|
||||
label: string;
|
||||
schemes: OpenAPISecurityWithRequired[];
|
||||
schemes: OpenAPISecuritySchemeWithRequired[];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user