Skip to content

Commit 8444461

Browse files
authored
feat: add support openapi 3.1 (#122)
1 parent d60cadd commit 8444461

15 files changed

+380
-110
lines changed

README.md

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,33 @@
55
Tool for generation samples based on OpenAPI payload/response schema
66

77
## Features
8-
- deterministic (given a particular input, will always produce the same output)
9-
- Supports `allOf`
8+
9+
- Deterministic (given a particular input, will always produce the same output)
10+
- Supports compound keywords: `allOf`, `oneOf`, `anyOf`, `if/then/else`
1011
- Supports `additionalProperties`
1112
- Uses `default`, `const`, `enum` and `examples` where possible
12-
- Full array support: supports `minItems`, and tuples (`items` as an array)
13+
- Good array support: supports `contains`, `minItems`, `maxItems`, and tuples (`items` as an array)
1314
- Supports `minLength`, `maxLength`, `min`, `max`, `exclusiveMinimum`, `exclusiveMaximum`
14-
- Supports the next `string` formats:
15+
- Supports the following `string` formats:
1516
- email
17+
- idn-email
1618
- password
1719
- date-time
1820
- date
21+
- time
1922
- ipv4
2023
- ipv6
2124
- hostname
25+
- idn-hostname
2226
- uri
27+
- uri-reference
28+
- uri-template
29+
- iri
30+
- iri-reference
2331
- uuid
32+
- json-pointer
33+
- relative-json-pointer
34+
- regex
2435
- Infers schema type automatically following same rules as [json-schema-faker](https://www.npmjs.com/package/json-schema-faker#inferred-types)
2536
- Support for `$ref` resolving
2637

package.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "openapi-sampler",
3-
"version": "1.0.0-beta.18",
3+
"version": "1.0.0",
44
"description": "Tool for generation samples based on OpenAPI payload/response schema",
55
"main": "dist/openapi-sampler.js",
66
"module": "src/openapi-sampler.js",
@@ -36,6 +36,8 @@
3636
"@babel/core": "^7.7.2",
3737
"@babel/preset-env": "^7.7.1",
3838
"@babel/register": "^7.7.0",
39+
"ajv": "^8.1.0",
40+
"ajv-formats": "^2.0.2",
3941
"babel-eslint": "^10.0.3",
4042
"babel-loader": "^8.0.6",
4143
"babel-plugin-istanbul": "^5.2.0",
@@ -58,6 +60,7 @@
5860
"gulp-rename": "^1.4.0",
5961
"gulp-sourcemaps": "^2.6.5",
6062
"gulp-uglify": "^3.0.2",
63+
"it-each": "^0.4.0",
6164
"json-loader": "^0.5.7",
6265
"karma": "^4.4.1",
6366
"karma-babel-preprocessor": "^8.0.1",
@@ -78,6 +81,7 @@
7881
"vinyl-source-stream": "^2.0.0"
7982
},
8083
"dependencies": {
81-
"json-pointer": "^0.6.0"
84+
"@types/json-schema": "^7.0.7",
85+
"json-pointer": "^0.6.1"
8286
}
8387
}

