Skip to content

Commit d7200df

Browse files
committed
wip
1 parent e345f9c commit d7200df

File tree

10 files changed

+233
-57
lines changed

10 files changed

+233
-57
lines changed

packages/gitbook/src/components/DocumentView/OpenAPI/style.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -619,3 +619,15 @@
619619
.openapi-section-body.openapi-schema.openapi-schema-root {
620620
@apply space-y-2.5;
621621
}
622+
623+
.openapi-models-properties {
624+
@apply border border-tint-subtle rounded-lg;
625+
}
626+
627+
.openapi-models-properties .openapi-schema-body {
628+
@apply p-2.5;
629+
}
630+
631+
.openapi-models .openapi-disclosure-group-panel > .openapi-section > .openapi-schema-body {
632+
@apply py-0;
633+
}

packages/gitbook/src/lib/openapi/fetch.ts

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation } from '@gitbook/api';
22
import { OpenAPIParseError, parseOpenAPI } from '@gitbook/openapi-parser';
3-
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
3+
import {
4+
type OpenAPIModelsData,
5+
type OpenAPIOperationData,
6+
resolveOpenAPIModels,
7+
resolveOpenAPIOperation,
8+
} from '@gitbook/react-openapi';
49
import type { GitBookAnyContext } from '@v2/lib/context';
510

611
import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache';
@@ -12,47 +17,71 @@ import { enrichFilesystem } from './enrich';
1217

1318
export type AnyOpenAPIOperationBlock = DocumentBlockOpenAPI | DocumentBlockOpenAPIOperation;
1419

20+
export type OpenAPIBlockType = 'operation' | 'models';
21+
22+
export type ResolveOpenAPIOperationBlockResult = {
23+
error: undefined;
24+
data: OpenAPIOperationData | null;
25+
specUrl: string | null;
26+
};
27+
28+
export type ResolveOpenAPIModelsBlockResult = {
29+
error: undefined;
30+
data: OpenAPIModelsData | null;
31+
specUrl: string | null;
32+
};
33+
34+
export type ResolveOpenAPIBlockResult<T extends OpenAPIBlockType = OpenAPIBlockType> =
35+
| (T extends 'operation' ? ResolveOpenAPIOperationBlockResult : ResolveOpenAPIModelsBlockResult)
36+
| ResolveOpenAPIBlockError;
37+
38+
type ResolveOpenAPIBlockError = { error: OpenAPIParseError; data?: undefined; specUrl?: undefined };
39+
40+
type ResolveOpenAPIBlockArgs<T extends OpenAPIBlockType = OpenAPIBlockType> = {
41+
block: AnyOpenAPIOperationBlock;
42+
context: GitBookAnyContext;
43+
/**
44+
* The type of the block.
45+
* Can be either 'operation' or 'models'.
46+
* @default 'operation'
47+
*/
48+
type?: T | OpenAPIBlockType;
49+
};
50+
1551
const weakmap = new WeakMap<AnyOpenAPIOperationBlock, Promise<ResolveOpenAPIBlockResult>>();
1652

