Skip to content

Commit

Permalink
feat: add support openapi 3.1 (#122)
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexVarchuk authored May 24, 2021
1 parent d60cadd commit 8444461
Show file tree
Hide file tree
Showing 15 changed files with 380 additions and 110 deletions.
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,33 @@
Tool for generation samples based on OpenAPI payload/response schema

## Features
- deterministic (given a particular input, will always produce the same output)
- Supports `allOf`

- Deterministic (given a particular input, will always produce the same output)
- Supports compound keywords: `allOf`, `oneOf`, `anyOf`, `if/then/else`
- Supports `additionalProperties`
- Uses `default`, `const`, `enum` and `examples` where possible
- Full array support: supports `minItems`, and tuples (`items` as an array)
- Good array support: supports `contains`, `minItems`, `maxItems`, and tuples (`items` as an array)
- Supports `minLength`, `maxLength`, `min`, `max`, `exclusiveMinimum`, `exclusiveMaximum`
- Supports the next `string` formats:
- Supports the following `string` formats:
- email
- idn-email
- password
- date-time
- date
- time
- ipv4
- ipv6
- hostname
- idn-hostname
- uri
- uri-reference
- uri-template
- iri
- iri-reference
- uuid
- json-pointer
- relative-json-pointer
- regex
- Infers schema type automatically following same rules as [json-schema-faker](https://www.npmjs.com/package/json-schema-faker#inferred-types)
- Support for `$ref` resolving

Expand Down
8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "openapi-sampler",
"version": "1.0.0-beta.18",
"version": "1.0.0",
"description": "Tool for generation samples based on OpenAPI payload/response schema",
"main": "dist/openapi-sampler.js",
"module": "src/openapi-sampler.js",
Expand Down Expand Up @@ -36,6 +36,8 @@
"@babel/core": "^7.7.2",
"@babel/preset-env": "^7.7.1",
"@babel/register": "^7.7.0",
"ajv": "^8.1.0",
"ajv-formats": "^2.0.2",
"babel-eslint": "^10.0.3",
"babel-loader": "^8.0.6",
"babel-plugin-istanbul": "^5.2.0",
Expand All @@ -58,6 +60,7 @@
"gulp-rename": "^1.4.0",
"gulp-sourcemaps": "^2.6.5",
"gulp-uglify": "^3.0.2",
"it-each": "^0.4.0",
"json-loader": "^0.5.7",
"karma": "^4.4.1",
"karma-babel-preprocessor": "^8.0.1",
Expand All @@ -78,6 +81,7 @@
"vinyl-source-stream": "^2.0.0"
},
"dependencies": {
"json-pointer": "^0.6.0"
"@types/json-schema": "^7.0.7",
"json-pointer": "^0.6.1"
}
}
2 changes: 1 addition & 1 deletion src/infer.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const schemaKeywordTypes = {

export function inferType(schema) {
if (schema.type !== undefined) {
return schema.type;
return Array.isArray(schema.type) ? schema.type.length === 0 ? null : schema.type[0] : schema.type;
}
const keywords = Object.keys(schemaKeywordTypes);
for (var i = 0; i < keywords.length; i++) {
Expand Down
2 changes: 1 addition & 1 deletion src/openapi-sampler.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ const defaults = {
maxSampleDepth: 15,
};

export function sample(schema, options, spec) {
export function sample(schema, options, spec = schema) {
let opts = Object.assign({}, defaults, options);
clearCache();
return traverse(schema, opts, spec).value;
Expand Down
14 changes: 8 additions & 6 deletions src/samplers/array.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,22 @@ import { traverse } from '../traverse';
export function sampleArray(schema, options = {}, spec, context) {
const depth = (context && context.depth || 1);

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

let itemSchemaGetter = itemNumber => {
if (Array.isArray(schema.items)) {
return schema.items[itemNumber] || {};
return items[itemNumber] || {};
}
return schema.items || {};
return items || {};
};

let res = [];
if (!schema.items) return res;
if (!items) return res;

for (let i = 0; i < arrayLength; i++) {
let itemSchema = itemSchemaGetter(i);
Expand Down
55 changes: 36 additions & 19 deletions src/samplers/number.js
Original file line number Diff line number Diff line change
@@ -1,27 +1,44 @@
export function sampleNumber(schema) {
let res;
if (schema.maximum && schema.minimum) {
res = schema.exclusiveMinimum ? Math.floor(schema.minimum) + 1 : schema.minimum;
if ((schema.exclusiveMaximum && res >= schema.maximum) ||
((!schema.exclusiveMaximum && res > schema.maximum))) {
res = (schema.maximum + schema.minimum) / 2;
let res = 0;
if (typeof schema.exclusiveMinimum === 'boolean' || typeof schema.exclusiveMaximum === 'boolean') { //legacy support for jsonschema draft 4 of exclusiveMaximum/exclusiveMinimum as booleans
if (schema.maximum && schema.minimum) {
res = schema.exclusiveMinimum ? Math.floor(schema.minimum) + 1 : schema.minimum;
if ((schema.exclusiveMaximum && res >= schema.maximum) ||
((!schema.exclusiveMaximum && res > schema.maximum))) {
res = (schema.maximum + schema.minimum) / 2;
}
return res;
}
return res;
}
if (schema.minimum) {
if (schema.exclusiveMinimum) {
return Math.floor(schema.minimum) + 1;
} else {
if (schema.minimum) {
if (schema.exclusiveMinimum) {
return Math.floor(schema.minimum) + 1;
} else {
return schema.minimum;
}
}
if (schema.maximum) {
if (schema.exclusiveMaximum) {
return (schema.maximum > 0) ? 0 : Math.floor(schema.maximum) - 1;
} else {
return (schema.maximum > 0) ? 0 : schema.maximum;
}
}
} else {
if (schema.minimum) {
return schema.minimum;
}
}
if (schema.maximum) {
if (schema.exclusiveMaximum) {
return (schema.maximum > 0) ? 0 : Math.floor(schema.maximum) - 1;
} else {
return (schema.maximum > 0) ? 0 : schema.maximum;
if (schema.exclusiveMinimum) {
res = Math.floor(schema.exclusiveMinimum) + 1;

if (res === schema.exclusiveMaximum) {
res = (res + Math.floor(schema.exclusiveMaximum) - 1) / 2;
}
} else if (schema.exclusiveMaximum) {
res = Math.floor(schema.exclusiveMaximum) - 1;
} else if (schema.maximum) {
res = schema.maximum;
}
}

return 0;
return res;
}
52 changes: 47 additions & 5 deletions src/samplers/string.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ function passwordSample(min, max) {
return res;
}

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

function dateTimeSample(min, max) {
return commonDateTimeSample(min, max);
return commonDateTimeSample({ min, max, omitTime: false, omitDate: false });
}

function dateSample(min, max) {
return commonDateTimeSample(min, max, true);
return commonDateTimeSample({ min, max, omitTime: true, omitDate: false });
}

function timeSample(min, max) {
return commonDateTimeSample({ min, max, omitTime: false, omitDate: true }).slice(1);
}

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

function uriReferenceSample() {
return '../dictionary';
}

function uriTemplateSample() {
return 'http://example.com/{endpoint}';
}

function iriSample() {
return 'http://example.com';
}

function iriReferenceSample() {
return '../dictionary';
}

function uuidSample(_min, _max, propertyName) {
return uuid(propertyName || 'id');
}

function jsonPointerSample() {
return '/json/pointer';
}

function relativeJsonPointerSample() {
return '1/relative/json/pointer';
}

function regexSample() {
return '/regex/';
}

const stringFormats = {
'email': emailSample,
'idn-email': emailSample, // https://tools.ietf.org/html/rfc6531#section-3.3
'password': passwordSample,
'date-time': dateTimeSample,
'date': dateSample,
'time': timeSample, // full-time in https://tools.ietf.org/html/rfc3339#section-5.6
'ipv4': ipv4Sample,
'ipv6': ipv6Sample,
'hostname': hostnameSample,
'idn-hostname': hostnameSample, // https://tools.ietf.org/html/rfc5890#section-2.3.2.3
'iri': iriSample, // https://tools.ietf.org/html/rfc3987
'iri-reference': iriReferenceSample,
'uri': uriSample,
'uri-reference': uriReferenceSample, // either a URI or relative-reference https://tools.ietf.org/html/rfc3986#section-4.1
'uri-template': uriTemplateSample,
'uuid': uuidSample,
'default': defaultSample
'default': defaultSample,
'json-pointer': jsonPointerSample,
'relative-json-pointer': relativeJsonPointerSample, // https://tools.ietf.org/html/draft-handrews-relative-json-pointer-01
'regex': regexSample,
};

export function sampleString(schema, options, spec, context) {
Expand Down
12 changes: 8 additions & 4 deletions src/traverse.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { _samplers } from './openapi-sampler';
import { allOfSample } from './allOf';
import { inferType } from './infer';
import { getResultForCircular, popSchemaStack } from './utils';
import { getResultForCircular, mergeDeep, popSchemaStack } from './utils';
import JsonPointer from 'json-pointer';

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

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

if (schema.if && schema.then) {
return traverse(mergeDeep(schema.if, schema.then), options, spec, context);
}

let example = null;
let type = null;
if (schema.default !== undefined) {
Expand All @@ -98,6 +99,9 @@ export function traverse(schema, options, spec, context) {
example = schema.examples[0];
} else {
type = schema.type;
if (Array.isArray(type) && schema.type.length > 0) {
type = schema.type[0];
}
if (!type) {
type = inferType(schema);
}
Expand Down
10 changes: 10 additions & 0 deletions src/types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { JSONSchema7 } from 'json-schema';

export interface Options {
readonly skipNonRequired?: boolean;
readonly skipReadOnly?: boolean;
readonly skipWriteOnly?: boolean;
readonly quiet?: boolean;
}

export function sample(schema: JSONSchema7, options?: Options, document?: object): unknown;
8 changes: 4 additions & 4 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ function pad(number) {
return number;
}

export function toRFCDateTime(date, omitTime, milliseconds) {
var res = date.getUTCFullYear() +
export function toRFCDateTime(date, omitTime, omitDate, milliseconds) {
var res = omitDate ? '' : (date.getUTCFullYear() +
'-' + pad(date.getUTCMonth() + 1) +
'-' + pad(date.getUTCDate());
'-' + pad(date.getUTCDate()));
if (!omitTime) {
res += 'T' + pad(date.getUTCHours()) +
':' + pad(date.getUTCMinutes()) +
Expand Down Expand Up @@ -92,4 +92,4 @@ function jsf32(a, b, c, d) {
d = a + t | 0;
return (d >>> 0) / 4294967296;
}
}
}
Loading

0 comments on commit 8444461

Please sign in to comment.