@gitbook/react-openapi 1.0.5 → 1.1.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 (40) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/OpenAPIDisclosure.d.ts +5 -9
  3. package/dist/OpenAPIDisclosure.jsx +24 -27
  4. package/dist/OpenAPIDisclosureGroup.d.ts +1 -1
  5. package/dist/OpenAPIDisclosureGroup.jsx +5 -5
  6. package/dist/OpenAPIPath.jsx +5 -1
  7. package/dist/OpenAPISchema.d.ts +3 -26
  8. package/dist/OpenAPISchema.jsx +80 -131
  9. package/dist/ScalarApiButton.d.ts +3 -2
  10. package/dist/ScalarApiButton.jsx +22 -18
  11. package/dist/dereference.d.ts +5 -0
  12. package/dist/dereference.js +68 -0
  13. package/dist/index.d.ts +3 -2
  14. package/dist/index.js +2 -1
  15. package/dist/models/OpenAPIModels.d.ts +9 -0
  16. package/dist/models/OpenAPIModels.jsx +62 -0
  17. package/dist/models/index.d.ts +2 -0
  18. package/dist/models/index.js +2 -0
  19. package/dist/models/resolveOpenAPIModels.d.ts +7 -0
  20. package/dist/models/resolveOpenAPIModels.js +73 -0
  21. package/dist/resolveOpenAPIOperation.d.ts +2 -2
  22. package/dist/resolveOpenAPIOperation.js +3 -34
  23. package/dist/tsconfig.build.tsbuildinfo +1 -1
  24. package/dist/types.d.ts +8 -0
  25. package/dist/utils.js +42 -3
  26. package/package.json +3 -3
  27. package/src/OpenAPIDisclosure.tsx +34 -42
  28. package/src/OpenAPIDisclosureGroup.tsx +2 -2
  29. package/src/OpenAPIPath.tsx +7 -1
  30. package/src/OpenAPISchema.test.ts +26 -35
  31. package/src/OpenAPISchema.tsx +136 -225
  32. package/src/ScalarApiButton.tsx +26 -28
  33. package/src/dereference.ts +29 -0
  34. package/src/index.ts +3 -2
  35. package/src/models/OpenAPIModels.tsx +89 -0
  36. package/src/models/index.ts +2 -0
  37. package/src/models/resolveOpenAPIModels.ts +35 -0
  38. package/src/resolveOpenAPIOperation.ts +8 -36
  39. package/src/types.ts +10 -0
  40. package/src/utils.ts +51 -3
package/dist/types.d.ts CHANGED
@@ -51,3 +51,11 @@ export interface OpenAPIOperationData extends OpenAPICustomSpecProperties {
51
51
  /** Securities that should be used for this operation */
52
52
  securities: [string, OpenAPIV3.SecuritySchemeObject][];
53
53
  }
54
+ export type OpenAPIModel = {
55
+ name: string;
56
+ schema: OpenAPIV3.SchemaObject;
57
+ };
58
+ export interface OpenAPIModelsData {
59
+ /** Components schemas to be used for models */
60
+ models: OpenAPIModel[];
61
+ }
package/dist/utils.js CHANGED
@@ -9,17 +9,25 @@ var __assign = (this && this.__assign) || function () {
9
9
  };
10
10
  return __assign.apply(this, arguments);
11
11
  };
12
+ import { stringifyOpenAPI } from './stringifyOpenAPI';
12
13
  export function checkIsReference(input) {
13
14
  return typeof input === 'object' && !!input && '$ref' in input;
14
15
  }
15
16
  export function createStateKey(key, scope) {
16
17
  return scope ? "".concat(scope, "_").concat(key) : key;
17
18
  }
19
+ /**
20
+ * Check if an object has a description. Either at the root level or in items.
21
+ */
22
+ function hasDescription(object) {
23
+ return 'description' in object || 'x-gitbook-description-html' in object;
24
+ }
18
25
  /**
19
26
  * Resolve the description of an object.
20
27
  */
21
28
  export function resolveDescription(object) {
22
- if ('items' in object && object.items) {
29
+ // If the object has items and has a description, we resolve the description from items
30
+ if ('items' in object && typeof object.items === 'object' && hasDescription(object.items)) {
23
31
  return resolveDescription(object.items);
24
32
  }
25
33
  return 'x-gitbook-description-html' in object &&
@@ -51,8 +59,13 @@ export function resolveFirstExample(object) {
51
59
  return object.examples[firstKey];
52
60
  }
53
61
  }
54
- if ('example' in object && object.example !== undefined) {
55
- return object.example;
62
+ // Resolve top level example first
63
+ if (shouldDisplayExample(object)) {
64
+ return formatExample(object.example);
65
+ }
66
+ // Resolve example from items if it exists
67
+ if (object.items && typeof object.items === 'object') {
68
+ return formatExample(object.items.example);
56
69
  }
57
70
  return undefined;
58
71
  }
@@ -84,3 +97,29 @@ export function parameterToProperty(parameter) {
84
97
  required: parameter.required,
85
98
  };
86
99
  }
