@gitbook/react-openapi 0.5.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import * as React from 'react';
2
2
 
3
3
  import { CodeSampleInput, codeSampleGenerators } from './code-samples';
4
- import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
5
- import { generateMediaTypeExample } from './generateSchemaExample';
4
+ import { OpenAPIOperationData } from './fetchOpenAPIOperation';
5
+ import { generateMediaTypeExample, generateSchemaExample } from './generateSchemaExample';
6
6
  import { InteractiveSection } from './InteractiveSection';
7
7
  import { getServersURL } from './OpenAPIServerURL';
8
8
  import { ScalarApiButton } from './ScalarApiButton';
@@ -19,17 +19,51 @@ export function OpenAPICodeSample(props: {
19
19
  }) {
20
20
  const { data, context } = props;
21
21
 
22
+ const searchParams = new URLSearchParams();
23
+ const headersObject: { [k: string]: string } = {};
24
+
25
+ data.operation.parameters?.forEach((rawParam) => {
26
+ const param = noReference(rawParam);
27
+ if (!param) {
28
+ return;
29
+ }
30
+
31
+ if (param.in === 'header' && param.required) {
32
+ const example = param.schema
33
+ ? generateSchemaExample(noReference(param.schema))
34
+ : undefined;
35
+ if (example !== undefined) {
36
+ headersObject[param.name] =
37
+ typeof example !== 'string' ? JSON.stringify(example) : example;
38
+ }
39
+ } else if (param.in === 'query' && param.required) {
40
+ const example = param.schema
41
+ ? generateSchemaExample(noReference(param.schema))
42
+ : undefined;
43
+ if (example !== undefined) {
44
+ searchParams.append(
45
+ param.name,
46
+ String(Array.isArray(example) ? example[0] : example),
47
+ );
48
+ }
49
+ }
50
+ });
51
+
22
52
  const requestBody = noReference(data.operation.requestBody);
23
53
  const requestBodyContent = requestBody ? Object.entries(requestBody.content)[0] : undefined;
24
54
 
25
55
  const input: CodeSampleInput = {
26
- url: getServersURL(data.servers) + data.path,
56
+ url:
57
+ getServersURL(data.servers) +
58
+ data.path +
59
+ (searchParams.size ? `?${searchParams.toString()}` : ''),
27
60
  method: data.method,
28
61
  body: requestBodyContent
29
62
  ? generateMediaTypeExample(requestBodyContent[1], { onlyRequired: true })
30
63
  : undefined,
31
64
  headers: {
32
65
  ...getSecurityHeaders(data.securities),
66
+ ...headersObject,
33
67
  ...(requestBodyContent
34
68
  ? {
35
69
  'Content-Type': requestBodyContent[0],
@@ -52,25 +86,32 @@ export function OpenAPICodeSample(props: {
52
86
  }> = null;
53
87
  (['x-custom-examples', 'x-code-samples', 'x-codeSamples'] as const).forEach((key) => {
54
88
  const customSamples = data.operation[key];
55
- if (customSamples) {
56
- customCodeSamples = customSamples.map((sample) => ({
57
- key: `redocly-${sample.lang}`,
58
- label: sample.label,
59
- body: <context.CodeBlock code={sample.source} syntax={sample.lang} />,
60
- }));
89
+ if (customSamples && Array.isArray(customSamples)) {
90
+ customCodeSamples = customSamples
91
+ .filter((sample) => {
92
+ return (
93
+ typeof sample.label === 'string' &&
94
+ typeof sample.source === 'string' &&
95
+ typeof sample.lang === 'string'
96
+ );
97
+ })
98
+ .map((sample) => ({
99
+ key: `redocly-${sample.lang}`,
100
+ label: sample.label,
101
+ body: <context.CodeBlock code={sample.source} syntax={sample.lang} />,
102
+ }));
61
103
  }
62
104
  });
63
105
 
64
- const samples = customCodeSamples ?? (data['x-codeSamples'] !== false ? autoCodeSamples : []);
106
+ // Code samples can be disabled at the top-level or at the operation level
107
+ // If code samples are defined at the operation level, it will override the top-level setting
108
+ const codeSamplesDisabled =
109
+ data['x-codeSamples'] === false || data.operation['x-codeSamples'] === false;
110
+ const samples = customCodeSamples ?? (!codeSamplesDisabled ? autoCodeSamples : []);
65
111
  if (samples.length === 0) {
66
112
  return null;
67
113
  }
68
114
 
69
- async function fetchOperationData() {
70
- 'use server';
71
- return toJSON(data);
72
- }
73
-
74
115
  return (
75
116
  <InteractiveSection
76
117
  header="Request"
@@ -78,7 +119,7 @@ export function OpenAPICodeSample(props: {
78
119
  tabs={samples}
79
120
  overlay={
80
121
  data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
81
- <ScalarApiButton fetchOperationData={fetchOperationData} />
122
+ <ScalarApiButton />
82
123
  )
83
124
  }
84
125
  />
@@ -1,5 +1,6 @@
1
1
  import * as React from 'react';
2
2
  import classNames from 'classnames';
3
+ import { ApiClientModalProvider } from '@scalar/api-client-react';
3
4
 
4
5
  import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
5
6
  import { Markdown } from './Markdown';
@@ -7,7 +8,6 @@ import { OpenAPICodeSample } from './OpenAPICodeSample';
7
8
  import { OpenAPIResponseExample } from './OpenAPIResponseExample';
8
9
  import { OpenAPIServerURL } from './OpenAPIServerURL';
9
10
  import { OpenAPISpec } from './OpenAPISpec';
10
- import { ScalarApiClient } from './ScalarApiButton';
11
11
  import { OpenAPIClientContext, OpenAPIContextProps } from './types';
12
12
 
13
13
  /**
@@ -24,13 +24,19 @@ export function OpenAPIOperation(props: {
24
24
  const clientContext: OpenAPIClientContext = {
25
25
  defaultInteractiveOpened: context.defaultInteractiveOpened,
26
26
  icons: context.icons,
27
+ blockKey: context.blockKey,
27
28
  };
28
29
 
29
30
  return (
30
- <ScalarApiClient>
31
+ <ApiClientModalProvider
32
+ configuration={{ spec: { url: context.specUrl } }}
33
+ initialRequest={{ path: data.path, method: data.method }}
34
+ >
31
35
  <div className={classNames('openapi-operation', className)}>
32
36
  <div className="openapi-intro">
33
- <h2 className="openapi-summary">{operation.summary}</h2>
37
+ <h2 className="openapi-summary" id={context.id}>
38
+ {operation.summary}
39
+ </h2>
34
40
  {operation.description ? (
35
41
  <Markdown className="openapi-description" source={operation.description} />
36
42
  ) : null}
@@ -61,6 +67,6 @@ export function OpenAPIOperation(props: {
61
67
  </div>
62
68
  </div>
63
69
  </div>
64
- </ScalarApiClient>
70
+ </ApiClientModalProvider>
65
71
  );
66
72
  }
@@ -3,7 +3,7 @@ import { InteractiveSection } from './InteractiveSection';
3
3
  import { OpenAPIOperationData } from './fetchOpenAPIOperation';
4
4
  import { generateSchemaExample } from './generateSchemaExample';
5
5
  import { OpenAPIContextProps } from './types';
6
- import { noReference } from './utils';
6
+ import { createStateKey, noReference } from './utils';
7
7
 
8
8
  /**
9
9
  * Display an example of the response content.
@@ -36,37 +36,52 @@ export function OpenAPIResponseExample(props: {
36
36
  }
37
37
  return Number(a) - Number(b);
38
38
  });
39
- // Take the first one
40
- const response = responses[0];
41
39
 
42
- if (!response) {
43
- return null;
44
- }
40
+ const examples = responses
41
+ .map((response) => {
42
+ const responseObject = noReference(response[1]);
45
43
 
46
- const responseObject = noReference(response[1]);
44
+ const schema = noReference(
45
+ (
46
+ responseObject.content?.['application/json'] ??
47
+ responseObject.content?.[Object.keys(responseObject.content)[0]]
48
+ )?.schema,
49
+ );
47
50
 
48
- const schema = noReference(
49
- (
50
- responseObject.content?.['application/json'] ??
51
- responseObject.content?.[Object.keys(responseObject.content)[0]]
52
- )?.schema,
53
- );
51
+ if (!schema) {
52
+ return null;
53
+ }
54
54
 
55
- if (!schema) {
56
- return null;
57
- }
55
+ const example = generateSchemaExample(schema);
56
+ if (example === undefined) {
57
+ return null;
58
+ }
59
+
60
+ return {
61
+ key: `${response[0]}`,
62
+ label: `${response[0]}`,
63
+ body: (
64
+ <context.CodeBlock
65
+ code={
66
+ typeof example === 'string' ? example : JSON.stringify(example, null, 2)
67
+ }
68
+ syntax="json"
69
+ />
70
+ ),
71
+ };
72
+ })
73
+ .filter((val): val is { key: string; label: string; body: any } => Boolean(val));
58
74
 
59
- const example = generateSchemaExample(schema);
60
- if (example === undefined) {
75
+ if (examples.length === 0) {
61
76
  return null;
62
77
  }
63
78
 
64
79
  return (
65
- <InteractiveSection header="Response" className="openapi-response-example">
66
- <context.CodeBlock
67
- code={typeof example === 'string' ? example : JSON.stringify(example, null, 2)}
68
- syntax="json"
69
- />
70
- </InteractiveSection>
80
+ <InteractiveSection
81
+ stateKey={createStateKey('response', context.blockKey)}
82
+ header="Response"
83
+ className="openapi-response-example"
84
+ tabs={examples}
85
+ />
71
86
  );
72
87
  }
@@ -1,7 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { OpenAPIV3 } from 'openapi-types';
4
- import { noReference } from './utils';
4
+ import { createStateKey, noReference } from './utils';
5
5
  import { OpenAPIResponse } from './OpenAPIResponse';
6
6
  import { OpenAPIClientContext } from './types';
7
7
  import { InteractiveSection } from './InteractiveSection';
@@ -17,6 +17,7 @@ export function OpenAPIResponses(props: {
17
17
 
18
18
  return (
19
19
  <InteractiveSection
20
+ stateKey={createStateKey('response', context.blockKey)}
20
21
  header="Response"
21
22
  className={classNames('openapi-responses')}
22
23
  tabs={Object.entries(responses).map(([statusCode, response]) => {
@@ -104,6 +104,11 @@ export function OpenAPISchemaProperty(
104
104
  Example: <code>{JSON.stringify(schema.example)}</code>
105
105
  </span>
106
106
  ) : null}
107
+ {schema.pattern ? (
108
+ <div className="openapi-schema-pattern">
109
+ Pattern: <code>{schema.pattern}</code>
110
+ </div>
111
+ ) : null}
107
112
  </div>
108
113
  }
109
114
  >
@@ -1,44 +1,17 @@
1
1
  'use client';
2
- import {
3
- Cookie,
4
- getHarRequest,
5
- getParametersFromOperation,
6
- type TransformedOperation,
7
- getRequestFromOperation,
8
- Query,
9
- Header,
10
- RequestBody,
11
- } from '@scalar/oas-utils';
12
- import React from 'react';
13
-
14
- import { OpenAPIOperationData, fromJSON } from './fetchOpenAPIOperation';
15
2
 
16
- const ApiClientReact = React.lazy(async () => {
17
- const mod = await import('@scalar/api-client-react');
18
- return { default: mod.ApiClientReact };
19
- });
20
-
21
- const ScalarContext = React.createContext<
22
- (fetchOperationData: () => Promise<OpenAPIOperationData>) => void
23
- >(() => {});
3
+ import { useApiClientModal } from '@scalar/api-client-react';
4
+ import React from 'react';
24
5
 
25
6
  /**
26
7
  * Button which launches the Scalar API Client
27
8
  */
28
- export function ScalarApiButton(props: {
29
- fetchOperationData: () => Promise<OpenAPIOperationData>;
30
- }) {
31
- const { fetchOperationData } = props;
32
- const open = React.useContext(ScalarContext);
9
+ export function ScalarApiButton() {
10
+ const client = useApiClientModal();
33
11
 
34
12
  return (
35
13
  <div className="scalar scalar-activate">
36
- <button
37
- className="scalar-activate-button"
38
- onClick={() => {
39
- open(fetchOperationData);
40
- }}
41
- >
14
+ <button className="scalar-activate-button" onClick={() => client?.open()}>
42
15
  <svg xmlns="http://www.w3.org/2000/svg" width="10" height="12" fill="none">
43
16
  <path
44
17
  stroke="currentColor"
@@ -51,109 +24,3 @@ export function ScalarApiButton(props: {
51
24
  </div>
52
25
  );
53
26
  }
54
-
55
- /**
56
- * Wrap the rendering with a context to open the scalar modal.
57
- */
58
- export function ScalarApiClient(props: { children: React.ReactNode }) {
59
- const { children } = props;
60
-
61
- const [active, setActive] = React.useState<null | {
62
- operationData: OpenAPIOperationData | null;
63
- }>(null);
64
-
65
- const proxy = '/~scalar/proxy';
66
-
67
- const open = React.useCallback(
68
- async (fetchOperationData: () => Promise<OpenAPIOperationData>) => {
69
- setActive({ operationData: null });
70
- const operationData = fromJSON(await fetchOperationData());
71
- setActive({ operationData });
72
- },
73
- [],
74
- );
75
-
76
- const onClose = React.useCallback(() => {
77
- setActive(null);
78
- }, []);
79
-
80
- const request = React.useMemo(() => {
81
- const operationData = active?.operationData;
82
-
83
- if (!operationData) {
84
- return null;
85
- }
86
-
87
- const operationId =
88
- operationData.operation.operationId ?? operationData.method + operationData.path;
89
-
90
- const operation = {
91
- ...operationData,
92
- httpVerb: operationData.method,
93
- pathParameters: operationData.operation.parameters,
94
- } as TransformedOperation;
95
-
96
- const variables = getParametersFromOperation(operation, 'path', false);
97
-
98
- const request = getHarRequest(
99
- {
100
- url: operationData.path,
101
- },
102
- getRequestFromOperation(
103
- {
104
- ...operation,
105
- information: {
106
- requestBody: operationData.operation.requestBody as RequestBody,
107
- },
108
- },
109
- { requiredOnly: false },
110
- ),
111
- );
112
-
113
- return {
114
- id: operationId,
115
- type: operationData.method,
116
- path: operationData.path,
117
- variables,
118
- cookies: request.cookies.map((cookie: Cookie) => {
119
- return { ...cookie, enabled: true };
120
- }),
121
- query: request.queryString.map((queryString: Query) => {
122
- const query: typeof queryString & { required?: boolean } = queryString;
123
- return { ...queryString, enabled: query.required ?? true };
124
- }),
125
- headers: request.headers.map((header: Header) => {
126
- return { ...header, enabled: true };
127
- }),
128
- url: operationData.servers[0]?.url,
129
- body: request.postData?.text,
130
- };
131
- }, [active]);
132
-
133
- return (
134
- <ScalarContext.Provider value={open}>
135
- {children}
136
- {active ? (
137
- <div className="scalar">
138
- <div className="scalar-container">
139
- <div className="scalar-app">
140
- <React.Suspense fallback={<ScalarLoading />}>
141
- <ApiClientReact
142
- close={onClose}
143
- proxy={proxy}
144
- isOpen={true}
145
- request={request}
146
- />
147
- </React.Suspense>
148
- </div>
149
- <div onClick={() => onClose()} className="scalar-app-exit"></div>
150
- </div>
151
- </div>
152
- ) : null}
153
- </ScalarContext.Provider>
154
- );
155
- }
156
-
157
- function ScalarLoading() {
158
- return <div className="scalar-app-loading">Loading...</div>;
159
- }
@@ -65,6 +65,25 @@ export const codeSampleGenerators: CodeSampleGenerator[] = [
65
65
  return lines.map((line, index) => (index > 0 ? indent(line, 2) : line)).join(separator);
66
66
  },
67
67
  },
68
+ {
69
+ id: 'python',
70
+ label: 'Python',
71
+ syntax: 'python',
72
+ generate: ({ method, url, headers, body }) => {
73
+ let code = 'import requests\n\n';
74
+ code += `response = requests.${method.toLowerCase()}(\n`;
75
+ code += indent(`"${url}",\n`, 4);
76
+ if (headers) {
77
+ code += indent(`headers=${JSON.stringify(headers)},\n`, 4);
78
+ }
79
+ if (body) {
80
+ code += indent(`json=${JSON.stringify(body)}\n`, 4);
81
+ }
82
+ code += ')\n';
83
+ code += `data = response.json()`;
84
+ return code;
85
+ },
86
+ },
68
87
  ];
69
88
 
70
89
  function indent(code: string, spaces: number) {
@@ -43,7 +43,7 @@ export interface OpenAPICustomSpecProperties {
43
43
  */
44
44
  export interface OpenAPICustomOperationProperties {
45
45
  'x-code-samples'?: OpenAPICustomCodeSample[];
46
- 'x-codeSamples'?: OpenAPICustomCodeSample[];
46
+ 'x-codeSamples'?: OpenAPICustomCodeSample[] | false;
47
47
  'x-custom-examples'?: OpenAPICustomCodeSample[];
48
48
 
49
49
  /**
@@ -104,7 +104,7 @@ export function generateSchemaExample(
104
104
 
105
105
  if (schema.properties) {
106
106
  const example: { [key: string]: JSONValue } = {};
107
- const props = onlyRequired ? schema.required ?? [] : Object.keys(schema.properties);
107
+ const props = onlyRequired ? (schema.required ?? []) : Object.keys(schema.properties);
108
108
 
109
109
  for (const key of props) {
110
110
  const property = noReference(schema.properties[key]);
package/src/types.ts CHANGED
@@ -2,6 +2,9 @@ export type IconComponent = React.ComponentType<{ className?: string }>;
2
2
 
3
3
  export interface OpenAPIContextProps extends OpenAPIClientContext {
4
4
  CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
5
+
6
+ /** Spec url for the Scalar Api Client */
7
+ specUrl: string;
5
8
  }
6
9
 
7
10
  export interface OpenAPIClientContext {
@@ -15,6 +18,12 @@ export interface OpenAPIClientContext {
15
18
  * @default false
16
19
  */
17
20
  defaultInteractiveOpened?: boolean;
21
+ /**
22
+ * The key of the block
23
+ */
24
+ blockKey?: string;
25
+ /** Optional id attached to the OpenAPI Operation heading and used as an anchor */
26
+ id?: string;
18
27
  }
19
28
 
20
29
  export interface OpenAPIFetcher {
package/src/utils.ts CHANGED
@@ -7,3 +7,7 @@ export function noReference<T>(input: T | OpenAPIV3.ReferenceObject): T {
7
7
 
8
8
  return input;
9
9
  }
10
+
11
+ export function createStateKey(key: string, scope?: string) {
12
+ return scope ? `${scope}_${key}` : key;
13
+ }