@gitbook/react-openapi 0.2.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.
- package/CHANGELOG.md +13 -0
- package/README.md +12 -0
- package/package.json +20 -0
- package/src/InteractiveSection.tsx +129 -0
- package/src/Markdown.tsx +12 -0
- package/src/OpenAPICodeSample.tsx +111 -0
- package/src/OpenAPIOperation.tsx +65 -0
- package/src/OpenAPIRequestBody.tsx +45 -0
- package/src/OpenAPIResponse.tsx +71 -0
- package/src/OpenAPIResponseExample.tsx +71 -0
- package/src/OpenAPIResponses.tsx +30 -0
- package/src/OpenAPISchema.test.ts +101 -0
- package/src/OpenAPISchema.tsx +401 -0
- package/src/OpenAPISecurities.tsx +71 -0
- package/src/OpenAPIServerURL.tsx +65 -0
- package/src/OpenAPIServerURLVariable.tsx +16 -0
- package/src/OpenAPISpec.tsx +118 -0
- package/src/ScalarApiButton.tsx +159 -0
- package/src/code-samples.ts +76 -0
- package/src/fetchOpenAPIOperation.test.ts +185 -0
- package/src/fetchOpenAPIOperation.ts +230 -0
- package/src/generateSchemaExample.ts +189 -0
- package/src/index.ts +3 -0
- package/src/resolveOpenAPIPath.test.ts +60 -0
- package/src/resolveOpenAPIPath.ts +145 -0
- package/src/types.ts +30 -0
- package/src/utils.ts +9 -0
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import { OpenAPIV3 } from 'openapi-types';
|
|
2
|
+
import { noReference } from './utils';
|
|
3
|
+
|
|
4
|
+
type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate a JSON example from a schema
|
|
8
|
+
*/
|
|
9
|
+
export function generateSchemaExample(
|
|
10
|
+
schema: OpenAPIV3.SchemaObject,
|
|
11
|
+
options: {
|
|
12
|
+
onlyRequired?: boolean;
|
|
13
|
+
} = {},
|
|
14
|
+
ancestors: Set<OpenAPIV3.SchemaObject> = new Set(),
|
|
15
|
+
): JSONValue | undefined {
|
|
16
|
+
const { onlyRequired = false } = options;
|
|
17
|
+
|
|
18
|
+
if (ancestors.has(schema)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof schema.example !== 'undefined') {
|
|
23
|
+
return schema.example;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
27
|
+
return schema.enum[0];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (schema.type === 'string') {
|
|
31
|
+
if (schema.default) {
|
|
32
|
+
return schema.default;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (schema.format === 'date-time') {
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (schema.format === 'date') {
|
|
40
|
+
return new Date().toISOString().split('T')[0];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (schema.format === 'email') {
|
|
44
|
+
return 'name@gmail.com';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (schema.format === 'hostname') {
|
|
48
|
+
return 'example.com';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (schema.format === 'ipv4') {
|
|
52
|
+
return '0.0.0.0';
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (schema.format === 'ipv6') {
|
|
56
|
+
return '2001:0db8:85a3:0000:0000:8a2e:0370:7334';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (schema.format === 'uri') {
|
|
60
|
+
return 'https://example.com';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (schema.format === 'uuid') {
|
|
64
|
+
return '123e4567-e89b-12d3-a456-426614174000';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (schema.format === 'binary') {
|
|
68
|
+
return 'binary';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (schema.format === 'byte') {
|
|
72
|
+
return 'Ynl0ZXM=';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (schema.format === 'password') {
|
|
76
|
+
return 'password';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return 'text';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (schema.type === 'number') {
|
|
83
|
+
return schema.default || 0;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (schema.type === 'boolean') {
|
|
87
|
+
return schema.default || false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (schema.type === 'array') {
|
|
91
|
+
if (schema.items) {
|
|
92
|
+
const exampleValue = generateSchemaExample(
|
|
93
|
+
noReference(schema.items),
|
|
94
|
+
options,
|
|
95
|
+
new Set(ancestors).add(schema),
|
|
96
|
+
);
|
|
97
|
+
if (exampleValue !== undefined) {
|
|
98
|
+
return [exampleValue];
|
|
99
|
+
}
|
|
100
|
+
return [];
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (schema.properties) {
|
|
106
|
+
const example: { [key: string]: JSONValue } = {};
|
|
107
|
+
const props = onlyRequired ? schema.required ?? [] : Object.keys(schema.properties);
|
|
108
|
+
|
|
109
|
+
for (const key of props) {
|
|
110
|
+
const property = noReference(schema.properties[key]);
|
|
111
|
+
if (property && (onlyRequired || !property.deprecated)) {
|
|
112
|
+
const exampleValue = generateSchemaExample(
|
|
113
|
+
noReference(property),
|
|
114
|
+
options,
|
|
115
|
+
new Set(ancestors).add(schema),
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
if (exampleValue !== undefined) {
|
|
119
|
+
example[key] = exampleValue;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return example;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
127
|
+
return generateSchemaExample(
|
|
128
|
+
noReference(schema.oneOf[0]),
|
|
129
|
+
options,
|
|
130
|
+
new Set(ancestors).add(schema),
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
135
|
+
return generateSchemaExample(
|
|
136
|
+
noReference(schema.anyOf[0]),
|
|
137
|
+
options,
|
|
138
|
+
new Set(ancestors).add(schema),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (schema.allOf && schema.allOf.length > 0) {
|
|
143
|
+
return schema.allOf.reduce(
|
|
144
|
+
(acc, curr) => {
|
|
145
|
+
const example = generateSchemaExample(
|
|
146
|
+
noReference(curr),
|
|
147
|
+
options,
|
|
148
|
+
new Set(ancestors).add(schema),
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (typeof example === 'object' && !Array.isArray(example) && example !== null) {
|
|
152
|
+
return { ...acc, ...example };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return acc;
|
|
156
|
+
},
|
|
157
|
+
{} as { [key: string]: JSONValue },
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return undefined;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Generate an example for a media type.
|
|
166
|
+
*/
|
|
167
|
+
export function generateMediaTypeExample(
|
|
168
|
+
mediaType: OpenAPIV3.MediaTypeObject,
|
|
169
|
+
options: {
|
|
170
|
+
onlyRequired?: boolean;
|
|
171
|
+
} = {},
|
|
172
|
+
): JSONValue | undefined {
|
|
173
|
+
if (mediaType.example) {
|
|
174
|
+
return mediaType.example;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (mediaType.examples) {
|
|
178
|
+
const example = mediaType.examples[Object.keys(mediaType.examples)[0]];
|
|
179
|
+
if (example) {
|
|
180
|
+
return noReference(example).value;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (mediaType.schema) {
|
|
185
|
+
return generateSchemaExample(noReference(mediaType.schema), options);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { it, expect } from 'bun:test';
|
|
2
|
+
|
|
3
|
+
import { resolveOpenAPIPath } from './resolveOpenAPIPath';
|
|
4
|
+
import { OpenAPIFetcher } from './types';
|
|
5
|
+
|
|
6
|
+
const createFetcherForSchema = (schema: any): OpenAPIFetcher => {
|
|
7
|
+
return {
|
|
8
|
+
fetch: async (url) => {
|
|
9
|
+
return schema;
|
|
10
|
+
},
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
it('should resolve a simple path through objects', async () => {
|
|
15
|
+
const resolved = await resolveOpenAPIPath(
|
|
16
|
+
'https://test.com',
|
|
17
|
+
['a', 'b', 'c'],
|
|
18
|
+
createFetcherForSchema({
|
|
19
|
+
a: {
|
|
20
|
+
b: {
|
|
21
|
+
c: 'hello',
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
expect(resolved).toBe('hello');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should return undefined if the last part of the path does not exists', async () => {
|
|
31
|
+
const resolved = await resolveOpenAPIPath(
|
|
32
|
+
'https://test.com',
|
|
33
|
+
['a', 'b', 'c'],
|
|
34
|
+
createFetcherForSchema({
|
|
35
|
+
a: {
|
|
36
|
+
b: {
|
|
37
|
+
d: 'hello',
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
expect(resolved).toBe(undefined);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should return undefined if a middle part of the path does not exists', async () => {
|
|
47
|
+
const resolved = await resolveOpenAPIPath(
|
|
48
|
+
'https://test.com',
|
|
49
|
+
['a', 'x', 'c'],
|
|
50
|
+
createFetcherForSchema({
|
|
51
|
+
a: {
|
|
52
|
+
b: {
|
|
53
|
+
c: 'hello',
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
expect(resolved).toBe(undefined);
|
|
60
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { OpenAPIFetcher } from './types';
|
|
2
|
+
|
|
3
|
+
const SYMBOL_MARKDOWN_PARSED = '__$markdownParsed';
|
|
4
|
+
export const SYMBOL_REF_RESOLVED = '__$refResolved';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Resolve a path in a OpenAPI file.
|
|
8
|
+
* It resolves any reference needed to resolve the path, ignoring other references outside the path.
|
|
9
|
+
*/
|
|
10
|
+
export async function resolveOpenAPIPath<T>(
|
|
11
|
+
url: string,
|
|
12
|
+
dataPath: string[],
|
|
13
|
+
fetcher: OpenAPIFetcher,
|
|
14
|
+
): Promise<T | undefined> {
|
|
15
|
+
const data = await fetcher.fetch(url);
|
|
16
|
+
let value: unknown = data;
|
|
17
|
+
|
|
18
|
+
if (!value) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lastKey = dataPath[dataPath.length - 1];
|
|
23
|
+
dataPath = dataPath.slice(0, -1);
|
|
24
|
+
|
|
25
|
+
for (const part of dataPath) {
|
|
26
|
+
// @ts-ignore
|
|
27
|
+
if (isRef(value[part])) {
|
|
28
|
+
await transformAll(url, value, part, fetcher);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// @ts-ignore
|
|
32
|
+
value = value[part];
|
|
33
|
+
|
|
34
|
+
// If any part along the path is undefined, return undefined.
|
|
35
|
+
if (typeof value !== 'object' || value === null) {
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await transformAll(url, value, lastKey, fetcher);
|
|
41
|
+
|
|
42
|
+
// @ts-expect-error
|
|
43
|
+
return value[lastKey] as T;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Recursively process a part of the OpenAPI spec to resolve all references.
|
|
48
|
+
*/
|
|
49
|
+
async function transformAll(
|
|
50
|
+
url: string,
|
|
51
|
+
data: any,
|
|
52
|
+
key: string | number,
|
|
53
|
+
fetcher: OpenAPIFetcher,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
const value = data[key];
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
typeof value === 'string' &&
|
|
59
|
+
key === 'description' &&
|
|
60
|
+
fetcher.parseMarkdown &&
|
|
61
|
+
!data[SYMBOL_MARKDOWN_PARSED]
|
|
62
|
+
) {
|
|
63
|
+
// Parse markdown
|
|
64
|
+
data[SYMBOL_MARKDOWN_PARSED] = true;
|
|
65
|
+
data[key] = await fetcher.parseMarkdown(value);
|
|
66
|
+
} else if (
|
|
67
|
+
typeof value === 'string' ||
|
|
68
|
+
typeof value === 'number' ||
|
|
69
|
+
typeof value === 'boolean' ||
|
|
70
|
+
value === null
|
|
71
|
+
) {
|
|
72
|
+
// Primitives
|
|
73
|
+
} else if (typeof value === 'object' && value !== null && SYMBOL_REF_RESOLVED in value) {
|
|
74
|
+
// Ref was already resolved
|
|
75
|
+
} else if (isRef(value)) {
|
|
76
|
+
const ref = value.$ref;
|
|
77
|
+
|
|
78
|
+
// Delete the ref to avoid infinite loop with circular references
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
delete value.$ref;
|
|
81
|
+
|
|
82
|
+
data[key] = await resolveReference(url, ref, fetcher);
|
|
83
|
+
if (data[key]) {
|
|
84
|
+
data[key][SYMBOL_REF_RESOLVED] = extractRefName(ref);
|
|
85
|
+
}
|
|
86
|
+
} else if (Array.isArray(value)) {
|
|
87
|
+
// Recursively resolve all references in the array
|
|
88
|
+
await Promise.all(value.map((item, index) => transformAll(url, value, index, fetcher)));
|
|
89
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
90
|
+
// Recursively resolve all references in the object
|
|
91
|
+
const keys = Object.keys(value);
|
|
92
|
+
for (const key of keys) {
|
|
93
|
+
await transformAll(url, value, key, fetcher);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function resolveReference(
|
|
99
|
+
origin: string,
|
|
100
|
+
ref: string,
|
|
101
|
+
fetcher: OpenAPIFetcher,
|
|
102
|
+
): Promise<any> {
|
|
103
|
+
const parsed = parseReference(origin, ref);
|
|
104
|
+
return resolveOpenAPIPath(parsed.url, parsed.dataPath, fetcher);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function parseReference(origin: string, ref: string): { url: string; dataPath: string[] } {
|
|
108
|
+
if (!ref) {
|
|
109
|
+
return {
|
|
110
|
+
url: origin,
|
|
111
|
+
dataPath: [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (ref.startsWith('#')) {
|
|
116
|
+
// Local references
|
|
117
|
+
const dataPath = ref.split('/').filter(Boolean).slice(1);
|
|
118
|
+
return {
|
|
119
|
+
url: origin,
|
|
120
|
+
dataPath,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Absolute references
|
|
125
|
+
const url = new URL(ref, origin);
|
|
126
|
+
if (url.hash) {
|
|
127
|
+
const hash = url.hash;
|
|
128
|
+
url.hash = '';
|
|
129
|
+
return parseReference(url.toString(), hash);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
url: url.toString(),
|
|
134
|
+
dataPath: [],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function extractRefName(ref: string): string {
|
|
139
|
+
const parts = ref.split('/');
|
|
140
|
+
return parts[parts.length - 1];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isRef(ref: any): ref is { $ref: string } {
|
|
144
|
+
return typeof ref === 'object' && ref !== null && '$ref' in ref && ref.$ref;
|
|
145
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type IconComponent = React.ComponentType<{ className?: string }>;
|
|
2
|
+
|
|
3
|
+
export interface OpenAPIContextProps extends OpenAPIClientContext {
|
|
4
|
+
CodeBlock: React.ComponentType<{ code: string; syntax: string }>;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface OpenAPIClientContext {
|
|
8
|
+
icons: {
|
|
9
|
+
chevronDown: React.ReactNode;
|
|
10
|
+
chevronRight: React.ReactNode;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Force all sections to be opened by default.
|
|
15
|
+
* @default false
|
|
16
|
+
*/
|
|
17
|
+
defaultInteractiveOpened?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface OpenAPIFetcher {
|
|
21
|
+
/**
|
|
22
|
+
* Fetch an OpenAPI file by its URL. It should return a fully parsed OpenAPI v3 document.
|
|
23
|
+
*/
|
|
24
|
+
fetch: (url: string) => Promise<any>;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parse markdown to the react element to render.
|
|
28
|
+
*/
|
|
29
|
+
parseMarkdown?: (input: string) => Promise<string>;
|
|
30
|
+
}
|
package/src/utils.ts
ADDED