@gitbook/react-openapi 1.1.5 → 1.1.6

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.
@@ -1,5 +1,5 @@
1
1
  import type { OpenAPIV3 } from '@gitbook/openapi-parser';
2
- import { getExampleFromSchema } from '@scalar/oas-utils/spec-getters';
2
+ import { checkIsReference } from './utils';
3
3
 
4
4
  type JSONValue = string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue };
5
5
 
@@ -20,19 +20,6 @@ export function generateSchemaExample(
20
20
  schema,
21
21
  {
22
22
  emptyString: 'text',
23
- variables: {
24
- 'date-time': new Date().toISOString(),
25
- date: new Date().toISOString().split('T')[0],
26
- email: 'name@gmail.com',
27
- hostname: 'example.com',
28
- ipv4: '0.0.0.0',
29
- ipv6: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
30
- uri: 'https://example.com',
31
- uuid: '123e4567-e89b-12d3-a456-426614174000',
32
- binary: 'binary',
33
- byte: 'Ynl0ZXM=',
34
- password: 'password',
35
- },
36
23
  ...options,
37
24
  },
38
25
  3 // Max depth for circular references
@@ -42,27 +29,427 @@ export function generateSchemaExample(
42
29
  /**
43
30
  * Generate an example for a media type.
44
31
  */
45
- export function generateMediaTypeExample(
32
+ export function generateMediaTypeExamples(
46
33
  mediaType: OpenAPIV3.MediaTypeObject,
47
34
  options?: GenerateSchemaExampleOptions
48
- ): JSONValue | undefined {
35
+ ): OpenAPIV3.ExampleObject[] {
49
36
  if (mediaType.example) {
50
- return mediaType.example;
37
+ return [{ summary: 'default', value: mediaType.example }];
51
38
  }
52
39
 
53
40
  if (mediaType.examples) {
54
- const key = Object.keys(mediaType.examples)[0];
55
- if (key) {
56
- const example = mediaType.examples[key];
57
- if (example) {
58
- return example.value;
59
- }
41
+ const { examples } = mediaType;
42
+ const keys = Object.keys(examples);
43
+ if (keys.length > 0) {
44
+ return keys.reduce<OpenAPIV3.ExampleObject[]>((result, key) => {
45
+ const example = examples[key];
46
+ if (!example || checkIsReference(example)) {
47
+ return result;
48
+ }
49
+ result.push({
50
+ summary: example.summary || key,
51
+ value: example.value,
52
+ description: example.description,
53
+ externalValue: example.externalValue,
54
+ });
55
+ return result;
56
+ }, []);
60
57
  }
61
58
  }
62
59
 
63
60
  if (mediaType.schema) {
64
- return generateSchemaExample(mediaType.schema, options);
61
+ return [{ summary: 'default', value: generateSchemaExample(mediaType.schema, options) }];
62
+ }
63
+
64
+ return [];
65
+ }
66
+
67
+ /** Hard limit for rendering circular references */
68
+ const MAX_LEVELS_DEEP = 5;
69
+
70
+ const genericExampleValues: Record<string, string> = {
71
+ 'date-time': new Date().toISOString(),
72
+ date: new Date().toISOString().split('T')[0] ?? '1970-01-01',
73
+ email: 'name@gmail.com',
74
+ hostname: 'example.com',
75
+ ipv4: '0.0.0.0',
76
+ ipv6: '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
77
+ uri: 'https://example.com',
78
+ uuid: '123e4567-e89b-12d3-a456-426614174000',
79
+ binary: 'binary',
80
+ byte: 'Ynl0ZXM=',
81
+ password: 'password',
82
+ 'idn-email': 'jane.doe@example.com',
83
+ 'idn-hostname': 'example.com',
84
+ 'iri-reference': '/entitiy/1',
85
+ // https://tools.ietf.org/html/rfc3987
86
+ iri: 'https://example.com/entity/123',
87
+ 'json-pointer': '/nested/objects',
88
+ regex: '/[a-z]/',
89
+ // https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01
90
+ 'relative-json-pointer': '1/nested/objects',
91
+ // full-time in https://tools.ietf.org/html/rfc3339#section-5.6
92
+ time: new Date().toISOString().split('T')[1]?.split('.')[0] ?? '00:00:00Z',
93
+ // either a URI or relative-reference https://tools.ietf.org/html/rfc3986#section-4.1
94
+ 'uri-reference': '../folder',
95
+ 'uri-template': 'https://example.com/{id}',
96
+ 'object-id': '6592008029c8c3e4dc76256c',
97
+ };
98
+
99
+ /**
100
+ * We can use the `format` to generate some random values.
101
+ */
102
+ function guessFromFormat(schema: Record<string, any>, fallback = '') {
103
+ return genericExampleValues[schema.format] ?? fallback;
104
+ }
105
+
106
+ /** Map of all the results */
107
+ const resultCache = new WeakMap<Record<string, any>, any>();
108
+
109
+ /** Store result in the cache, and return the result */
110
+ function cache(schema: Record<string, any>, result: unknown) {
111
+ // Avoid unnecessary WeakMap operations for primitive values
112
+ if (typeof result !== 'object' || result === null) {
113
+ return result;
65
114
  }
66
115
 
67
- return undefined;
116
+ resultCache.set(schema, result);
117
+
118
+ return result;
68
119
  }
120
+
121
+ /**
122
+ * This function takes an OpenAPI schema and generates an example from it
123
+ * Forked from : https://github.com/scalar/scalar/blob/main/packages/oas-utils/src/spec-getters/getExampleFromSchema.ts
124
+ */
125
+ const getExampleFromSchema = (
126
+ schema: Record<string, any>,
127
+ options?: {
128
+ /**
129
+ * The fallback string for empty string values.
130
+ * @default ''
131
+ */
132
+ emptyString?: string;
133
+ /**
134
+ * Whether to use the XML tag names as keys
135
+ * @default false
136
+ */
137
+ xml?: boolean;
138
+ /**
139
+ * Whether to show read-only/write-only properties. Otherwise all properties are shown.
140
+ * @default undefined
141
+ */
142
+ mode?: 'read' | 'write';
143
+ /**
144
+ * Dynamic values to add to the example.
145
+ */
146
+ variables?: Record<string, any>;
147
+ /**
148
+ * Whether to omit empty and optional properties.
149
+ * @default false
150
+ */
151
+ omitEmptyAndOptionalProperties?: boolean;
152
+ },
153
+ level = 0,
154
+ parentSchema?: Record<string, any>,
155
+ name?: string
156
+ ): any => {
157
+ // Check if the result is already cached
158
+ if (resultCache.has(schema)) {
159
+ return resultCache.get(schema);
160
+ }
161
+
162
+ // Check whether it’s a circular reference
163
+ if (level === MAX_LEVELS_DEEP + 1) {
164
+ try {
165
+ // Fails if it contains a circular reference
166
+ JSON.stringify(schema);
167
+ } catch {
168
+ return '[Circular Reference]';
169
+ }
170
+ }
171
+
172
+ // Sometimes, we just want the structure and no values.
173
+ // But if `emptyString` is set, we do want to see some values.
174
+ const makeUpRandomData = !!options?.emptyString;
175
+
176
+ // Check if the property is read-only/write-only
177
+ if (
178
+ (options?.mode === 'write' && schema.readOnly) ||
179
+ (options?.mode === 'read' && schema.writeOnly)
180
+ ) {
181
+ return undefined;
182
+ }
183
+
184
+ // Use given variables as values
185
+ if (schema['x-variable']) {
186
+ const value = options?.variables?.[schema['x-variable']];
187
+
188
+ // Return the value if it’s defined
189
+ if (value !== undefined) {
190
+ // Type-casting
191
+ if (schema.type === 'number' || schema.type === 'integer') {
192
+ return Number.parseInt(value, 10);
193
+ }
194
+
195
+ return cache(schema, value);
196
+ }
197
+ }
198
+
199
+ // Use the first example, if there’s an array
200
+ if (Array.isArray(schema.examples) && schema.examples.length > 0) {
201
+ return cache(schema, schema.examples[0]);
202
+ }
203
+
204
+ // Use an example, if there’s one
205
+ if (schema.example !== undefined) {
206
+ return cache(schema, schema.example);
207
+ }
208
+
209
+ // enum: [ 'available', 'pending', 'sold' ]
210
+ if (Array.isArray(schema.enum) && schema.enum.length > 0) {
211
+ return cache(schema, schema.enum[0]);
212
+ }
213
+
214
+ // Check if the property is required
215
+ const isObjectOrArray =
216
+ schema.type === 'object' ||
217
+ schema.type === 'array' ||
218
+ !!schema.allOf?.at?.(0) ||
219
+ !!schema.anyOf?.at?.(0) ||
220
+ !!schema.oneOf?.at?.(0);
221
+ if (!isObjectOrArray && options?.omitEmptyAndOptionalProperties === true) {
222
+ const isRequired =
223
+ schema.required === true ||
224
+ parentSchema?.required === true ||
225
+ parentSchema?.required?.includes(name ?? schema.name);
226
+
227
+ if (!isRequired) {
228
+ return undefined;
229
+ }
230
+ }
231
+
232
+ // Object
233
+ if (schema.type === 'object' || schema.properties !== undefined) {
234
+ const response: Record<string, any> = {};
235
+
236
+ // Regular properties
237
+ if (schema.properties !== undefined) {
238
+ for (const propertyName in schema.properties) {
239
+ if (Object.prototype.hasOwnProperty.call(schema.properties, propertyName)) {
240
+ const property = schema.properties[propertyName];
241
+ const propertyXmlTagName = options?.xml ? property.xml?.name : undefined;
242
+
243
+ response[propertyXmlTagName ?? propertyName] = getExampleFromSchema(
244
+ property,
245
+ options,
246
+ level + 1,
247
+ schema,
248
+ propertyName
249
+ );
250
+
251
+ if (typeof response[propertyXmlTagName ?? propertyName] === 'undefined') {
252
+ delete response[propertyXmlTagName ?? propertyName];
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ // Pattern properties (regex)
259
+ if (schema.patternProperties !== undefined) {
260
+ for (const pattern in schema.patternProperties) {
261
+ if (Object.prototype.hasOwnProperty.call(schema.patternProperties, pattern)) {
262
+ const property = schema.patternProperties[pattern];
263
+
264
+ // Use the regex pattern as an example key
265
+ const exampleKey = pattern;
266
+
267
+ response[exampleKey] = getExampleFromSchema(
268
+ property,
269
+ options,
270
+ level + 1,
271
+ schema,
272
+ exampleKey
273
+ );
274
+ }
275
+ }
276
+ }
277
+
278
+ // Additional properties
279
+ if (schema.additionalProperties !== undefined) {
280
+ const anyTypeIsValid =
281
+ // true
282
+ schema.additionalProperties === true ||
283
+ // or an empty object {}
284
+ (typeof schema.additionalProperties === 'object' &&
285
+ !Object.keys(schema.additionalProperties).length);
286
+
287
+ if (anyTypeIsValid) {
288
+ response.ANY_ADDITIONAL_PROPERTY = 'anything';
289
+ } else if (schema.additionalProperties !== false) {
290
+ response.ANY_ADDITIONAL_PROPERTY = getExampleFromSchema(
291
+ schema.additionalProperties,
292
+ options,
293
+ level + 1
294
+ );
295
+ }
296
+ }
297
+
298
+ if (schema.anyOf !== undefined) {
299
+ Object.assign(response, getExampleFromSchema(schema.anyOf[0], options, level + 1));
300
+ } else if (schema.oneOf !== undefined) {
301
+ Object.assign(response, getExampleFromSchema(schema.oneOf[0], options, level + 1));
302
+ } else if (schema.allOf !== undefined) {
303
+ Object.assign(
304
+ response,
305
+ ...schema.allOf
306
+ .map((item: Record<string, any>) =>
307
+ getExampleFromSchema(item, options, level + 1, schema)
308
+ )
309
+ .filter((item: any) => item !== undefined)
310
+ );
311
+ }
312
+
313
+ return cache(schema, response);
314
+ }
315
+
316
+ // Array
317
+ if (schema.type === 'array' || schema.items !== undefined) {
318
+ const itemsXmlTagName = schema?.items?.xml?.name;
319
+ const wrapItems = !!(options?.xml && schema.xml?.wrapped && itemsXmlTagName);
320
+
321
+ if (schema.example !== undefined) {
322
+ return cache(
323
+ schema,
324
+ wrapItems ? { [itemsXmlTagName]: schema.example } : schema.example
325
+ );
326
+ }
327
+
328
+ // Check whether the array has a anyOf, oneOf, or allOf rule
329
+ if (schema.items) {
330
+ // First handle allOf separately since it needs special handling
331
+ if (schema.items.allOf) {
332
+ // If the first item is an object type, merge all schemas
333
+ if (schema.items.allOf[0].type === 'object') {
334
+ const mergedExample = getExampleFromSchema(
335
+ { type: 'object', allOf: schema.items.allOf },
336
+ options,
337
+ level + 1,
338
+ schema
339
+ );
340
+
341
+ return cache(
342
+ schema,
343
+ wrapItems ? [{ [itemsXmlTagName]: mergedExample }] : [mergedExample]
344
+ );
345
+ }
346
+ // For non-objects (like strings), collect all examples
347
+ const examples = schema.items.allOf
348
+ .map((item: Record<string, any>) =>
349
+ getExampleFromSchema(item, options, level + 1, schema)
350
+ )
351
+ .filter((item: any) => item !== undefined);
352
+
353
+ return cache(
354
+ schema,
355
+ wrapItems
356
+ ? examples.map((example: any) => ({ [itemsXmlTagName]: example }))
357
+ : examples
358
+ );
359
+ }
360
+
361
+ // Handle other rules (anyOf, oneOf)
362
+ const rules = ['anyOf', 'oneOf'];
363
+ for (const rule of rules) {
364
+ if (!schema.items[rule]) {
365
+ continue;
366
+ }
367
+
368
+ const schemas = schema.items[rule].slice(0, 1);
369
+ const exampleFromRule = schemas
370
+ .map((item: Record<string, any>) =>
371
+ getExampleFromSchema(item, options, level + 1, schema)
372
+ )
373
+ .filter((item: any) => item !== undefined);
374
+
375
+ return cache(
376
+ schema,
377
+ wrapItems ? [{ [itemsXmlTagName]: exampleFromRule }] : exampleFromRule
378
+ );
379
+ }
380
+ }
381
+
382
+ if (schema.items?.type) {
383
+ const exampleFromSchema = getExampleFromSchema(schema.items, options, level + 1);
384
+
385
+ return wrapItems ? [{ [itemsXmlTagName]: exampleFromSchema }] : [exampleFromSchema];
386
+ }
387
+
388
+ return [];
389
+ }
390
+
391
+ const exampleValues: Record<any, any> = {
392
+ string: makeUpRandomData ? guessFromFormat(schema, options?.emptyString) : '',
393
+ boolean: true,
394
+ integer: schema.min ?? 1,
395
+ number: schema.min ?? 1,
396
+ array: [],
397
+ };
398
+
399
+ if (schema.type !== undefined && exampleValues[schema.type] !== undefined) {
400
+ return cache(schema, exampleValues[schema.type]);
401
+ }
402
+
403
+ const discriminateSchema = schema.oneOf || schema.anyOf;
404
+ // Check if property has the `oneOf` | `anyOf` key
405
+ if (Array.isArray(discriminateSchema) && discriminateSchema.length > 0) {
406
+ // Get the first item from the `oneOf` | `anyOf` array
407
+ const firstOneOfItem = discriminateSchema[0];
408
+
409
+ // Return an example for the first item
410
+ return getExampleFromSchema(firstOneOfItem, options, level + 1);
411
+ }
412
+
413
+ // Check if schema has the `allOf` key
414
+ if (Array.isArray(schema.allOf)) {
415
+ let example: any = null;
416
+
417
+ // Loop through all `allOf` schemas
418
+ schema.allOf.forEach((allOfItem: Record<string, any>) => {
419
+ // Return an example from the schema
420
+ const newExample = getExampleFromSchema(allOfItem, options, level + 1);
421
+
422
+ // Merge or overwrite the example
423
+ example =
424
+ typeof newExample === 'object' && typeof example === 'object'
425
+ ? {
426
+ ...(example ?? {}),
427
+ ...newExample,
428
+ }
429
+ : Array.isArray(newExample) && Array.isArray(example)
430
+ ? [...(example ?? {}), ...newExample]
431
+ : newExample;
432
+ });
433
+
434
+ return cache(schema, example);
435
+ }
436
+
437
+ // Check if schema is a union type
438
+ if (Array.isArray(schema.type)) {
439
+ // Return null if the type is nullable
440
+ if (schema.type.includes('null')) {
441
+ return null;
442
+ }
443
+ // Return an example for the first type in the union
444
+ const exampleValue = exampleValues[schema.type[0]];
445
+ if (exampleValue !== undefined) {
446
+ return cache(schema, exampleValue);
447
+ }
448
+ }
449
+
450
+ // Warn if the type is unknown …
451
+ // console.warn(`[getExampleFromSchema] Unknown property type "${schema.type}".`)
452
+
453
+ // … and just return null for now.
454
+ return null;
455
+ };
@@ -31,8 +31,8 @@ describe('#resolveOpenAPIOperation', () => {
31
31
  ],
32
32
  operation: {
33
33
  tags: ['pet'],
34
- summary: 'Update an existing pet',
35
- description: 'Update an existing pet by Id',
34
+ summary: 'Update an existing pet.',
35
+ description: 'Update an existing pet by Id.',
36
36
  requestBody: {
37
37
  content: {
38
38
  'application/json': {
@@ -61,8 +61,8 @@ describe('#resolveOpenAPIOperation', () => {
61
61
  ],
62
62
  operation: {
63
63
  tags: ['pet'],
64
- summary: 'Update an existing pet',
65
- description: 'Update an existing pet by Id',
64
+ summary: 'Update an existing pet.',
65
+ description: 'Update an existing pet by Id.',
66
66
  requestBody: {
67
67
  content: {
68
68
  'application/json': {
@@ -159,8 +159,8 @@ describe('#resolveOpenAPIOperation', () => {
159
159
  ],
160
160
  operation: {
161
161
  tags: ['pet'],
162
- summary: 'Update an existing pet',
163
- description: 'Update an existing pet by Id',
162
+ summary: 'Update an existing pet.',
163
+ description: 'Update an existing pet by Id.',
164
164
  requestBody: {
165
165
  content: {
166
166
  'application/json': {
package/src/utils.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  import type { AnyObject, OpenAPIV3, OpenAPIV3_1 } from '@gitbook/openapi-parser';
2
2
  import { stringifyOpenAPI } from './stringifyOpenAPI';
3
3
 
4
- export function checkIsReference(
5
- input: unknown
6
- ): input is OpenAPIV3.ReferenceObject | OpenAPIV3_1.ReferenceObject {
4
+ export function checkIsReference(input: unknown): input is OpenAPIV3.ReferenceObject {
7
5
  return typeof input === 'object' && !!input && '$ref' in input;
8
6
  }
9
7
 
@@ -22,17 +20,22 @@ function hasDescription(object: AnyObject) {
22
20
  * Resolve the description of an object.
23
21
  */
24
22
  export function resolveDescription(object: OpenAPIV3.SchemaObject | AnyObject) {
25
- // If the object has items and has a description, we resolve the description from items
23
+ // Resolve description from the object first
24
+ if (hasDescription(object)) {
25
+ return 'x-gitbook-description-html' in object &&
26
+ typeof object['x-gitbook-description-html'] === 'string'
27
+ ? object['x-gitbook-description-html'].trim()
28
+ : typeof object.description === 'string'
29
+ ? object.description.trim()
30
+ : undefined;
31
+ }
32
+
33
+ // If the object has no description, try to resolve it from the items
26
34
  if ('items' in object && typeof object.items === 'object' && hasDescription(object.items)) {
27
35
  return resolveDescription(object.items);
28
36
  }
29
37
 
30
- return 'x-gitbook-description-html' in object &&
31
- typeof object['x-gitbook-description-html'] === 'string'
32
- ? object['x-gitbook-description-html'].trim()
33
- : typeof object.description === 'string'
34
- ? object.description.trim()
35
- : undefined;
38
+ return undefined;
36
39
  }
37
40
 
38
41
  /**