src/infer.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const schemaKeywordTypes = {
2626

2727
export function inferType(schema) {
2828
if (schema.type !== undefined) {
29-
return schema.type;
29+
return Array.isArray(schema.type) ? schema.type.length === 0 ? null : schema.type[0] : schema.type;
3030
}
3131
const keywords = Object.keys(schemaKeywordTypes);
3232
for (var i = 0; i < keywords.length; i++) {

src/openapi-sampler.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const defaults = {
88
maxSampleDepth: 15,
99
};
1010

11-
export function sample(schema, options, spec) {
11+
export function sample(schema, options, spec = schema) {
1212
let opts = Object.assign({}, defaults, options);
1313
clearCache();
1414
return traverse(schema, opts, spec).value;

src/samplers/array.js

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ import { traverse } from '../traverse';
22
export function sampleArray(schema, options = {}, spec, context) {
33
const depth = (context && context.depth || 1);
44

5-
let arrayLength = schema.minItems || 1;
6-
if (Array.isArray(schema.items)) {
7-
arrayLength = Math.max(arrayLength, schema.items.length);
5+
let arrayLength = Math.min('maxItems' in schema ? schema.maxItems : Infinity, schema.minItems || 1);
6+
// for the sake of simplicity, we're treating `contains` in a similar way to `items`
7+
const items = schema.items || schema.contains;
8+
if (Array.isArray(items)) {
9+
arrayLength = Math.max(arrayLength, items.length);
810
}
911

1012
let itemSchemaGetter = itemNumber => {
1113
if (Array.isArray(schema.items)) {
12-
return schema.items[itemNumber] || {};
14+
return items[itemNumber] || {};
1315
}
14-
return schema.items || {};
16+
return items || {};
1517
};
1618

1719
let res = [];
18-
if (!schema.items) return res;
20+
if (!items) return res;
1921

2022
for (let i = 0; i < arrayLength; i++) {
2123
let itemSchema = itemSchemaGetter(i);

src/samplers/number.js

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,44 @@
11
export function sampleNumber(schema) {
2-
let res;
3-
if (schema.maximum && schema.minimum) {
4-
res = schema.exclusiveMinimum ? Math.floor(schema.minimum) + 1 : schema.minimum;
5-
if ((schema.exclusiveMaximum && res >= schema.maximum) ||
6-
((!schema.exclusiveMaximum && res > schema.maximum))) {
7-
res = (schema.maximum + schema.minimum) / 2;
2+
let res = 0;
3+
if (typeof schema.exclusiveMinimum === 'boolean' || typeof schema.exclusiveMaximum === 'boolean') { //legacy support for jsonschema draft 4 of exclusiveMaximum/exclusiveMinimum as booleans
4+
if (schema.maximum && schema.minimum) {
5+
res = schema.exclusiveMinimum ? Math.floor(schema.minimum) + 1 : schema.minimum;
6+
if ((schema.exclusiveMaximum && res >= schema.maximum) ||
7+
((!schema.exclusiveMaximum && res > schema.maximum))) {
8+
res = (schema.maximum + schema.minimum) / 2;
9+
}
10+
return res;
811
}
9-
return res;
10-
}
11-
if (schema.minimum) {
12-
if (schema.exclusiveMinimum) {
13-
return Math.floor(schema.minimum) + 1;
14-
} else {
12+
if (schema.minimum) {
13+
if (schema.exclusiveMinimum) {
14+
return Math.floor(schema.minimum) + 1;
15+
} else {
16+
return schema.minimum;
17+
}
18+
}
19+
if (schema.maximum) {
20+
if (schema.exclusiveMaximum) {
21+
return (schema.maximum > 0) ? 0 : Math.floor(schema.maximum) - 1;
22+
} else {
23+
return (schema.maximum > 0) ? 0 : schema.maximum;
24+
}
25+
}
26+
} else {
27+
if (schema.minimum) {
1528
return schema.minimum;
1629
}
17-
}
18-
if (schema.maximum) {
19-
if (schema.exclusiveMaximum) {
20-
return (schema.maximum > 0) ? 0 : Math.floor(schema.maximum) - 1;
21-
} else {
22-
return (schema.maximum > 0) ? 0 : schema.maximum;
30+
if (schema.exclusiveMinimum) {
31+
res = Math.floor(schema.exclusiveMinimum) + 1;
32+
33+
if (res === schema.exclusiveMaximum) {
34+
res = (res + Math.floor(schema.exclusiveMaximum) - 1) / 2;
35+
}
36+
} else if (schema.exclusiveMaximum) {
37+
res = Math.floor(schema.exclusiveMaximum) - 1;
38+
} else if (schema.maximum) {
39+
res = schema.maximum;
2340
}
2441
}
2542

26-
return 0;
43+
return res;
2744
}

src/samplers/string.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ function passwordSample(min, max) {
1717
return res;
1818
}
1919

20-
function commonDateTimeSample(min, max, omitTime) {
21-
let res = toRFCDateTime(new Date('2019-08-24T14:15:22.123Z'), omitTime, false);
20+
function commonDateTimeSample({ min, max, omitTime, omitDate }) {
21+
let res = toRFCDateTime(new Date('2019-08-24T14:15:22.123Z'), omitTime, omitDate, false);
2222
if (res.length < min) {
2323
console.warn(`Using minLength = ${min} is incorrect with format "date-time"`);
2424
}
@@ -29,11 +29,15 @@ function commonDateTimeSample(min, max, omitTime) {
2929
}
3030

3131
function dateTimeSample(min, max) {
32-
return commonDateTimeSample(min, max);
32+
return commonDateTimeSample({ min, max, omitTime: false, omitDate: false });
3333
}
3434

3535
function dateSample(min, max) {
36-
return commonDateTimeSample(min, max, true);
36+
return commonDateTimeSample({ min, max, omitTime: true, omitDate: false });
37+
}
38+
39+
function timeSample(min, max) {
40+
return commonDateTimeSample({ min, max, omitTime: false, omitDate: true }).slice(1);
3741
}
3842

3943
function defaultSample(min, max) {
@@ -60,21 +64,59 @@ function uriSample() {
6064
return 'http://example.com';
6165
}
6266

67+
function uriReferenceSample() {
68+
return '../dictionary';
69+
}
70+
71+
function uriTemplateSample() {
72+
return 'http://example.com/{endpoint}';
73+
}
74+
75+
function iriSample() {
76+
return 'http://example.com';
77+
}
78+
79+
function iriReferenceSample() {
80+
return '../dictionary';
81+
}
82+
6383
function uuidSample(_min, _max, propertyName) {
6484
return uuid(propertyName || 'id');
6585
}
6686

87+
function jsonPointerSample() {
88+
return '/json/pointer';
89+
}
90+
91+
function relativeJsonPointerSample() {
92+
return '1/relative/json/pointer';
93+
}
94+
95+
function regexSample() {
96+
return '/regex/';
97+
}
98+
6799
const stringFormats = {
68100
'email': emailSample,
101+
'idn-email': emailSample, // https://tools.ietf.org/html/rfc6531#section-3.3
69102
'password': passwordSample,
70103
'date-time': dateTimeSample,
71104
'date': dateSample,
105+
'time': timeSample, // full-time in https://tools.ietf.org/html/rfc3339#section-5.6
72106
'ipv4': ipv4Sample,
73107
'ipv6': ipv6Sample,
74108
'hostname': hostnameSample,
109+
'idn-hostname': hostnameSample, // https://tools.ietf.org/html/rfc5890#section-2.3.2.3
110+
'iri': iriSample, // https://tools.ietf.org/html/rfc3987
111+
'iri-reference': iriReferenceSample,
75112
'uri': uriSample,
113+
'uri-reference': uriReferenceSample, // either a URI or relative-reference https://tools.ietf.org/html/rfc3986#section-4.1
114+
'uri-template': uriTemplateSample,
76115
'uuid': uuidSample,
77-
'default': defaultSample
116+
'default': defaultSample,
117+
'json-pointer': jsonPointerSample,
118+
'relative-json-pointer': relativeJsonPointerSample, // https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01
119+
'regex': regexSample,
78120
};
79121

80122
export function sampleString(schema, options, spec, context) {

src/traverse.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { _samplers } from './openapi-sampler';
22
import { allOfSample } from './allOf';
33
import { inferType } from './infer';
4-
import { getResultForCircular, popSchemaStack } from './utils';
4+
import { getResultForCircular, mergeDeep, popSchemaStack } from './utils';
55
import JsonPointer from 'json-pointer';
66

77
let $refCache = {};
@@ -28,9 +28,6 @@ export function traverse(schema, options, spec, context) {
2828
}
2929

3030
if (schema.$ref) {
31-
if (!spec) {
32-
throw new Error('Your schema contains $ref. You must provide full specification in the third parameter.');
33-
}
3431
let ref = decodeURIComponent(schema.$ref);
3532
if (ref.startsWith('#')) {
3633
ref = ref.substring(1);
@@ -86,6 +83,10 @@ export function traverse(schema, options, spec, context) {
8683
return traverse(schema.anyOf[0], options, spec, context);
8784
}
8885

86+
if (schema.if && schema.then) {
87+
return traverse(mergeDeep(schema.if, schema.then), options, spec, context);
88+
}
89+
8990
let example = null;
9091
let type = null;
9192
if (schema.default !== undefined) {
@@ -98,6 +99,9 @@ export function traverse(schema, options, spec, context) {
9899
example = schema.examples[0];
99100
} else {
100101
type = schema.type;
102+
if (Array.isArray(type) && schema.type.length > 0) {
103+
type = schema.type[0];
104+
}
101105
if (!type) {
102106
type = inferType(schema);
103107
}

src/types.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import type { JSONSchema7 } from 'json-schema';
2+
3+
export interface Options {
4+
readonly skipNonRequired?: boolean;
5+
readonly skipReadOnly?: boolean;
6+
readonly skipWriteOnly?: boolean;
7+
readonly quiet?: boolean;
8+
}
9+
10+
export function sample(schema: JSONSchema7, options?: Options, document?: object): unknown;

src/utils.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ function pad(number) {
77
return number;
88
}
99

10-
export function toRFCDateTime(date, omitTime, milliseconds) {
11-
var res = date.getUTCFullYear() +
10+
export function toRFCDateTime(date, omitTime, omitDate, milliseconds) {
11+
var res = omitDate ? '' : (date.getUTCFullYear() +
1212
'-' + pad(date.getUTCMonth() + 1) +
13-
'-' + pad(date.getUTCDate());
13+
'-' + pad(date.getUTCDate()));
1414
if (!omitTime) {
1515
res += 'T' + pad(date.getUTCHours()) +
1616
':' + pad(date.getUTCMinutes()) +
@@ -92,4 +92,4 @@ function jsf32(a, b, c, d) {
9292
d = a + t | 0;
9393
return (d >>> 0) / 4294967296;
9494
}
95-
}
95+
}

0 commit comments

Comments
 (0)