@gitbook/react-openapi 0.7.0 → 1.0.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.
Files changed (115) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/dist/InteractiveSection.d.ts +4 -6
  3. package/dist/InteractiveSection.jsx +96 -0
  4. package/dist/Markdown.d.ts +1 -2
  5. package/dist/Markdown.jsx +5 -0
  6. package/dist/OpenAPICodeSample.d.ts +2 -4
  7. package/dist/OpenAPICodeSample.jsx +143 -0
  8. package/dist/OpenAPIDisclosure.d.ts +12 -0
  9. package/dist/OpenAPIDisclosure.jsx +32 -0
  10. package/dist/OpenAPIDisclosureGroup.d.ts +19 -0
  11. package/dist/OpenAPIDisclosureGroup.jsx +81 -0
  12. package/dist/OpenAPIOperation.d.ts +2 -4
  13. package/dist/OpenAPIOperation.jsx +51 -0
  14. package/dist/OpenAPIOperationContext.d.ts +16 -0
  15. package/dist/OpenAPIOperationContext.jsx +26 -0
  16. package/dist/OpenAPIPath.d.ts +8 -0
  17. package/dist/OpenAPIPath.jsx +54 -0
  18. package/dist/OpenAPIRequestBody.d.ts +3 -4
  19. package/dist/OpenAPIRequestBody.jsx +19 -0
  20. package/dist/OpenAPIResponse.d.ts +4 -4
  21. package/dist/OpenAPIResponse.jsx +49 -0
  22. package/dist/OpenAPIResponseExample.d.ts +2 -4
  23. package/dist/OpenAPIResponseExample.jsx +108 -0
  24. package/dist/OpenAPIResponses.d.ts +3 -4
  25. package/dist/OpenAPIResponses.jsx +36 -0
  26. package/dist/OpenAPISchema.d.ts +11 -8
  27. package/dist/OpenAPISchema.jsx +295 -0
  28. package/dist/OpenAPISchemaName.d.ts +12 -0
  29. package/dist/OpenAPISchemaName.jsx +15 -0
  30. package/dist/OpenAPISecurities.d.ts +2 -4
  31. package/dist/OpenAPISecurities.jsx +55 -0
  32. package/dist/OpenAPIServerURL.d.ts +2 -3
  33. package/dist/OpenAPIServerURL.jsx +67 -0
  34. package/dist/OpenAPIServerURLVariable.d.ts +2 -3
  35. package/dist/OpenAPIServerURLVariable.jsx +8 -0
  36. package/dist/OpenAPISpec.d.ts +3 -4
  37. package/dist/OpenAPISpec.jsx +91 -0
  38. package/dist/OpenAPITabs.d.ts +25 -0
  39. package/dist/OpenAPITabs.jsx +67 -0
  40. package/dist/ScalarApiButton.d.ts +5 -2
  41. package/dist/ScalarApiButton.jsx +51 -0
  42. package/dist/code-samples.d.ts +4 -0
  43. package/dist/code-samples.js +103 -38
  44. package/dist/fetchOpenAPIOperation.d.ts +9 -54
  45. package/dist/fetchOpenAPIOperation.js +178 -107
  46. package/dist/generateSchemaExample.d.ts +2 -2
  47. package/dist/generateSchemaExample.js +28 -100
  48. package/dist/index.d.ts +3 -2
  49. package/dist/index.js +2 -1
  50. package/dist/resolveOpenAPIOperation.d.ts +11 -0
  51. package/dist/resolveOpenAPIOperation.js +194 -0
  52. package/dist/stringifyOpenAPI.d.ts +4 -0
  53. package/dist/stringifyOpenAPI.js +6 -0
  54. package/dist/tsconfig.build.tsbuildinfo +1 -0
  55. package/dist/tsconfig.tsbuildinfo +1 -1
  56. package/dist/types.d.ts +11 -12
  57. package/dist/utils.d.ts +6 -1
  58. package/dist/utils.js +15 -2
  59. package/package.json +12 -11
  60. package/src/InteractiveSection.tsx +119 -78
  61. package/src/Markdown.tsx +2 -3
  62. package/src/OpenAPICodeSample.tsx +35 -21
  63. package/src/OpenAPIDisclosure.tsx +50 -0
  64. package/src/OpenAPIDisclosureGroup.tsx +136 -0
  65. package/src/OpenAPIOperation.tsx +36 -42
  66. package/src/OpenAPIOperationContext.tsx +45 -0
  67. package/src/OpenAPIPath.tsx +65 -0
  68. package/src/OpenAPIRequestBody.tsx +3 -14
  69. package/src/OpenAPIResponse.tsx +39 -43
  70. package/src/OpenAPIResponseExample.tsx +89 -31
  71. package/src/OpenAPIResponses.tsx +51 -15
  72. package/src/OpenAPISchema.test.ts +1 -1
  73. package/src/OpenAPISchema.tsx +124 -92
  74. package/src/OpenAPISchemaName.tsx +27 -0
  75. package/src/OpenAPISecurities.tsx +45 -24
  76. package/src/OpenAPIServerURL.tsx +17 -10
  77. package/src/OpenAPIServerURLVariable.tsx +2 -4
  78. package/src/OpenAPISpec.tsx +56 -53
  79. package/src/OpenAPITabs.tsx +113 -0
  80. package/src/ScalarApiButton.tsx +86 -6
  81. package/src/code-samples.test.ts +51 -0
  82. package/src/code-samples.ts +95 -31
  83. package/src/generateSchemaExample.ts +25 -151
  84. package/src/index.ts +3 -2
  85. package/src/resolveOpenAPIOperation.test.ts +177 -0
  86. package/src/resolveOpenAPIOperation.ts +163 -0
  87. package/src/stringifyOpenAPI.ts +6 -0
  88. package/src/types.ts +17 -10
  89. package/src/utils.ts +17 -2
  90. package/dist/InteractiveSection.js +0 -47
  91. package/dist/Markdown.js +0 -6
  92. package/dist/OpenAPICodeSample.js +0 -110
  93. package/dist/OpenAPIOperation.js +0 -38
  94. package/dist/OpenAPIRequestBody.js +0 -18
  95. package/dist/OpenAPIResponse.js +0 -32
  96. package/dist/OpenAPIResponseExample.js +0 -54
  97. package/dist/OpenAPIResponses.js +0 -18
  98. package/dist/OpenAPISchema.js +0 -235
  99. package/dist/OpenAPISchema.test.d.ts +0 -1
  100. package/dist/OpenAPISchema.test.js +0 -91
  101. package/dist/OpenAPISecurities.js +0 -42
  102. package/dist/OpenAPIServerURL.js +0 -51
  103. package/dist/OpenAPIServerURLVariable.js +0 -10
  104. package/dist/OpenAPISpec.js +0 -70
  105. package/dist/ScalarApiButton.js +0 -14
  106. package/dist/fetchOpenAPIOperation.test.d.ts +0 -1
  107. package/dist/fetchOpenAPIOperation.test.js +0 -152
  108. package/dist/resolveOpenAPIPath.d.ts +0 -7
  109. package/dist/resolveOpenAPIPath.js +0 -112
  110. package/dist/resolveOpenAPIPath.test.d.ts +0 -1
  111. package/dist/resolveOpenAPIPath.test.js +0 -39
  112. package/src/fetchOpenAPIOperation.test.ts +0 -185
  113. package/src/fetchOpenAPIOperation.ts +0 -230
  114. package/src/resolveOpenAPIPath.test.ts +0 -60
  115. package/src/resolveOpenAPIPath.ts +0 -145