100
+ /**
101
+ * Format the example of a schema.
102
+ */
103
+ function formatExample(example) {
104
+ if (typeof example === 'string') {
105
+ return example
106
+ .replace(/\n/g, ' ') // Replace newlines with spaces
107
+ .replace(/\s+/g, ' ') // Collapse multiple spaces/newlines into a single space
108
+ .replace(/([\{\}:,])\s+/g, '$1 ') // Ensure a space after {, }, :, and ,
109
+ .replace(/\s+([\{\}:,])/g, ' $1') // Ensure a space before {, }, :, and ,
110
+ .trim();
111
+ }
112
+ return stringifyOpenAPI(example);
113
+ }
114
+ /**
115
+ * Check if an example should be displayed.
116
+ */
117
+ function shouldDisplayExample(schema) {
118
+ return ((typeof schema.example === 'string' && !!schema.example) ||
119
+ typeof schema.example === 'number' ||
120
+ typeof schema.example === 'boolean' ||
121
+ (Array.isArray(schema.example) && schema.example.length > 0) ||
122
+ (typeof schema.example === 'object' &&
123
+ schema.example !== null &&
124
+ Object.keys(schema.example).length > 0));
125
+ }
package/package.json CHANGED
@@ -8,12 +8,12 @@
8
8
  "default": "./dist/index.js"
9
9
  }
10
10
  },
11
- "version": "1.0.5",
11
+ "version": "1.1.0",
12
12
  "sideEffects": false,
13
13
  "dependencies": {
14
14
  "@gitbook/openapi-parser": "workspace:*",
15
- "@scalar/api-client-react": "1.0.87",
16
- "@scalar/oas-utils": "^0.2.101",
15
+ "@scalar/api-client-react": "^1.1.36",
16
+ "@scalar/oas-utils": "^0.2.110",
17
17
  "clsx": "^2.1.1",
18
18
  "flatted": "^3.2.9",
19
19
  "json-xml-parse": "^1.3.0",
@@ -1,52 +1,44 @@
1
1
  'use client';
2
- import type React from 'react';
3
- import { useRef } from 'react';
4
- import { mergeProps, useButton, useDisclosure, useFocusRing } from 'react-aria';
5
- import { useDisclosureState } from 'react-stately';
2
+ import { useState } from 'react';
3
+ import { Button, Disclosure, DisclosurePanel, Heading } from 'react-aria-components';
6
4
  import type { OpenAPIClientContext } from './types';
7
5
 
8
- interface Props {
9
- context: OpenAPIClientContext;
10
- children: React.ReactNode;
11
- label?: string;
12
- }
13
-
14
6
  /**
15
7
  * Display an interactive OpenAPI disclosure.
16
- * The label is optional and defaults to "child attributes".
17
8
  */
18
- export function OpenAPIDisclosure({ context, children, label }: Props): React.JSX.Element {
19
- const state = useDisclosureState({});
20
- const panelRef = useRef<HTMLDivElement | null>(null);
21
- const triggerRef = useRef<HTMLButtonElement | null>(null);
22
- const { buttonProps: triggerProps, panelProps } = useDisclosure({}, state, panelRef);
23
- const { buttonProps } = useButton(triggerProps, triggerRef);
24
- const { isFocusVisible, focusProps } = useFocusRing();
9
+ export function OpenAPIDisclosure(props: {
10
+ context: OpenAPIClientContext;
11
+ children: React.ReactNode;
12
+ label: string;
13
+ }): React.JSX.Element {
14
+ const { context, children, label } = props;
15
+ const [isExpanded, setIsExpanded] = useState(false);
25
16
 
26
17
  return (
27
- <div className="openapi-disclosure">
28
- <button
29
- ref={triggerRef}
30
- {...mergeProps(buttonProps, focusProps)}
31
- slot="trigger"
32
- className="openapi-disclosure-trigger"
33
- style={{
34
- outline: isFocusVisible
35
- ? '2px solid rgb(var(--primary-color-500) / 0.4)'
36
- : 'none',
37
- }}
38
- >
39
- {context.icons.plus}
40
- <span>
41
- {`${state.isExpanded ? 'Hide' : 'Show'} ${label ? label : 'child attributes'}`}
42
- </span>
43
- </button>
44
-
45
- {state.isExpanded && (
46
- <div ref={panelRef} {...panelProps} className="openapi-disclosure-panel">
47
- {children}
48
- </div>
49
- )}
50
- </div>
18
+ <Disclosure
19
+ className="openapi-disclosure"
20
+ isExpanded={isExpanded}
21
+ onExpandedChange={setIsExpanded}
22
+ >
23
+ <Heading>
24
+ <Button
25
+ slot="trigger"
26
+ className="openapi-disclosure-trigger"
27
+ style={({ isFocusVisible }) => ({
28
+ outline: isFocusVisible
29
+ ? '2px solid rgb(var(--primary-color-500) / 0.4)'
30
+ : 'none',
31
+ })}
32
+ >
33
+ {context.icons.plus}
34
+ <span>
35
+ {isExpanded ? 'Hide' : 'Show'} {label}
36
+ </span>
37
+ </Button>
38
+ </Heading>
39
+ <DisclosurePanel className="openapi-disclosure-panel">
40
+ {isExpanded ? children : null}
41
+ </DisclosurePanel>
42
+ </Disclosure>
51
43
  );
52
44
  }
@@ -19,7 +19,7 @@ type TDisclosureGroup = {
19
19
  label: string | React.ReactNode;
20
20
  tabs?: {
21
21
  id: string;
22
- label: string | React.ReactNode;
22
+ label?: string | React.ReactNode;
23
23
  body?: React.ReactNode;
24
24
  }[];
25
25
  };
@@ -121,7 +121,7 @@ function DisclosureItem(props: { group: TDisclosureGroup; icon?: React.ReactNode
121
121
  </option>
122
122
  ))}
123
123
  </select>
124
- ) : group.tabs[0] ? (
124
+ ) : group.tabs[0]?.label ? (
125
125
  <span>{group.tabs[0].label}</span>
126
126
  ) : null}
