oas 20.8.0 → 20.8.2

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 CHANGED
@@ -1,3 +1,17 @@
1
+ ## <small>20.8.2 (2023-05-04)</small>
2
+
3
+ * feat: adding a broken skipped test, retaining soeme more jsonschema props ([94436b8](https://github.com/readmeio/oas/commit/94436b8))
4
+ * feat: additional tweaks to json schema generation (#762) ([2bae3fe](https://github.com/readmeio/oas/commit/2bae3fe)), closes [#762](https://github.com/readmeio/oas/issues/762)
5
+ * feat: additional tweaks to json schema mixed type handling (#761) ([57d4442](https://github.com/readmeio/oas/commit/57d4442)), closes [#761](https://github.com/readmeio/oas/issues/761)
6
+
7
+
8
+
9
+ ## <small>20.8.1 (2023-05-04)</small>
10
+
11
+ * feat: additional tweaks to json schema mixed type handling (#761) ([57d4442](https://github.com/readmeio/oas/commit/57d4442)), closes [#761](https://github.com/readmeio/oas/issues/761)
12
+
13
+
14
+
1
15
  ## 20.8.0 (2023-05-03)
2
16
 
3
17
  * feat: improved support for mixed types and `nullable` (#760) ([5eff5c4](https://github.com/readmeio/oas/commit/5eff5c4)), closes [#760](https://github.com/readmeio/oas/issues/760)
@@ -1 +1,3 @@
1
+ import type { SchemaObject } from 'rmoas.types';
2
+ export declare function hasSchemaType(schema: SchemaObject, discriminator: 'array' | 'object'): boolean;
1
3
  export declare function isPrimitive(val: unknown): boolean;
@@ -1,6 +1,13 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.isPrimitive = void 0;
3
+ exports.isPrimitive = exports.hasSchemaType = void 0;
4
+ function hasSchemaType(schema, discriminator) {
5
+ if (Array.isArray(schema.type)) {
6
+ return schema.type.includes(discriminator);
7
+ }
8
+ return schema.type === discriminator;
9
+ }
10
+ exports.hasSchemaType = hasSchemaType;
4
11
  function isPrimitive(val) {
5
12
  return typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean';
6
13
  }
@@ -60,6 +60,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
60
60
  exports.getSchemaVersionString = void 0;
61
61
  var json_schema_merge_allof_1 = __importDefault(require("json-schema-merge-allof"));
62
62
  var jsonpointer_1 = __importDefault(require("jsonpointer"));
63
+ var remove_undefined_objects_1 = __importDefault(require("remove-undefined-objects"));
63
64
  var RMOAS = __importStar(require("../rmoas.types"));
64
65
  var helpers_1 = require("./helpers");
65
66
  /**
@@ -203,8 +204,8 @@ function searchForExampleByPointer(pointer, examples) {
203
204
  example = jsonpointer_1.default.get(schema, pointers[i]);
204
205
  }
205
206
  catch (err) {
206
- // If the schema we're looking at is `{obj: null}` and our pointer if `/obj/propertyName`
207
- // jsonpointer will throw an error. If that happens, we should silently catch and toss it
207
+ // If the schema we're looking at is `{obj: null}` and our pointer is `/obj/propertyName`
208
+ // `jsonpointer` will throw an error. If that happens, we should silently catch and toss it
208
209
  // and return no example.
209
210
  }
210
211
  if (example !== undefined) {
@@ -326,29 +327,6 @@ function toJSONSchema(data, opts) {
326
327
  });
327
328
  }
328
329
  }
329
- // To ease some of the burden on our frontend having to juggle mixed types we're opting to
330
- // transform them into a `oneOf`.
331
- if ('type' in schema && Array.isArray(schema.type)) {
332
- schema.type = Array.from(new Set(schema.type));
333
- // If we have a `null` type but there's only two types present then we can remove `null` as
334
- // an option and flag the whole schema as `nullable`.
335
- if (schema.type.includes('null')) {
336
- schema.type = schema.type.filter(function (t) { return t !== 'null'; });
337
- schema.nullable = true;
338
- }
339
- if (schema.type.length === 1) {
340
- schema.type = schema.type.shift();
341
- }
342
- else {
343
- var mixedOneOf_1 = [];
344
- schema.type.forEach(function (schemaType) {
345
- mixedOneOf_1.push(__assign(__assign({}, schema), { type: schemaType }));
346
- });
347
- schema = {
348
- oneOf: mixedOneOf_1,
349
- };
350
- }
351
- }
352
330
  ['anyOf', 'oneOf'].forEach(function (polyType) {
353
331
  if (polyType in schema && Array.isArray(schema[polyType])) {
354
332
  schema[polyType].forEach(function (item, idx) {
@@ -406,6 +384,110 @@ function toJSONSchema(data, opts) {
406
384
  // Whatever tooling that ingests the generated schema should handle it however it needs to.
407
385
  }
408
386
  }
387
+ if ('type' in schema) {
388
+ // `nullable` isn't a thing in JSON Schema but it was in OpenAPI 3.0 so we should retain and
389
+ // translate it into something that's compatible with JSON Schema.
390
+ if ('nullable' in schema) {
391
+ if (Array.isArray(schema.type)) {
392
+ schema.type.push('null');
393
+ }
394
+ else if (schema.type !== null && schema.type !== 'null') {
395
+ schema.type = [schema.type, 'null'];
396
+ }
397
+ delete schema.nullable;
398
+ }
399
+ if (schema.type === null) {
400
+ // `type: null` is possible in JSON Schema but we're translating it to a string version
401
+ // so we don't need to worry about asserting nullish types in our implementations of this
402
+ // generated schema.
403
+ schema.type = 'null';
404
+ }
405
+ else if (Array.isArray(schema.type)) {
406
+ if (schema.type.includes(null)) {
407
+ schema.type[schema.type.indexOf(null)] = 'null';
408
+ }
409
+ schema.type = Array.from(new Set(schema.type));
410
+ // We don't need `type: [<type>]` when we can just as easily make it `type: <type>`.
411
+ if (schema.type.length === 1) {
412
+ schema.type = schema.type.shift();
413
+ }
414
+ else if (schema.type.includes('array') || schema.type.includes('object')) {
415
+ // If we have a `null` type but there's only two types present then we can remove `null`
416
+ // as an option and flag the whole schema as `nullable`.
417
+ var isNullable_1 = schema.type.includes('null');
418
+ if (schema.type.length === 2 && isNullable_1) {
419
+ // If this is `array | null` or `object | null` then we don't need to do anything.
420
+ }
421
+ else {
422
+ // If this mixed type has non-primitives then we for convenience of our implementation
423
+ // we're moving them into a `oneOf`.
424
+ var nonPrimitives_1 = [];
425
+ // Because we're moving an `array` and/or an `object` into a `oneOf` we also want to take
426
+ // with it its specific properties that maybe present on our current schema.
427
+ Object.entries({
428
+ // json-schema.org/understanding-json-schema/reference/array.html
429
+ array: [
430
+ 'additionalItems',
431
+ 'contains',
432
+ 'items',
433
+ 'maxContains',
434
+ 'maxItems',
435
+ 'minContains',
436
+ 'minItems',
437
+ 'prefixItems',
438
+ 'uniqueItems',
439
+ ],
440
+ // https://json-schema.org/understanding-json-schema/reference/object.html
441
+ object: [
442
+ 'additionalProperties',
443
+ 'maxProperties',
444
+ 'minProperties',
445
+ 'nullable',
446
+ 'patternProperties',
447
+ 'properties',
448
+ 'propertyNames',
449
+ 'required',
450
+ ],
451
+ }).forEach(function (_a) {
452
+ var _b, _c, _d, _e, _f, _g;
453
+ var typeKey = _a[0], keywords = _a[1];
454
+ if (!schema.type.includes(typeKey)) {
455
+ return;
456
+ }
457
+ var reducedSchema = (0, remove_undefined_objects_1.default)({
458
+ type: isNullable_1 ? [typeKey, 'null'] : typeKey,
459
+ allowEmptyValue: (_b = schema.allowEmptyValue) !== null && _b !== void 0 ? _b : undefined,
460
+ deprecated: (_c = schema.deprecated) !== null && _c !== void 0 ? _c : undefined,
461
+ description: (_d = schema.description) !== null && _d !== void 0 ? _d : undefined,
462
+ readOnly: (_e = schema.readOnly) !== null && _e !== void 0 ? _e : undefined,
463
+ title: (_f = schema.title) !== null && _f !== void 0 ? _f : undefined,
464
+ writeOnly: (_g = schema.writeOnly) !== null && _g !== void 0 ? _g : undefined,
465
+ });
466
+ keywords.forEach(function (t) {
467
+ if (t in schema) {
468
+ reducedSchema[t] = schema[t];
469
+ delete schema[t];
470
+ }
471
+ });
472
+ nonPrimitives_1.push(reducedSchema);
473
+ });
474
+ schema.type = schema.type.filter(function (t) { return t !== 'array' && t !== 'object'; });
475
+ if (schema.type.length === 1) {
476
+ schema.type = schema.type.shift();
477
+ }
478
+ // Because we may have encountered a fully mixed non-primitive type like `array | object`
479
+ // we only want to retain the existing schema object if we still have types remaining
480
+ // in it.
481
+ if (schema.type.length > 1) {
482
+ schema = { oneOf: __spreadArray([schema], nonPrimitives_1, true) };
483
+ }
484
+ else {
485
+ schema = { oneOf: nonPrimitives_1 };
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
409
491
  if (RMOAS.isSchema(schema, isPolymorphicAllOfChild)) {
410
492
  // JSON Schema doesn't support OpenAPI-style examples so we need to reshape them a bit.
411
493
  if ('example' in schema) {
@@ -471,7 +553,7 @@ function toJSONSchema(data, opts) {
471
553
  // If we didn't have any immediately defined examples, let's search backwards and see if we can
472
554
  // find one. But as we're only looking for primitive example, only try to search for one if
473
555
  // we're dealing with a primitive schema.
474
- if (schema.type !== 'array' && schema.type !== 'object' && !schema.examples) {
556
+ if (!(0, helpers_1.hasSchemaType)(schema, 'array') && !(0, helpers_1.hasSchemaType)(schema, 'object') && !schema.examples) {
475
557
  var foundExample = searchForExampleByPointer(currentLocation, prevSchemas);
476
558
  if (foundExample) {
477
559
  // We can only really deal with primitives, so only promote those as the found example if
@@ -481,7 +563,7 @@ function toJSONSchema(data, opts) {
481
563
  }
482
564
  }
483
565
  }
484
- if (schema.type === 'array') {
566
+ if ((0, helpers_1.hasSchemaType)(schema, 'array')) {
485
567
  if ('items' in schema) {
486
568
  if (!Array.isArray(schema.items) && Object.keys(schema.items).length === 1 && RMOAS.isRef(schema.items)) {
487
569
  // `items` contains a `$ref`, so since it's circular we should do a no-op here and log
@@ -505,17 +587,15 @@ function toJSONSchema(data, opts) {
505
587
  // array. Since throwing a complete failure isn't ideal, we can see that they meant for the
506
588
  // type to be `object`, so we can do our best to shape the data into what they were
507
589
  // intending it to be.
508
- // README-6R
509
590
  schema.type = 'object';
510
591
  }
511
592
  else {
512
593
  // This is a fix to handle cases where we have a malformed array with no `items` property
513
594
  // present.
514
- // README-8E
515
595
  schema.items = {};
516
596
  }
517
597
  }
518
- else if (schema.type === 'object') {
598
+ else if ((0, helpers_1.hasSchemaType)(schema, 'object')) {
519
599
  if ('properties' in schema) {
520
600
  Object.keys(schema.properties).forEach(function (prop) {
521
601
  if (Array.isArray(schema.properties[prop]) ||
@@ -85,7 +85,7 @@ function sampleFromSchema(schema, opts) {
85
85
  return undefined;
86
86
  }
87
87
  }
88
- if (type === 'object') {
88
+ if (type === 'object' || (Array.isArray(type) && type.includes('object'))) {
89
89
  var props = (0, utils_1.objectify)(properties);
90
90
  var obj = {};
91
91
  // eslint-disable-next-line no-restricted-syntax
@@ -114,7 +114,7 @@ function sampleFromSchema(schema, opts) {
114
114
  }
115
115
  return obj;
116
116
  }
117
- if (type === 'array') {
117
+ if (type === 'array' || (Array.isArray(type) && type.includes('array'))) {
118
118
  // `items` should always be present on arrays, but if it isn't we should at least do our best
119
119
  // to support its absence.
120
120
  if (typeof items === 'undefined') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oas",
3
- "version": "20.8.0",
3
+ "version": "20.8.2",
4
4
  "description": "Comprehensive tooling for working with OpenAPI definitions",
5
5
  "license": "MIT",
6
6
  "author": "ReadMe <support@readme.io> (https://readme.com)",
@@ -54,7 +54,8 @@
54
54
  "memoizee": "^0.4.14",
55
55
  "oas-normalize": "^8.4.0",
56
56
  "openapi-types": "^12.1.0",
57
- "path-to-regexp": "^6.2.0"
57
+ "path-to-regexp": "^6.2.0",
58
+ "remove-undefined-objects": "^2.0.2"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@commitlint/cli": "^17.6.1",
@@ -1,3 +1,12 @@
1
+ import type { SchemaObject } from 'rmoas.types';
2
+
3
+ export function hasSchemaType(schema: SchemaObject, discriminator: 'array' | 'object') {
4
+ if (Array.isArray(schema.type)) {
5
+ return schema.type.includes(discriminator);
6
+ }
7
+
8
+ return schema.type === discriminator;
9
+ }
1
10
  export function isPrimitive(val: unknown): boolean {
2
11
  return typeof val === 'string' || typeof val === 'number' || typeof val === 'boolean';
3
12
  }
@@ -1,12 +1,15 @@
1
1
  /* eslint-disable no-continue */
2
+ import type { SchemaObject } from '../rmoas.types';
3
+ import type { JSONSchema7TypeName } from 'json-schema';
2
4
  import type { OpenAPIV3_1 } from 'openapi-types';
3
5
 
4
6
  import mergeJSONSchemaAllOf from 'json-schema-merge-allof';
5
7
  import jsonpointer from 'jsonpointer';
8
+ import removeUndefinedObjects from 'remove-undefined-objects';
6
9
 
7
10
  import * as RMOAS from '../rmoas.types';
8
11
 
9
- import { isPrimitive } from './helpers';
12
+ import { hasSchemaType, isPrimitive } from './helpers';
10
13
 
11
14
  /**
12
15
  * This list has been pulled from `openapi-schema-to-json-schema` but been slightly modified to fit
@@ -205,8 +208,8 @@ function searchForExampleByPointer(pointer: string, examples: PrevSchemasType =
205
208
  try {
206
209
  example = jsonpointer.get(schema, pointers[i]);
207
210
  } catch (err) {
208
- // If the schema we're looking at is `{obj: null}` and our pointer if `/obj/propertyName`
209
- // jsonpointer will throw an error. If that happens, we should silently catch and toss it
211
+ // If the schema we're looking at is `{obj: null}` and our pointer is `/obj/propertyName`
212
+ // `jsonpointer` will throw an error. If that happens, we should silently catch and toss it
210
213
  // and return no example.
211
214
  }
212
215
 
@@ -362,35 +365,6 @@ export default function toJSONSchema(
362
365
  }
363
366
  }
364
367
 
365
- // To ease some of the burden on our frontend having to juggle mixed types we're opting to
366
- // transform them into a `oneOf`.
367
- if ('type' in schema && Array.isArray(schema.type)) {
368
- schema.type = Array.from(new Set(schema.type));
369
-
370
- // If we have a `null` type but there's only two types present then we can remove `null` as
371
- // an option and flag the whole schema as `nullable`.
372
- if (schema.type.includes('null')) {
373
- schema.type = schema.type.filter(t => t !== 'null');
374
- schema.nullable = true;
375
- }
376
-
377
- if (schema.type.length === 1) {
378
- schema.type = schema.type.shift();
379
- } else {
380
- const mixedOneOf: any[] = [];
381
- schema.type.forEach(schemaType => {
382
- mixedOneOf.push({
383
- ...schema,
384
- type: schemaType,
385
- });
386
- });
387
-
388
- schema = {
389
- oneOf: mixedOneOf,
390
- };
391
- }
392
- }
393
-
394
368
  ['anyOf', 'oneOf'].forEach((polyType: 'anyOf' | 'oneOf') => {
395
369
  if (polyType in schema && Array.isArray(schema[polyType])) {
396
370
  schema[polyType].forEach((item, idx) => {
@@ -454,6 +428,117 @@ export default function toJSONSchema(
454
428
  }
455
429
  }
456
430
 
431
+ if ('type' in schema) {
432
+ // `nullable` isn't a thing in JSON Schema but it was in OpenAPI 3.0 so we should retain and
433
+ // translate it into something that's compatible with JSON Schema.
434
+ if ('nullable' in schema) {
435
+ if (Array.isArray(schema.type)) {
436
+ schema.type.push('null');
437
+ } else if (schema.type !== null && schema.type !== 'null') {
438
+ schema.type = [schema.type, 'null'];
439
+ }
440
+
441
+ delete schema.nullable;
442
+ }
443
+
444
+ if (schema.type === null) {
445
+ // `type: null` is possible in JSON Schema but we're translating it to a string version
446
+ // so we don't need to worry about asserting nullish types in our implementations of this
447
+ // generated schema.
448
+ schema.type = 'null';
449
+ } else if (Array.isArray(schema.type)) {
450
+ if (schema.type.includes(null)) {
451
+ schema.type[schema.type.indexOf(null)] = 'null';
452
+ }
453
+
454
+ schema.type = Array.from(new Set(schema.type));
455
+
456
+ // We don't need `type: [<type>]` when we can just as easily make it `type: <type>`.
457
+ if (schema.type.length === 1) {
458
+ schema.type = schema.type.shift();
459
+ } else if (schema.type.includes('array') || schema.type.includes('object')) {
460
+ // If we have a `null` type but there's only two types present then we can remove `null`
461
+ // as an option and flag the whole schema as `nullable`.
462
+ const isNullable = schema.type.includes('null');
463
+
464
+ if (schema.type.length === 2 && isNullable) {
465
+ // If this is `array | null` or `object | null` then we don't need to do anything.
466
+ } else {
467
+ // If this mixed type has non-primitives then we for convenience of our implementation
468
+ // we're moving them into a `oneOf`.
469
+ const nonPrimitives: any[] = [];
470
+
471
+ // Because we're moving an `array` and/or an `object` into a `oneOf` we also want to take
472
+ // with it its specific properties that maybe present on our current schema.
473
+ Object.entries({
474
+ // json-schema.org/understanding-json-schema/reference/array.html
475
+ array: [
476
+ 'additionalItems',
477
+ 'contains',
478
+ 'items',
479
+ 'maxContains',
480
+ 'maxItems',
481
+ 'minContains',
482
+ 'minItems',
483
+ 'prefixItems',
484
+ 'uniqueItems',
485
+ ],
486
+
487
+ // https://json-schema.org/understanding-json-schema/reference/object.html
488
+ object: [
489
+ 'additionalProperties',
490
+ 'maxProperties',
491
+ 'minProperties',
492
+ 'nullable',
493
+ 'patternProperties',
494
+ 'properties',
495
+ 'propertyNames',
496
+ 'required',
497
+ ],
498
+ }).forEach(([typeKey, keywords]) => {
499
+ if (!schema.type.includes(typeKey as JSONSchema7TypeName)) {
500
+ return;
501
+ }
502
+
503
+ const reducedSchema: any = removeUndefinedObjects({
504
+ type: isNullable ? [typeKey, 'null'] : typeKey,
505
+
506
+ allowEmptyValue: (schema as any).allowEmptyValue ?? undefined,
507
+ deprecated: schema.deprecated ?? undefined,
508
+ description: schema.description ?? undefined,
509
+ readOnly: schema.readOnly ?? undefined,
510
+ title: schema.title ?? undefined,
511
+ writeOnly: schema.writeOnly ?? undefined,
512
+ });
513
+
514
+ keywords.forEach((t: keyof SchemaObject) => {
515
+ if (t in schema) {
516
+ reducedSchema[t] = schema[t];
517
+ delete schema[t];
518
+ }
519
+ });
520
+
521
+ nonPrimitives.push(reducedSchema);
522
+ });
523
+
524
+ schema.type = schema.type.filter(t => t !== 'array' && t !== 'object');
525
+ if (schema.type.length === 1) {
526
+ schema.type = schema.type.shift();
527
+ }
528
+
529
+ // Because we may have encountered a fully mixed non-primitive type like `array | object`
530
+ // we only want to retain the existing schema object if we still have types remaining
531
+ // in it.
532
+ if (schema.type.length > 1) {
533
+ schema = { oneOf: [schema, ...nonPrimitives] };
534
+ } else {
535
+ schema = { oneOf: nonPrimitives };
536
+ }
537
+ }
538
+ }
539
+ }
540
+ }
541
+
457
542
  if (RMOAS.isSchema(schema, isPolymorphicAllOfChild)) {
458
543
  // JSON Schema doesn't support OpenAPI-style examples so we need to reshape them a bit.
459
544
  if ('example' in schema) {
@@ -516,7 +601,7 @@ export default function toJSONSchema(
516
601
  // If we didn't have any immediately defined examples, let's search backwards and see if we can
517
602
  // find one. But as we're only looking for primitive example, only try to search for one if
518
603
  // we're dealing with a primitive schema.
519
- if (schema.type !== 'array' && schema.type !== 'object' && !schema.examples) {
604
+ if (!hasSchemaType(schema, 'array') && !hasSchemaType(schema, 'object') && !schema.examples) {
520
605
  const foundExample = searchForExampleByPointer(currentLocation, prevSchemas);
521
606
  if (foundExample) {
522
607
  // We can only really deal with primitives, so only promote those as the found example if
@@ -527,7 +612,7 @@ export default function toJSONSchema(
527
612
  }
528
613
  }
529
614
 
530
- if (schema.type === 'array') {
615
+ if (hasSchemaType(schema, 'array')) {
531
616
  if ('items' in schema) {
532
617
  if (!Array.isArray(schema.items) && Object.keys(schema.items).length === 1 && RMOAS.isRef(schema.items)) {
533
618
  // `items` contains a `$ref`, so since it's circular we should do a no-op here and log
@@ -549,15 +634,13 @@ export default function toJSONSchema(
549
634
  // array. Since throwing a complete failure isn't ideal, we can see that they meant for the
550
635
  // type to be `object`, so we can do our best to shape the data into what they were
551
636
  // intending it to be.
552
- // README-6R
553
637
  schema.type = 'object';
554
638
  } else {
555
639
  // This is a fix to handle cases where we have a malformed array with no `items` property
556
640
  // present.
557
- // README-8E
558
- schema.items = {};
641
+ (schema as any).items = {};
559
642
  }
560
- } else if (schema.type === 'object') {
643
+ } else if (hasSchemaType(schema, 'object')) {
561
644
  if ('properties' in schema) {
562
645
  Object.keys(schema.properties).forEach(prop => {
563
646
  if (
@@ -110,7 +110,7 @@ function sampleFromSchema(
110
110
  }
111
111
  }
112
112
 
113
- if (type === 'object') {
113
+ if (type === 'object' || (Array.isArray(type) && type.includes('object'))) {
114
114
  const props = objectify(properties);
115
115
  const obj: Record<string, any> = {};
116
116
  // eslint-disable-next-line no-restricted-syntax
@@ -145,7 +145,7 @@ function sampleFromSchema(
145
145
  return obj;
146
146
  }
147
147
 
148
- if (type === 'array') {
148
+ if (type === 'array' || (Array.isArray(type) && type.includes('array'))) {
149
149
  // `items` should always be present on arrays, but if it isn't we should at least do our best
150
150
  // to support its absence.
151
151
  if (typeof items === 'undefined') {