1753
/**
1854
* Cache the result of resolving an OpenAPI block.
1955
* It is important because the resolve is called in sections and in the block itself.
2056
*/
21-
export function resolveOpenAPIBlock(
22-
args: ResolveOpenAPIBlockArgs
23-
): Promise<ResolveOpenAPIBlockResult> {
24-
if (weakmap.has(args.block)) {
25-
return weakmap.get(args.block)!;
26-
}
57+
export function resolveOpenAPIBlock<T extends OpenAPIBlockType = 'operation'>(
58+
args: ResolveOpenAPIBlockArgs<T>
59+
): Promise<ResolveOpenAPIBlockResult<T>> {
60+
// if (weakmap.has(args.block)) {
61+
// return weakmap.get(args.block) as Promise<ResolveOpenAPIBlockResult<T>>;
62+
// }
2763

2864
const result = baseResolveOpenAPIBlock(args);
29-
weakmap.set(args.block, result);
65+
weakmap.set(args.block, result as Promise<ResolveOpenAPIBlockResult<T>>);
3066
return result;
3167
}
3268

33-
type ResolveOpenAPIBlockArgs = {
34-
block: AnyOpenAPIOperationBlock;
35-
context: GitBookAnyContext;
36-
};
37-
export type ResolveOpenAPIBlockResult =
38-
| { error?: undefined; data: OpenAPIOperationData | null; specUrl: string | null }
39-
| { error: OpenAPIParseError; data?: undefined; specUrl?: undefined };
4069
/**
4170
* Resolve OpenAPI block.
4271
*/
43-
async function baseResolveOpenAPIBlock(
44-
args: ResolveOpenAPIBlockArgs
45-
): Promise<ResolveOpenAPIBlockResult> {
46-
const { context, block } = args;
72+
async function baseResolveOpenAPIBlock<T extends OpenAPIBlockType = 'operation'>(
73+
args: ResolveOpenAPIBlockArgs<T>
74+
): Promise<ResolveOpenAPIBlockResult<T>> {
75+
const { context, block, type = 'operation' } = args;
4776
if (!block.data.path || !block.data.method) {
48-
return { data: null, specUrl: null };
77+
return { data: null, specUrl: null } as ResolveOpenAPIBlockResult<T>;
4978
}
5079

5180
const ref = block.data.ref;
5281
const resolved = ref ? await resolveContentRef(ref, context) : null;
5382

5483
if (!resolved) {
55-
return { data: null, specUrl: null };
84+
return { data: null, specUrl: null } as ResolveOpenAPIBlockResult<T>;
5685
}
5786

5887
try {
@@ -64,12 +93,17 @@ async function baseResolveOpenAPIBlock(
6493
return fetchFilesystem(resolved.href);
6594
})();
6695

96+
if (type === 'models') {
97+
const data = await resolveOpenAPIModels(filesystem);
98+
return { data: data, specUrl: resolved.href } as ResolveOpenAPIBlockResult<T>;
99+
}
100+
67101
const data = await resolveOpenAPIOperation(filesystem, {
68102
path: block.data.path,
69103
method: block.data.method,
70104
});
71105

72-
return { data, specUrl: resolved.href };
106+
return { data, specUrl: resolved.href } as ResolveOpenAPIBlockResult<T>;
73107
} catch (error) {
74108
if (error instanceof OpenAPIParseError) {
75109
return { error };

packages/react-openapi/src/OpenAPISchema.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ function OpenAPISchemaAlternative(props: {
166166
/**
167167
* Render a circular reference to a schema.
168168
*/
169-
function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaObject }) {
169+
export function OpenAPISchemaCircularRef(props: { id: string; schema: OpenAPIV3.SchemaObject }) {
170170
const { id, schema } = props;
171171

172172
return (
@@ -198,12 +198,9 @@ export function OpenAPISchemaEnum(props: { enumValues: any[] }) {
198198
);
199199
}
200200

201-
export function OpenAPISchemaPresentation(
202-
props: { property: OpenAPISchemaPropertyEntry } & { showType?: boolean }
203-
) {
201+
export function OpenAPISchemaPresentation(props: { property: OpenAPISchemaPropertyEntry }) {
204202
const {
205203
property: { schema, propertyName, required },
206-
showType = true,
207204
} = props;
208205

209206
const description = resolveDescription(schema);
@@ -213,7 +210,7 @@ export function OpenAPISchemaPresentation(
213210
<div className="openapi-schema-presentation">
214211
<OpenAPISchemaName
215212
schema={schema}
216-
type={showType ? getSchemaTitle(schema) : undefined}
213+
type={getSchemaTitle(schema)}
217214
propertyName={propertyName}
218215
required={required}
219216
/>

packages/react-openapi/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export * from './models';
22
export * from './OpenAPIOperation';
33
export * from './OpenAPIOperationContext';
4-
export * from './resolveOpenAPIOperation';
5-
export type { OpenAPIOperationData } from './types';
4+
export { resolveOpenAPIOperation } from './resolveOpenAPIOperation';
5+
export type { OpenAPIModelsData, OpenAPIOperationData } from './types';
Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,96 @@
1-
export function OpenAPIModelSchema() {
2-
return <></>;
1+
import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2+
import clsx from 'clsx';
3+
import { useId } from 'react';
4+
import { InteractiveSection } from '../InteractiveSection';
5+
import {
6+
OpenAPISchemaAlternativesItem,
7+
OpenAPISchemaCircularRef,
8+
OpenAPISchemaProperties,
9+
OpenAPISchemaProperty,
10+
type OpenAPISchemaPropertyEntry,
11+
getSchemaAlternatives,
12+
getSchemaProperties,
13+
} from '../OpenAPISchema';
14+
import type { OpenAPIClientContext } from '../types';
15+
16+
type CircularRefsIds = Map<OpenAPIV3.SchemaObject, string>;
17+
18+
export function OpenAPIModelSchema(
19+
props: OpenAPISchemaPropertyEntry & {
20+
/** Set of objects already observed as parents */
21+
circularRefs?: CircularRefsIds;
22+
context: OpenAPIClientContext;
23+
className?: string;
24+
}
25+
) {
26+
const {
27+
schema,
28+
circularRefs: parentCircularRefs = new Map<OpenAPIV3.SchemaObject, string>(),
29+
context,
30+
className,
31+
} = props;
32+
33+
const id = useId();
34+
35+
const parentCircularRef = parentCircularRefs.get(schema);
36+
const circularRefs = new Map(parentCircularRefs).set(schema, id);
37+
38+
// Avoid recursing infinitely, and instead render a link to the parent schema
39+
const properties = parentCircularRef ? null : getSchemaProperties(schema);
40+
const alternatives = parentCircularRef
41+
? null
42+
: getSchemaAlternatives(schema, new Set(circularRefs.keys()));
43+
44+
if (alternatives?.[0].length) {
45+
return (
46+
<OpenAPISchemaAlternativesItem
47+
schema={schema}
48+
circularRefs={circularRefs}
49+
context={context}
50+
alternatives={alternatives}
51+
/>
52+
);
53+
}
54+
55+
if ((properties && properties.length > 0) || schema.type === 'object') {
56+
return (
57+
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
58+
{properties && properties.length > 0 ? (
59+
<div className="openapi-models-properties">
60+
<OpenAPISchemaProperties
61+
properties={properties}
62+
circularRefs={circularRefs}
63+
context={context}
64+
/>
65+
</div>
66+
) : null}
67+
{parentCircularRef ? (
68+
<OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
69+
) : null}
70+
</InteractiveSection>
71+
);
72+
}
73+
74+
return (
75+
<InteractiveSection id={id} className={clsx('openapi-schema', className)}>
76+
{(properties && properties.length > 0) ||
77+
(schema.enum && schema.enum.length > 0) ||
78+
parentCircularRef ? (
79+
<div className="openapi-models-properties">
80+
{properties?.length ? (
81+
<OpenAPISchemaProperties
82+
properties={properties}
83+
circularRefs={circularRefs}
84+
context={context}
85+
/>
86+
) : (
87+
<OpenAPISchemaProperty schema={schema} context={context} />
88+
)}
89+
{parentCircularRef ? (
90+
<OpenAPISchemaCircularRef id={parentCircularRef} schema={schema} />
91+
) : null}
92+
</div>
93+
) : null}
94+
</InteractiveSection>
95+
);
396
}

packages/react-openapi/src/models/OpenAPIModels.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
import clsx from 'clsx';
22

33
import { OpenAPIDisclosureGroup } from '../OpenAPIDisclosureGroup';
4-
import { OpenAPISchemaProperty } from '../OpenAPISchema';
5-
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIOperationData } from '../types';
4+
import type { OpenAPIClientContext, OpenAPIContextProps, OpenAPIModelsData } from '../types';
5+
import { OpenAPIModelSchema } from './OpenAPIModelSchema';
66

77
/**
88
* Display OpenAPI Models.
99
*/
1010
export function OpenAPIModels(props: {
1111
className?: string;
12-
data: OpenAPIOperationData;
12+
data: OpenAPIModelsData;
1313
context: OpenAPIContextProps;
1414
}) {
1515
const { className, data, context } = props;
16-
const { components } = data;
16+
const { models } = data;
1717

1818
const clientContext: OpenAPIClientContext = {
1919
defaultInteractiveOpened: context.defaultInteractiveOpened,
@@ -23,18 +23,18 @@ export function OpenAPIModels(props: {
2323

2424
return (
2525
<div className={clsx('openapi-models', className)}>
26-
{components.length ? (
26+
{models.length ? (
2727
<OpenAPIDisclosureGroup
2828
allowsMultipleExpanded
2929
icon={context.icons.chevronRight}
30-
groups={components.map(([name, schema]) => ({
30+
groups={models.map(([name, schema]) => ({
3131
id: name,
3232
label: (
3333
<div className="openapi-response-tab-content" key={`model-${name}`}>
3434
<span className="openapi-response-statuscode">{name}</span>
3535
</div>
3636
),
37-
body: <OpenAPISchemaProperty schema={schema} context={clientContext} />,
37+
body: <OpenAPIModelSchema schema={schema} context={clientContext} />,
3838
}))}
3939
/>
4040
) : null}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
export { OpenAPIModels } from './OpenAPIModels';
2+
export * from './resolveOpenAPIModels';
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { fromJSON, toJSON } from 'flatted';
2+
3+
import {
4+
type Filesystem,
5+
type OpenAPIV3,
6+
type OpenAPIV3_1,
7+
type OpenAPIV3xDocument,
8+
shouldIgnoreEntity,
9+
} from '@gitbook/openapi-parser';
10+
import { memoDereferenceFilesystem } from '../resolveOpenAPIOperation';
11+
import type { OpenAPIModelsData } from '../types';
12+
13+
export { fromJSON, toJSON };
14+
15+
/**
16+
* Resolve an OpenAPI models from a file and compile it to a more usable format.
17+
* Models are extracted from the OpenAPI components.schemas
18+
*/
19+
export async function resolveOpenAPIModels(
20+
filesystem: Filesystem<OpenAPIV3xDocument>
21+
// operationDescriptor: {
22+
// path: string;
23+
// method: string;
24+
// }
25+
): Promise<OpenAPIModelsData | null> {
26+
// const { path, method } = operationDescriptor;
27+
const schema = await memoDereferenceFilesystem(filesystem);
28+
29+
if (
30+
!schema.components ||
31+
!schema.components.schemas ||
32+
!Object.keys(schema.components.schemas).length
33+
) {
34+
return null;
35+
}
36+
37+
let models: OpenAPIModelsData['models'] = [];
38+
39+
models = getOpenAPIComponents(schema);
40+
41+
return { models };
42+
}
43+
44+
/**
45+
* Get OpenAPI components.schemas that are not ignored.
46+
*/
47+
function getOpenAPIComponents(
48+
schema: OpenAPIV3.Document | OpenAPIV3_1.Document
49+
): [string, OpenAPIV3.SchemaObject][] {
50+
const schemas = schema.components?.schemas ?? {};
51+
return Object.entries(schemas).filter(([, schema]) => !shouldIgnoreEntity(schema));
52+
}

0 commit comments

Comments
 (0)