127
127
  </div>
@@ -1,3 +1,4 @@
1
+ import type { OpenAPIV3_1 } from '@gitbook/openapi-parser';
1
2
  import type React from 'react';
2
3
  import { ScalarApiButton } from './ScalarApiButton';
3
4
  import type { OpenAPIContextProps, OpenAPIOperationData } from './types';
@@ -12,6 +13,7 @@ export function OpenAPIPath(props: {
12
13
  const { data, context } = props;
13
14
  const { method, path } = data;
14
15
  const { specUrl } = context;
16
+ const hideTryItPanel = data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'];
15
17
 
16
18
  return (
17
19
  <div className="openapi-path">
@@ -19,13 +21,17 @@ export function OpenAPIPath(props: {
19
21
  <div className="openapi-path-title" data-deprecated={data.operation.deprecated}>
20
22
  <p>{formatPath(path)}</p>
21
23
  </div>
22
- {data['x-hideTryItPanel'] || data.operation['x-hideTryItPanel'] ? null : (
24
+ {!hideTryItPanel && validateHttpMethod(method) && (
23
25
  <ScalarApiButton method={method} path={path} specUrl={specUrl} />
24
26
  )}
25
27
  </div>
26
28
  );
27
29
  }
28
30
 
31
+ function validateHttpMethod(method: string): method is OpenAPIV3_1.HttpMethods {
32
+ return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'trace'].includes(method);
33
+ }
34
+
29
35
  // Format the path to highlight placeholders
30
36
  function formatPath(path: string) {
31
37
  // Matches placeholders like {id}, {userId}, etc.
@@ -23,18 +23,15 @@ describe('getSchemaAlternatives', () => {
23
23
  ],
24
24
  })
25
25
  ).toEqual([
26
- [
27
- {
28
- type: 'number',
29
- },
30
- {
31
- type: 'boolean',
32
- },
33
- {
34
- type: 'string',
35
- },
36
- ],
37
- undefined,
26
+ {
27
+ type: 'number',
28
+ },
29
+ {
30
+ type: 'boolean',
31
+ },
32
+ {
33
+ type: 'string',
34
+ },
38
35
  ]);
39
36
  });
40
37
 
@@ -58,22 +55,19 @@ describe('getSchemaAlternatives', () => {
58
55
  ],
59
56
  })
60
57
  ).toEqual([
61
- [
62
- {
63
- allOf: [
64
- {
65
- type: 'number',
66
- },
67
- {
68
- type: 'boolean',
69
- },
70
- ],
71
- },
72
- {
73
- type: 'string',
74
- },
75
- ],
76
- undefined,
58
+ {
59
+ allOf: [
60
+ {
61
+ type: 'number',
62
+ },
63
+ {
64
+ type: 'boolean',
65
+ },
66
+ ],
67
+ },
68
+ {
69
+ type: 'string',
70
+ },
77
71
  ]);
78
72
  });
79
73
 
@@ -89,13 +83,10 @@ describe('getSchemaAlternatives', () => {
89
83
  a.anyOf?.push(a);
90
84
 
91
85
  expect(getSchemaAlternatives(a)).toEqual([
92
- [
93
- {
94
- type: 'string',
95
- },
96
- a,
97
- ],
98
- undefined,
86
+ {
87
+ type: 'string',
88
+ },
89
+ a,
99
90
  ]);
100
91
  });
101
92
  });