@@ -0,0 +1,136 @@
1
+ interface Props {
2
+ groups: TDisclosureGroup[];
3
+ icon?: React.ReactNode;
4
+ }
5
+
6
+ type TDisclosureGroup = {
7
+ id: string;
8
+ label: string | React.ReactNode;
9
+ tabs?: {
10
+ id: string;
11
+ label: string | React.ReactNode;
12
+ body?: React.ReactNode;
13
+ }[];
14
+ };
15
+
16
+ import { mergeProps, useButton, useDisclosure, useFocusRing, useId } from 'react-aria';
17
+ import {
18
+ DisclosureGroupProps,
19
+ DisclosureGroupState,
20
+ useDisclosureGroupState,
21
+ useDisclosureState,
22
+ } from 'react-stately';
23
+ import { createContext, useContext, useRef, useState } from 'react';
24
+
25
+ const DisclosureGroupStateContext = createContext<DisclosureGroupState | null>(null);
26
+
27
+ /**
28
+ * Display an interactive OpenAPI disclosure group.
29
+ */
30
+ export function OpenAPIDisclosureGroup(props: DisclosureGroupProps & Props) {
31
+ const { icon, groups } = props;
32
+
33
+ const state = useDisclosureGroupState(props);
34
+
35
+ return (
36
+ <DisclosureGroupStateContext.Provider value={state}>
37
+ {groups.map((group) => (
38
+ <DisclosureItem icon={icon} key={group.id} group={group} />
39
+ ))}
40
+ </DisclosureGroupStateContext.Provider>
41
+ );
42
+ }
43
+
44
+ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode }) {
45
+ const { icon, group } = props;
46
+
47
+ const defaultId = useId();
48
+ const id = group.id || defaultId;
49
+ const groupState = useContext(DisclosureGroupStateContext);
50
+ const isExpanded = groupState?.expandedKeys.has(id) || false;
51
+ const state = useDisclosureState({
52
+ isExpanded,
53
+ onExpandedChange() {
54
+ if (groupState) {
55
+ groupState.toggleKey(id);
56
+ }
57
+ },
58
+ });
59
+
60
+ const panelRef = useRef<HTMLDivElement | null>(null);
61
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
62
+ const isDisabled = groupState?.isDisabled || !group.tabs?.length || false;
63
+ const { buttonProps: triggerProps, panelProps } = useDisclosure(
64
+ {
65
+ ...props,
66
+ isExpanded,
67
+ isDisabled,
68
+ },
69
+ state,
70
+ panelRef,
71
+ );
72
+ const { buttonProps } = useButton(triggerProps, triggerRef);
73
+ const { isFocusVisible, focusProps } = useFocusRing();
74
+
75
+ const defaultTab = group.tabs?.[0]?.id || '';
76
+ const [selectedTabKey, setSelectedTabKey] = useState(defaultTab);
77
+ const selectedTab = group.tabs?.find((tab) => tab.id === selectedTabKey);
78
+
79
+ return (
80
+ <div className="openapi-disclosure-group" aria-expanded={state.isExpanded}>
81
+ <div className="openapi-disclosure-group-header">
82
+ <button
83
+ slot="trigger"
84
+ ref={triggerRef}
85
+ {...mergeProps(buttonProps, focusProps)}
86
+ disabled={isDisabled}
87
+ style={{
88
+ outline: isFocusVisible
89
+ ? '2px solid rgb(var(--primary-color-500)/0.4)'
90
+ : 'none',
91
+ }}
92
+ className="openapi-disclosure-group-trigger"
93
+ >
94
+ <div className="openapi-disclosure-group-icon">
95
+ {icon || (
96
+ <svg viewBox="0 0 24 24" className="openapi-disclosure-group-icon">
97
+ <path d="m8.25 4.5 7.5 7.5-7.5 7.5" />
98
+ </svg>
99
+ )}
100
+ </div>
101
+
102
+ {group.label}
103
+ </button>
104
+ {group.tabs ? (
105
+ <div className="openapi-disclosure-group-mediatype">
106
+ {group.tabs?.length > 1 ? (
107
+ <select
108
+ className="openapi-section-select openapi-select openapi-disclosure-group-tabs-select"
109
+ onClick={(event) => event.stopPropagation()}
110
+ value={selectedTab?.id}
111
+ onChange={(event) => {
112
+ setSelectedTabKey(event.target.value);
113
+ state.expand();
114
+ }}
115
+ >
116
+ {group.tabs.map((tab) => (
117
+ <option key={tab.id} value={tab.id}>
118
+ {tab.label}
119
+ </option>
120
+ ))}
121
+ </select>
122
+ ) : !!group.tabs[0] ? (
123
+ <span>{group.tabs[0].label}</span>
124
+ ) : null}
125
+ </div>
126
+ ) : null}
127
+ </div>
128
+
129
+ {state.isExpanded && selectedTab && (
130
+ <div className="openapi-disclosure-group-panel" ref={panelRef} {...panelProps}>
131
+ {selectedTab.body}
132
+ </div>
133
+ )}
134
+ </div>
135
+ );
136
+ }
@@ -1,14 +1,12 @@
1
- import * as React from 'react';
2
- import classNames from 'classnames';
3
- import { ApiClientModalProvider } from '@scalar/api-client-react';
1
+ import clsx from 'clsx';
4
2
 
