Skip to content

Commit

Permalink
feat: add support for circular references in schema object (#115)
Browse files Browse the repository at this point in the history
  • Loading branch information
derberg authored Sep 10, 2020
1 parent 7abda89 commit 8da5184
Show file tree
Hide file tree
Showing 4 changed files with 177 additions and 7 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ bower_components

# Users Environment Variables
.lock-wscript
package-lock.json
26 changes: 19 additions & 7 deletions src/traverse.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,26 @@
import { _samplers } from './openapi-sampler';
import { allOfSample } from './allOf';
import { inferType } from './infer';
import { getResultForCircular, popSchemaStack } from './utils';
import JsonPointer from 'json-pointer';

let $refCache = {};
// for circular JS references we use additional array and not object as we need to compare entire schemas and not strings
let seenSchemasStack = [];

export function clearCache() {
$refCache = {};
seenSchemasStack = [];
}

export function traverse(schema, options, spec, context) {
// checking circular JS references by checking context
// because context is passed only when traversing through nested objects happens
if (context) {
if (seenSchemasStack.includes(schema)) return getResultForCircular(inferType(schema));
seenSchemasStack.push(schema);
}

if (schema.$ref) {
if (!spec) {
throw new Error('Your schema contains $ref. You must provide full specification in the third parameter.');
Expand All @@ -29,17 +40,14 @@ export function traverse(schema, options, spec, context) {
$refCache[ref] = false;
} else {
const referencedType = inferType(referenced);
result = {
value: referencedType === 'object' ?
{}
: referencedType === 'array' ? [] : undefined
};
result = getResultForCircular(referencedType);
}

popSchemaStack(seenSchemasStack, context);
return result;
}

if (schema.example !== undefined) {
popSchemaStack(seenSchemasStack, context);
return {
value: schema.example,
readOnly: schema.readOnly,
Expand All @@ -49,6 +57,7 @@ export function traverse(schema, options, spec, context) {
}

if (schema.allOf !== undefined) {
popSchemaStack(seenSchemasStack, context);
return allOfSample(
{ ...schema, allOf: undefined },
schema.allOf,
Expand All @@ -61,10 +70,12 @@ export function traverse(schema, options, spec, context) {
if (schema.anyOf) {
if (!options.quiet) console.warn('oneOf and anyOf are not supported on the same level. Skipping anyOf');
}
popSchemaStack(seenSchemasStack, context);
return traverse(schema.oneOf[0], options, spec);
}

if (schema.anyOf && schema.anyOf.length) {
popSchemaStack(seenSchemasStack, context);
return traverse(schema.anyOf[0], options, spec);
}

Expand All @@ -88,7 +99,8 @@ export function traverse(schema, options, spec, context) {
example = sampler(schema, options, spec, context);
}
}


popSchemaStack(seenSchemasStack, context);
return {
value: example,
readOnly: schema.readOnly,
Expand Down
12 changes: 12 additions & 0 deletions src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,18 @@ export function uuid(str) {
return uuid;
}

export function getResultForCircular(type) {
return {
value: type === 'object' ?
{}
: type === 'array' ? [] : undefined
};
}

export function popSchemaStack(seenSchemasStack, context) {
if (context) seenSchemasStack.pop();
}

function hashCode(str) {
var hash = 0;
if (str.length == 0) return hash;
Expand Down
145 changes: 145 additions & 0 deletions test/integration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -603,4 +603,149 @@ describe('Integration', function() {
expect(result).to.deep.equal(expected);
});
});

describe('circular references in JS object', function() {

let result, schema, expected;

it('should not follow circular references in JS object', function() {
const someType = {
type: 'string'
};

const circularSchema = {
type: 'object',
properties: {
a: someType
}
}

circularSchema.properties.b = circularSchema;
schema = circularSchema;
result = OpenAPISampler.sample(schema);
expected = {
a: 'string',
b: {
a: 'string',
b: {}
}
};
expect(result).to.deep.equal(expected);
});

it('should not detect false-positive circular references in JS object', function() {
const a = {
type: 'string',
example: 'test'
};

const b = {
type: 'integer',
example: 1
};

const c = {
type: 'object',
properties: {
test: {
'type': 'string'
}
}
};

const d = {
type: 'array',
items: {
'type': 'string',
}
};

const e = {
allOf: [ c, c ]
};

const f = {
oneOf: [d, d ]
};

const g = {
anyOf: [ c, c ]
};

const h = { $ref: '#/a' };

const nonCircularSchema = {
type: 'object',
properties: {
a: a,
aa: a,
b: b,
bb: b,
c: c,
cc: c,
d: d,
dd: d,
e: e,
ee: e,
f: f,
ff: f,
g: g,
gg: g,
h: h,
hh: h
}
}

const spec = {
nonCircularSchema,
a: a
}
result = OpenAPISampler.sample(nonCircularSchema, {}, spec);

expected = {
a: 'test',
aa: 'test',
b: 1,
bb: 1,
c: {'test': 'string'},
cc: {'test': 'string'},
d: ['string'],
dd: ['string'],
e: {'test': 'string'},
ee: {'test': 'string'},
f: ['string'],
ff: ['string'],
g: {'test': 'string'},
gg: {'test': 'string'},
h: 'test',
hh: 'test'
};
expect(result).to.deep.equal(expected);
});

it('should not follow circular references in JS object when more that one circular reference present', function() {

const circularSchema = {
type: 'object',
properties: {}
}

circularSchema.properties.a = circularSchema;
circularSchema.properties.b = circularSchema;

schema = circularSchema;
result = OpenAPISampler.sample(schema);
expected = {
a: {
a: {},
b: {}
},
b: {
a: {},
b: {}
}
};
expect(result).to.deep.equal(expected);
});
});
});

0 comments on commit 8da5184

Please sign in to comment.