Skip to content

Commit bb3ca9c

Browse files
authored
Implement OpenAPI models blocks (#2908)
1 parent 8ee9757 commit bb3ca9c

20 files changed

+457
-144
lines changed

.changeset/good-dogs-shave.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@gitbook/openapi-parser': minor
3+
'@gitbook/react-openapi': minor
4+
'gitbook': minor
5+
---
6+
7+
Implement OpenAPI models blocks

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"prettier.enable": false,
1212
"editor.defaultFormatter": "biomejs.biome",
1313
"editor.codeActionsOnSave": {
14-
"source.organizeImports.biome": "explicit"
14+
"source.organizeImports.biome": "explicit",
15+
"source.fixAll.biome": "explicit"
1516
}
1617
}

packages/gitbook/src/components/DocumentView/OpenAPI/OpenAPI.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { JSONDocument } from '@gitbook/api';
22
import { Icon } from '@gitbook/icons';
33
import { OpenAPIOperation } from '@gitbook/react-openapi';
44

5-
import { type AnyOpenAPIOperationBlock, resolveOpenAPIBlock } from '@/lib/openapi/fetch';
5+
import { resolveOpenAPIOperationBlock } from '@/lib/openapi/resolveOpenAPIOperationBlock';
66
import { tcls } from '@/lib/tailwind';
77

88
import type { BlockProps } from '../Block';
@@ -12,11 +12,12 @@ import { Heading } from '../Heading';
1212

1313
import './scalar.css';
1414
import './style.css';
15+
import type { AnyOpenAPIBlock } from '@/lib/openapi/types';
1516