5
- import { OpenAPIOperationData, toJSON } from './fetchOpenAPIOperation';
6
3
  import { Markdown } from './Markdown';
7
4
  import { OpenAPICodeSample } from './OpenAPICodeSample';
8
5
  import { OpenAPIResponseExample } from './OpenAPIResponseExample';
9
- import { OpenAPIServerURL } from './OpenAPIServerURL';
10
6
  import { OpenAPISpec } from './OpenAPISpec';
11
- import { OpenAPIClientContext, OpenAPIContextProps } from './types';
7
+ import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from './types';
8
+ import { OpenAPIPath } from './OpenAPIPath';
9
+ import { resolveDescription } from './utils';
12
10
 
13
11
  /**
14
12
  * Display an interactive OpenAPI operation.
@@ -19,7 +17,7 @@ export function OpenAPIOperation(props: {
19
17
  context: OpenAPIContextProps;
20
18
  }) {
21
19
  const { className, data, context } = props;
22
- const { operation, servers, method, path } = data;
20
+ const { operation } = data;
23
21
 
24
22
  const clientContext: OpenAPIClientContext = {
25
23
  defaultInteractiveOpened: context.defaultInteractiveOpened,
@@ -27,46 +25,42 @@ export function OpenAPIOperation(props: {
27
25
  blockKey: context.blockKey,
28
26
  };
29
27
 
28
+ const description = resolveDescription(operation)?.trim();
29
+
30
30
  return (
31
- <ApiClientModalProvider
32
- configuration={{ spec: { url: context.specUrl } }}
33
- initialRequest={{ path: data.path, method: data.method }}
34
- >
35
- <div className={classNames('openapi-operation', className)}>
36
- <div className="openapi-intro">
37
- <h2 className="openapi-summary" id={context.id}>
38
- {operation.summary}
39
- </h2>
40
- {operation.description ? (
41
- <Markdown className="openapi-description" source={operation.description} />
31
+ <div className={clsx('openapi-operation', className)}>
32
+ <div className="openapi-summary" id={context.id}>
33
+ <h2 className="openapi-summary-title" data-deprecated={operation.deprecated}>
34
+ {operation.summary}
35
+ </h2>
36
+ {operation.deprecated && <div className="openapi-deprecated">Deprecated</div>}
37
+ </div>
38
+ <div className="openapi-columns">
39
+ <div className="openapi-column-spec">
40
+ {operation['x-deprecated-sunset'] ? (
41
+ <div className="openapi-deprecated-sunset openapi-description openapi-markdown">
42
+ This operation is deprecated and will be sunset on{' '}
43
+ <span className="openapi-deprecated-sunset-date">
44
+ {operation['x-deprecated-sunset']}
45
+ </span>
46
+ {`.`}
47
+ </div>
42
48
  ) : null}
43
- <div className="openapi-target">
44
- <span
45
- className={classNames(
46
- 'openapi-method',
47
- `openapi-method-${method.toLowerCase()}`,
48
- )}
49
- >
50
- {method.toUpperCase()}
51
- </span>
52
- <span className="openapi-url">
53
- <OpenAPIServerURL servers={servers} />
54
- {path}
55
- </span>
56
- </div>
57
- </div>
58
- <div className={classNames('openapi-columns')}>
59
- <div className={classNames('openapi-column-spec')}>
60
- <OpenAPISpec rawData={toJSON(data)} context={clientContext} />
61
- </div>
62
- <div className={classNames('openapi-column-preview')}>
63
- <div className={classNames('openapi-column-preview-body')}>
64
- <OpenAPICodeSample {...props} />
65
- <OpenAPIResponseExample {...props} />
49
+ {description ? (
50
+ <div className="openapi-intro">
51
+ <Markdown className="openapi-description" source={description} />
66
52
  </div>
53
+ ) : null}
54
+ <OpenAPIPath data={data} context={context} />
55
+ <OpenAPISpec data={data} context={clientContext} />
56
+ </div>
57
+ <div className="openapi-column-preview">
58
+ <div className="openapi-column-preview-body">
59
+ <OpenAPICodeSample {...props} />
60
+ <OpenAPIResponseExample {...props} />
67
61
  </div>
68
62
  </div>
69
63
  </div>
70
- </ApiClientModalProvider>
64
+ </div>
71
65
  );
72
66
  }
@@ -0,0 +1,45 @@
1
+ 'use client';
2
+
3
+ import { createContext, useContext, useMemo } from 'react';
4
+ import { useEventCallback } from 'usehooks-ts';
5
+
6
+ interface OpenAPIOperationPointer {
7
+ path: string;
8
+ method: string;
9
+ }
10
+
11
+ interface OpenAPIOperationContextValue {
12
+ onOpenClient: (pointer: OpenAPIOperationPointer) => void;
13
+ }
14
+
15
+ const OpenAPIOperationContext = createContext<OpenAPIOperationContextValue>({
16
+ onOpenClient: () => {},
17
+ });
18
+
19
+ /**
20
+ * Provider for the OpenAPIOperationContext.
21
+ */
22
+ export function OpenAPIOperationContextProvider(
23
+ props: React.PropsWithChildren<Partial<OpenAPIOperationContextValue>>,
24
+ ) {
25
+ const { children } = props;
26
+
27
+ const onOpenClient = useEventCallback((pointer: OpenAPIOperationPointer) => {
28
+ props.onOpenClient?.(pointer);
29
+ });
30
+
31
+ const value = useMemo(() => ({ onOpenClient }), [onOpenClient]);
32
+
33
+ return (
34
+ <OpenAPIOperationContext.Provider value={value}>
35
+ {children}
36
+ </OpenAPIOperationContext.Provider>
37
+ );
38
+ }
39
+
40
+ /**
41
+ * Hook to access the OpenAPIOperationContext.
42
+ */
43
+ export function useOpenAPIOperationContext() {
44
+ return useContext(OpenAPIOperationContext);
45
+ }
@@ -0,0 +1,65 @@
1
+ import { ScalarApiButton } from './ScalarApiButton';
2
+ import type { OpenAPIOperationData, OpenAPIContextProps } from './types';
3
+
4
+ /**
5
+ * Display the path of an operation.
6
+ */
7
+ export function OpenAPIPath(props: {
8
+ data: OpenAPIOperationData;
9
+ context: OpenAPIContextProps;
10
+ }): JSX.Element {
11
+ const { data, context } = props;
12
+ const { method, path } = data;
13
+ const { specUrl } = context;
14
+
15
+ return (
16
+ <div className="openapi-path">
17
+ <div className={`openapi-method openapi-method-${method}`}>{method}</div>
18
+ <div className="openapi-path-title" data-deprecated={data.operation.deprecated}>
19
+ <p>{formatPath(path)}</p>
20
+ </div>
21
+ {data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
22
+ <ScalarApiButton method={method} path={path} specUrl={specUrl} />
23
+ )}
24
+ </div>
25
+ );
26
+ }
27
+
28
+ // Format the path to highlight placeholders
29
+ function formatPath(path: string) {
30
+ // Matches placeholders like {id}, {userId}, etc.
31
+ const regex = /\{(\w+)\}/g;
32
+
33
+ const parts: (string | JSX.Element)[] = [];
34
+ let lastIndex = 0;
35
+
36
+ // Replace placeholders with <em> tags
37
+ path.replace(regex, (match, key, offset) => {
38
+ parts.push(path.slice(lastIndex, offset));
39
+ parts.push(<em key={key}>{`{${key}}`}</em>);
40
+ lastIndex = offset + match.length;
41
+ return match;
42
+ });
43
+
44
+ // Push remaining text after the last placeholder
45
+ parts.push(path.slice(lastIndex));
46
+
47
+ // Join parts with separators wrapped in <span>
48
+ const formattedPath = parts.reduce(
49
+ (acc, part, index) => {
50
+ if (typeof part === 'string' && index > 0 && part === '/') {
51
+ return [
52
+ ...acc,
53
+ <span className="openapi-path-separator" key={`sep-${index}`}>
54
+ /
55
+ </span>,
56
+ part,
57
+ ];
58
+ }
59
+ return [...acc, part];
60
+ },
61
+ [] as (string | JSX.Element)[],
62
+ );
63
+
64
+ return <span>{formattedPath}</span>;
65
+ }
@@ -1,11 +1,8 @@
1
- import * as React from 'react';
2
-
3
- import { OpenAPIV3 } from 'openapi-types';
1
+ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
4
2
  import { OpenAPIRootSchema } from './OpenAPISchema';
5
3
  import { noReference } from './utils';
6
- import { OpenAPIClientContext } from './types';
4
+ import type { OpenAPIClientContext } from './types';
7
5
  import { InteractiveSection } from './InteractiveSection';
8
- import { Markdown } from './Markdown';
9
6
 
10
7
  /**
11
8
  * Display an interactive request body.
@@ -34,14 +31,6 @@ export function OpenAPIRequestBody(props: {
34
31
  };
35
32
  },
36
33
  )}
37
- defaultOpened={context.defaultInteractiveOpened}
38
- >
39
- {requestBody.description ? (
40
- <Markdown
41
- source={requestBody.description}
42
- className="openapi-requestbody-description"
43
- />
44
- ) : null}
45
- </InteractiveSection>
34
+ />
46
35
  );
47
36
  }
@@ -1,44 +1,33 @@
1
- import * as React from 'react';
2
- import classNames from 'classnames';
3
- import { OpenAPIV3 } from 'openapi-types';
4
- import { OpenAPIRootSchema, OpenAPISchemaProperties } from './OpenAPISchema';
5
- import { noReference } from './utils';
6
- import { OpenAPIClientContext } from './types';
7
- import { InteractiveSection } from './InteractiveSection';
8
- import { Markdown } from './Markdown';
1
+ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
+ import { OpenAPISchemaProperties } from './OpenAPISchema';
3
+ import { checkIsReference, noReference, resolveDescription } from './utils';
4
+ import type { OpenAPIClientContext } from './types';
5
+ import { OpenAPIDisclosure } from './OpenAPIDisclosure';
9
6
 
10
7
  /**
11
8
  * Display an interactive response body.
12
9
  */