1617
/**
1718
* Render an openapi block or an openapi-operation block.
1819
*/
19-
export async function OpenAPI(props: BlockProps<AnyOpenAPIOperationBlock>) {
20+
export async function OpenAPI(props: BlockProps<AnyOpenAPIBlock>) {
2021
const { style } = props;
2122
return (
2223
<div className={tcls('flex w-full', style, 'max-w-full')}>
@@ -25,14 +26,14 @@ export async function OpenAPI(props: BlockProps<AnyOpenAPIOperationBlock>) {
2526
);
2627
}
2728

28-
async function OpenAPIBody(props: BlockProps<AnyOpenAPIOperationBlock>) {
29+
async function OpenAPIBody(props: BlockProps<AnyOpenAPIBlock>) {
2930
const { block, context } = props;
3031

3132
if (!context.contentContext) {
3233
return null;
3334
}
3435

35-
const { data, specUrl, error } = await resolveOpenAPIBlock({
36+
const { data, specUrl, error } = await resolveOpenAPIOperationBlock({
3637
block,
3738
context: context.contentContext,
3839
});

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
@apply flex-1 flex flex-col gap-4 mb-14;
44
}
55

6+
.openapi-models {
7+
@apply flex flex-col mb-14 flex-1;
8+
}
9+
610
.openapi-columns {
711
@apply grid grid-cols-1 lg:grid-cols-2 gap-6 print-mode:grid-cols-1 justify-stretch;
812
}
@@ -615,3 +619,11 @@
615619
.openapi-section-body.openapi-schema.openapi-schema-root {
616620
@apply space-y-2.5;
617621
}
622+
623+
.openapi-section-models {
624+
@apply border border-tint-subtle rounded-lg;
625+
}
626+
627+
.openapi-section-models > .openapi-section-body > .openapi-schema-properties > .openapi-schema {
628+
@apply p-2.5;
629+
}

packages/gitbook/src/lib/document-sections.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type { JSONDocument } from '@gitbook/api';
22
import type { GitBookAnyContext } from '@v2/lib/context';
33

44
import { getNodeText } from './document';
5-
import { resolveOpenAPIBlock } from './openapi/fetch';
5+
import { resolveOpenAPIOperationBlock } from './openapi/resolveOpenAPIOperationBlock';
66

77
export interface DocumentSection {
88
id: string;
@@ -38,7 +38,7 @@ export async function getDocumentSections(
3838
}
3939

4040
if ((block.type === 'swagger' || block.type === 'openapi-operation') && block.meta?.id) {
41-
const { data: operation } = await resolveOpenAPIBlock({
41+
const { data: operation } = await resolveOpenAPIOperationBlock({
4242
block,
4343
context,
4444
});

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

Lines changed: 17 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,40 @@
1-
import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation } from '@gitbook/api';
2-
import { OpenAPIParseError, parseOpenAPI } from '@gitbook/openapi-parser';
3-
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
4-
import type { GitBookAnyContext } from '@v2/lib/context';
1+
import { parseOpenAPI } from '@gitbook/openapi-parser';
52

63
import { type CacheFunctionOptions, cache, noCacheFetchOptions } from '@/lib/cache';
7-
4+
import type { ResolveOpenAPIBlockArgs } from '@/lib/openapi/types';
85
import { assert } from 'ts-essentials';
96
import { resolveContentRef } from '../references';
107
import { isV2 } from '../v2';
118
import { enrichFilesystem } from './enrich';
9+
import type { FetchOpenAPIFilesystemResult } from './types';
1210

13-
export type AnyOpenAPIOperationBlock = DocumentBlockOpenAPI | DocumentBlockOpenAPIOperation;
14-
15-
const weakmap = new WeakMap<AnyOpenAPIOperationBlock, Promise<ResolveOpenAPIBlockResult>>();
16-
17-
/**
18-
* Cache the result of resolving an OpenAPI block.
19-
* It is important because the resolve is called in sections and in the block itself.
20-
*/
21-
export function resolveOpenAPIBlock(
22-
args: ResolveOpenAPIBlockArgs
23-
): Promise<ResolveOpenAPIBlockResult> {
24-
if (weakmap.has(args.block)) {
25-
return weakmap.get(args.block)!;
26-
}
27-
28-
const result = baseResolveOpenAPIBlock(args);
29-
weakmap.set(args.block, result);
30-
return result;
31-
}
32-
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 };
4011
/**
41-
* Resolve OpenAPI block.
12+
* Fetch OpenAPI block.
4213
*/
43-
async function baseResolveOpenAPIBlock(
14+
export async function fetchOpenAPIFilesystem(
4415
args: ResolveOpenAPIBlockArgs
45-
): Promise<ResolveOpenAPIBlockResult> {
16+
): Promise<FetchOpenAPIFilesystemResult> {
4617
const { context, block } = args;
47-
if (!block.data.path || !block.data.method) {
48-
return { data: null, specUrl: null };
49-
}
5018

5119
const ref = block.data.ref;
5220
const resolved = ref ? await resolveContentRef(ref, context) : null;
5321

5422
if (!resolved) {
55-
return { data: null, specUrl: null };
23+
return { filesystem: null, specUrl: null };
5624
}
5725

58-
try {
59-
const filesystem = await (() => {
60-
if (ref.kind === 'openapi') {
61-
assert(resolved.openAPIFilesystem);
62-
return resolved.openAPIFilesystem;
63-
}
64-
return fetchFilesystem(resolved.href);
65-
})();
66-
67-
const data = await resolveOpenAPIOperation(filesystem, {
68-
path: block.data.path,
69-
method: block.data.method,
70-
});
71-
72-
return { data, specUrl: resolved.href };
73-
} catch (error) {
74-
if (error instanceof OpenAPIParseError) {
75-
return { error };
26+
const filesystem = await (() => {
27+
if (ref.kind === 'openapi') {
28+
assert(resolved.openAPIFilesystem);
29+
return resolved.openAPIFilesystem;
7630
}
31+
return fetchFilesystem(resolved.href);
32+
})();
7733

78-
throw error;
79-
}
34+
return {
35+
filesystem,
36+
specUrl: resolved.href,
37+
};
8038
}
8139

8240
function fetchFilesystem(url: string) {
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { fetchOpenAPIFilesystem } from '@/lib/openapi/fetch';
2+
import type { ResolveOpenAPIBlockResult } from '@/lib/openapi/types';
3+
import { OpenAPIParseError } from '@gitbook/openapi-parser';
4+
import { type OpenAPIModelsData, resolveOpenAPIModels } from '@gitbook/react-openapi';
5+
import type { AnyOpenAPIBlock, ResolveOpenAPIBlockArgs } from './types';
6+
7+
type ResolveOpenAPIModelsBlockResult = ResolveOpenAPIBlockResult<OpenAPIModelsData>;
8+
9+
const weakmap = new WeakMap<AnyOpenAPIBlock, Promise<ResolveOpenAPIModelsBlockResult>>();
10+
11+
/**
12+
* Cache the result of resolving an OpenAPI block.
13+
* It is important because the resolve is called in sections and in the block itself.
14+
*/
15+
export function resolveOpenAPIModelsBlock(
16+
args: ResolveOpenAPIBlockArgs
17+
): Promise<ResolveOpenAPIModelsBlockResult> {
18+
if (weakmap.has(args.block)) {
19+
return weakmap.get(args.block)!;
20+
}
21+
22+
const result = baseResolveOpenAPIModelsBlock(args);
23+
weakmap.set(args.block, result);
24+
return result;
25+
}
26+
27+
/**
28+
* Resolve OpenAPI models block.
29+
*/
30+
async function baseResolveOpenAPIModelsBlock(
31+
args: ResolveOpenAPIBlockArgs
32+
): Promise<ResolveOpenAPIModelsBlockResult> {
33+
const { context, block } = args;
34+
if (!block.data.path || !block.data.method) {
35+
return { data: null, specUrl: null };
36+
}
37+
38+
try {
39+
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
40+
41+
if (!filesystem || !specUrl) {
42+
return { data: null, specUrl: null };
43+
}
44+
45+
const data = await resolveOpenAPIModels(filesystem);
46+
47+
return { data, specUrl };
48+
} catch (error) {
49+
if (error instanceof OpenAPIParseError) {
50+
return { error };
51+
}
52+
53+
throw error;
54+
}
55+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { fetchOpenAPIFilesystem } from '@/lib/openapi/fetch';
2+
import { OpenAPIParseError } from '@gitbook/openapi-parser';
3+
import { type OpenAPIOperationData, resolveOpenAPIOperation } from '@gitbook/react-openapi';
4+
import type { AnyOpenAPIBlock, ResolveOpenAPIBlockArgs, ResolveOpenAPIBlockResult } from './types';
5+
6+
type ResolveOpenAPIOperationBlockResult = ResolveOpenAPIBlockResult<OpenAPIOperationData>;
7+
8+
const weakmap = new WeakMap<AnyOpenAPIBlock, Promise<ResolveOpenAPIOperationBlockResult>>();
9+
10+
/**
11+
* Cache the result of resolving an OpenAPI block.
12+
* It is important because the resolve is called in sections and in the block itself.
13+
*/
14+
export function resolveOpenAPIOperationBlock(
15+
args: ResolveOpenAPIBlockArgs
16+
): Promise<ResolveOpenAPIOperationBlockResult> {
17+
if (weakmap.has(args.block)) {
18+
return weakmap.get(args.block)!;
19+
}
20+
21+
const result = baseResolveOpenAPIOperationBlock(args);
22+
weakmap.set(args.block, result);
23+
return result;
24+
}
25+
26+
/**
27+
* Resolve OpenAPI operation block.
28+
*/
29+
async function baseResolveOpenAPIOperationBlock(
30+
args: ResolveOpenAPIBlockArgs
31+
): Promise<ResolveOpenAPIOperationBlockResult> {
32+
const { context, block } = args;
33+
if (!block.data.path || !block.data.method) {
34+
return { data: null, specUrl: null };
35+
}
36+
37+
try {
38+
const { filesystem, specUrl } = await fetchOpenAPIFilesystem({ block, context });
39+
40+
if (!filesystem) {
41+
return { data: null, specUrl: null };
42+
}
43+
44+
const data = await resolveOpenAPIOperation(filesystem, {
45+
path: block.data.path,
46+
method: block.data.method,
47+
});
48+
49+
return { data, specUrl };
50+
} catch (error) {
51+
if (error instanceof OpenAPIParseError) {
52+
return { error };
53+
}
54+
55+
throw error;
56+
}
57+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import type { DocumentBlockOpenAPI, DocumentBlockOpenAPIOperation } from '@gitbook/api';
2+
import type { Filesystem, OpenAPIParseError, OpenAPIV3xDocument } from '@gitbook/openapi-parser';
3+
import type { GitBookAnyContext } from '@v2/lib/context';
4+
5+
//!!TODO: Add DocumentBlockOpenAPIModels when available in @gitbook/api
6+
export type AnyOpenAPIBlock = DocumentBlockOpenAPI | DocumentBlockOpenAPIOperation;
7+
8+
/**
9+
* Arguments for resolving OpenAPI block.
10+
*/
11+
export type ResolveOpenAPIBlockArgs = {
12+
block: AnyOpenAPIBlock;
13+
context: GitBookAnyContext;
14+
};
15+
16+
/**
17+
* Fetch OpenAPI filesystem result.
18+
*/
19+
export type FetchOpenAPIFilesystemResult =
20+
| {
21+
error?: undefined;
22+
filesystem: Filesystem<OpenAPIV3xDocument> | null;
23+
specUrl: string | null;
24+
}
25+
| FetchOpenAPIFilesystemError;
26+
27+
/**
28+
* Fetch OpenAPI filesystem error.
29+
*/
30+
type FetchOpenAPIFilesystemError = {
31+
error: OpenAPIParseError;
32+
filesystem?: undefined;
33+
specUrl?: undefined;
34+
};
35+
36+
/**
37+
* Resolved OpenAPI block result.
38+
*/
39+
export type ResolveOpenAPIBlockResult<T> =
40+
| { error?: undefined; data: T | null; specUrl: string | null }
41+
| ResolveOpenAPIBlockError;
42+
43+
/**
44+
* Resolved OpenAPI block error.
45+
*/
46+
type ResolveOpenAPIBlockError = {
47+
error: OpenAPIParseError;
48+
data?: undefined;
49+
specUrl?: undefined;
50+
};

packages/openapi-parser/src/traverse.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,20 @@ export async function traverse<T extends AnyObject | AnyObject[]>(
2929
}
3030

3131
const keys = Object.keys(specification);
32-
await Promise.all(
33-
keys.map(async (key) => {
32+
const results = await Promise.all(
33+
keys.map(async (key, index) => {
3434
const value = specification[key];
35-
result[key] = await traverse(value, transform, [...path, key], seen);
35+
const processed = await traverse(value, transform, [...path, key], seen);
36+
return { key, value: processed, index };
3637
})
3738
);
3839

40+
// Promise.all does not guarantee the order of the results
41+
// So we need to sort them to preserve the original order
42+
results.sort((a, b) => a.index - b.index);
43+
for (const { key, value } of results) {
44+
result[key] = value;
45+
}
46+
3947
return transform(result, path) as Promise<T>;
4048
}

0 commit comments

Comments
 (0)