13
10
  export function OpenAPIResponse(props: {
14
11
  response: OpenAPIV3.ResponseObject;
12
+ mediaType: OpenAPIV3.MediaTypeObject;
15
13
  context: OpenAPIClientContext;
16
14
  }) {
17
- const { response, context } = props;
18
- const content = Object.entries(response.content ?? {});
15
+ const { response, context, mediaType } = props;
19
16
  const headers = Object.entries(response.headers ?? {}).map(
20
17
  ([name, header]) => [name, noReference(header) ?? {}] as const,
21
18
  );
19
+ const content = Object.entries(mediaType.schema ?? {});
22
20
 
23
- if (content.length === 0 && !response.description && headers.length === 0) {
21
+ const description = resolveDescription(response);
22
+
23
+ if (content.length === 0 && !description && headers.length === 0) {
24
24
  return null;
25
25
  }
26
26
 
27
27
  return (
28
- <>
29
- {response.description ? (
30
- <Markdown source={response.description} className="openapi-response-description" />
31
- ) : null}
32
-
28
+ <div className="openapi-response-body">
33
29
  {headers.length > 0 ? (
34
- <InteractiveSection
35
- toggeable
36
- defaultOpened={!!context.defaultInteractiveOpened}
37
- toggleCloseIcon={context.icons.chevronDown}
38
- toggleOpenIcon={context.icons.chevronRight}
39
- header="Headers"
40
- className={classNames('openapi-responseheaders')}
41
- >
30
+ <OpenAPIDisclosure context={context} label={'Headers'}>
42
31
  <OpenAPISchemaProperties
43
32
  properties={headers.map(([name, header]) => ({
44
33
  propertyName: name,
@@ -47,26 +36,33 @@ export function OpenAPIResponse(props: {
47
36
  }))}
48
37
  context={context}
49
38
  />
50
- </InteractiveSection>
39
+ </OpenAPIDisclosure>
51
40
  ) : null}
52
- {content.length > 0 ? (
53
- <InteractiveSection
54
- header="Body"
55
- className={classNames('openapi-responsebody')}
56
- tabs={content.map(([contentType, mediaType]) => {
57
- return {
58
- key: contentType,
59
- label: contentType,
60
- body: (
61
- <OpenAPIRootSchema
62
- schema={noReference(mediaType.schema) ?? {}}
63
- context={context}
64
- />
65
- ),
66
- };
67
- })}
41
+ <div className="openapi-responsebody">
42
+ <OpenAPISchemaProperties
43
+ id={`response-${context.blockKey}`}
44
+ properties={[
45
+ {
46
+ schema: handleUnresolvedReference(mediaType.schema) ?? {},
47
+ },
48
+ ]}
49
+ context={context}
68
50
  />
69
- ) : null}
70
- </>
51
+ </div>
52
+ </div>
71
53
  );
72
54
  }
55
+
56
+ function handleUnresolvedReference(
57
+ input: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined,
58
+ ): OpenAPIV3.SchemaObject {
59
+ const isReference = checkIsReference(input);
60
+
61
+ if (isReference || input === undefined) {
62
+ // If we find a reference that wasn't resolved or needed to be resolved externally, do not try to render it.
63
+ // Instead we render `any`
64
+ return {};
65
+ }
66
+
67
+ return input;
68
+ }
@@ -1,9 +1,10 @@
1
- import * as React from 'react';
2
- import { InteractiveSection } from './InteractiveSection';
3
- import { OpenAPIOperationData } from './fetchOpenAPIOperation';
1
+ import type { OpenAPIV3 } from '@gitbook/openapi-parser';
4
2
  import { generateSchemaExample } from './generateSchemaExample';
5
- import { OpenAPIContextProps } from './types';
6
- import { createStateKey, noReference } from './utils';
3
+ import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
4
+ import { checkIsReference, noReference, resolveDescription } from './utils';
5
+ import { stringifyOpenAPI } from './stringifyOpenAPI';
6
+ import { OpenAPITabs, OpenAPITabsList, OpenAPITabsPanels } from './OpenAPITabs';
7
+ import { InteractiveSection } from './InteractiveSection';
7
8
 
8
9
  /**
9
10
  * Display an example of the response content.
@@ -38,50 +39,107 @@ export function OpenAPIResponseExample(props: {
38
39
  });
39
40
 
40
41
  const examples = responses
41
- .map((response) => {
42
- const responseObject = noReference(response[1]);
43
-
44
- const schema = noReference(
45
- (
46
- responseObject.content?.['application/json'] ??
47
- responseObject.content?.[Object.keys(responseObject.content)[0]]
48
- )?.schema,
49
- );
42
+ .map(([key, value]) => {
43
+ const responseObject = noReference(value);
44
+ const mediaTypeObject = (() => {
45
+ if (!responseObject.content) {
46
+ return null;
47
+ }
48
+ const key = Object.keys(responseObject.content)[0];
49
+ return (
50
+ responseObject.content['application/json'] ??
51
+ (key ? responseObject.content[key] : null)
52
+ );
53
+ })();
50
54
 
51
- if (!schema) {
52
- return null;
55
+ if (!mediaTypeObject) {
56
+ return {
57
+ key: key,
58
+ label: key,
59
+ description: resolveDescription(responseObject),
60
+ body: <OpenAPIEmptyResponseExample />,
61
+ };
53
62
  }
54
63
 
55
- const example = generateSchemaExample(schema);
56
- if (example === undefined) {
57
- return null;
58
- }
64
+ const example = handleUnresolvedReference(
65
+ (() => {
66
+ const { examples, example } = mediaTypeObject;
67
+ if (examples) {
68
+ const key = Object.keys(examples)[0];
69
+ if (key) {
70
+ // @TODO handle multiple examples
71
+ const firstExample = noReference(examples[key]);
72
+ if (firstExample) {
73
+ return firstExample;
74
+ }
75
+ }
76
+ }
77
+
78
+ if (example) {
79
+ return { value: example };
80
+ }
81
+
82
+ const schema = noReference(mediaTypeObject.schema);
83
+ if (!schema) {
84
+ return null;
85
+ }
86
+
87
+ return { value: generateSchemaExample(schema) };
88
+ })(),
89
+ );
59
90
 
60
91
  return {
61
- key: `${response[0]}`,
62
- label: `${response[0]}`,
63
- body: (
92
+ key: key,
93
+ label: key,
94
+ description: resolveDescription(responseObject),
95
+ body: example?.value ? (
64
96
  <context.CodeBlock
65
97
  code={
66
- typeof example === 'string' ? example : JSON.stringify(example, null, 2)
98
+ typeof example.value === 'string'
99
+ ? example.value
100
+ : stringifyOpenAPI(example.value, null, 2)
67
101
  }
68
102
  syntax="json"
69
103
  />
104
+ ) : (
105
+ <OpenAPIEmptyResponseExample />
70
106
  ),
71
107
  };
72
108
  })
73
- .filter((val): val is { key: string; label: string; body: any } => Boolean(val));
109
+ .filter((val): val is { key: string; label: string; body: any; description: string } =>
110
+ Boolean(val),
111
+ );
74
112
 
75
113
  if (examples.length === 0) {
76
114
  return null;
77
115
  }
78
116
 
79
117
  return (
80
- <InteractiveSection
81
- stateKey={createStateKey('response', context.blockKey)}
82
- header="Response"
83
- className="openapi-response-example"
84
- tabs={examples}
85
- />
118
+ <OpenAPITabs items={examples}>
119
+ <InteractiveSection header={<OpenAPITabsList />} className="openapi-response-example">
120
+ <OpenAPITabsPanels />
121
+ </InteractiveSection>
122
+ </OpenAPITabs>
86
123
  );
87
124
  }
125
+
126
+ function OpenAPIEmptyResponseExample() {
127
+ return (
128
+ <pre className="openapi-response-example-empty">
129
+ <p>No body</p>
130
+ </pre>
131
+ );
132
+ }
133
+
134
+ function handleUnresolvedReference(
135
+ input: OpenAPIV3.ExampleObject | null,
136
+ ): OpenAPIV3.ExampleObject | null {
137
+ const isReference = checkIsReference(input?.value);
138
+
139
+ if (isReference) {
140
+ // If we find a reference that wasn't resolved or needed to be resolved externally, render out the URL
141
+ return { value: input.value.$ref };
142
+ }
143
+
144
+ return input;
145
+ }