diff --git a/src/__fixtures__/array-of-allofs.json b/src/__fixtures__/array-of-allofs.json new file mode 100644 index 00000000..7fc961ef --- /dev/null +++ b/src/__fixtures__/array-of-allofs.json @@ -0,0 +1,28 @@ +{ + "title": "Test", + "type": "object", + "properties": { + "array-all-objects": { + "type": "array", + "items": { + "allOf": [ + { + "properties": { + "foo": { + "type": "string" + } + } + }, + { + "properties": { + "bar": { + "type": "string" + } + } + } + ], + "type": "object" + } + } + } +} diff --git a/src/components/JsonSchemaViewer.tsx b/src/components/JsonSchemaViewer.tsx index a0a74f16..1eda17a4 100644 --- a/src/components/JsonSchemaViewer.tsx +++ b/src/components/JsonSchemaViewer.tsx @@ -6,9 +6,10 @@ import * as React from 'react'; import SchemaWorker, { WebWorker } from 'web-worker:../workers/schema.ts'; import { JSONSchema4 } from 'json-schema'; -import { GoToRefHandler, RowRenderer, SchemaTreeListNode } from '../types'; -import { isCombiner } from '../utils/isCombiner'; +import { GoToRefHandler, IArrayNode, IBaseNode, RowRenderer, SchemaKind, SchemaTreeListNode } from '../types'; +import { getArraySubtype } from '../utils/getArraySubtype'; import { isSchemaViewerEmpty } from '../utils/isSchemaViewerEmpty'; +import { isCombinerNode } from '../utils/nodes'; import { renderSchema } from '../utils/renderSchema'; import { ComputeSchemaMessageData, isRenderedSchemaMessage } from '../workers/messages'; import { SchemaTree } from './SchemaTree'; @@ -111,17 +112,14 @@ export class JsonSchemaViewerComponent extends React.PureComponent = ({ node, onGoToRef }) => { - const type = isRef(node) ? '$ref' : isCombiner(node) ? node.combiner : node.type; + const type = isRefNode(node) ? '$ref' : isCombinerNode(node) ? node.combiner : node.type; const subtype = type === SchemaKind.Array && (node as IArrayNode).items !== undefined ? inferType((node as IArrayNode).items!) diff --git a/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap b/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap index 36cd5d77..1a0ecdf1 100644 --- a/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap +++ b/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap @@ -1,5 +1,234 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`renderSchema util should match array-of-allofs.json 1`] = ` +Array [ + Object { + "canHaveChildren": true, + "id": "random-id", + "level": 0, + "metadata": Object { + "additionalProperties": undefined, + "annotations": Object { + "title": "Test", + }, + "enum": undefined, + "id": "random-id", + "path": Array [], + "patternProperties": undefined, + "properties": Object { + "array-all-objects": Object { + "items": Object { + "allOf": Array [ + Object { + "properties": Object { + "foo": Object { + "type": "string", + }, + }, + "type": "object", + }, + Object { + "properties": Object { + "bar": Object { + "type": "string", + }, + }, + "type": "object", + }, + ], + "type": "object", + }, + "type": "array", + }, + }, + "type": "object", + "validations": Object {}, + }, + "name": "", + }, + Object { + "canHaveChildren": true, + "id": "random-id", + "level": 1, + "metadata": Object { + "additionalItems": undefined, + "annotations": Object {}, + "enum": undefined, + "id": "random-id", + "items": Object { + "allOf": Array [ + Object { + "properties": Object { + "foo": Object { + "type": "string", + }, + }, + "type": "object", + }, + Object { + "properties": Object { + "bar": Object { + "type": "string", + }, + }, + "type": "object", + }, + ], + "type": "object", + }, + "name": "array-all-objects", + "path": Array [ + "properties", + "array-all-objects", + ], + "required": false, + "subtype": "allOf", + "type": "array", + "validations": Object {}, + }, + "name": "", + }, + Object { + "canHaveChildren": true, + "id": "random-id", + "level": 2, + "metadata": Object { + "annotations": Object {}, + "combiner": "allOf", + "id": "random-id", + "name": "array-all-objects", + "path": Array [ + "properties", + "array-all-objects", + "items", + ], + "properties": Array [ + Object { + "properties": Object { + "foo": Object { + "type": "string", + }, + }, + "type": "object", + }, + Object { + "properties": Object { + "bar": Object { + "type": "string", + }, + }, + "type": "object", + }, + ], + "required": false, + "type": "object", + }, + "name": "", + }, + Object { + "canHaveChildren": true, + "id": "random-id", + "level": 3, + "metadata": Object { + "additionalProperties": undefined, + "annotations": Object {}, + "enum": undefined, + "id": "random-id", + "path": Array [ + "properties", + "array-all-objects", + "items", + "allOf", + 0, + ], + "patternProperties": undefined, + "properties": Object { + "foo": Object { + "type": "string", + }, + }, + "type": "object", + "validations": Object {}, + }, + "name": "", + }, + Object { + "id": "random-id", + "level": 4, + "metadata": Object { + "annotations": Object {}, + "enum": undefined, + "id": "random-id", + "name": "foo", + "path": Array [ + "properties", + "array-all-objects", + "items", + "allOf", + 0, + "properties", + "foo", + ], + "required": false, + "type": "string", + "validations": Object {}, + }, + "name": "", + }, + Object { + "canHaveChildren": true, + "id": "random-id", + "level": 3, + "metadata": Object { + "additionalProperties": undefined, + "annotations": Object {}, + "divider": "and", + "enum": undefined, + "id": "random-id", + "path": Array [ + "properties", + "array-all-objects", + "items", + "allOf", + 1, + ], + "patternProperties": undefined, + "properties": Object { + "bar": Object { + "type": "string", + }, + }, + "type": "object", + "validations": Object {}, + }, + "name": "", + }, + Object { + "id": "random-id", + "level": 4, + "metadata": Object { + "annotations": Object {}, + "enum": undefined, + "id": "random-id", + "name": "bar", + "path": Array [ + "properties", + "array-all-objects", + "items", + "allOf", + 1, + "properties", + "bar", + ], + "required": false, + "type": "string", + "validations": Object {}, + }, + "name": "", + }, +] +`; + exports[`renderSchema util should match array-of-objects.json 1`] = ` Array [ Object { diff --git a/src/utils/__tests__/isCombiner.spec.ts b/src/utils/__tests__/isCombiner.spec.ts index eac15c15..cfe0da4c 100644 --- a/src/utils/__tests__/isCombiner.spec.ts +++ b/src/utils/__tests__/isCombiner.spec.ts @@ -1,12 +1,12 @@ -import { isCombiner } from '../isCombiner'; +import { isCombinerNode } from '../nodes'; -describe('isCombiner function', () => { +describe('isCombinerNode function', () => { test('should return false if object without combiner is given', () => { - expect(isCombiner({} as any)).toBe(false); - expect(isCombiner({ properties: [] } as any)).toBe(false); + expect(isCombinerNode({} as any)).toBe(false); + expect(isCombinerNode({ properties: [] } as any)).toBe(false); }); test.each(['allOf', 'anyOf', 'oneOf'])('should return true if object with %s is given', combiner => { - expect(isCombiner({ combiner } as any)).toBe(true); + expect(isCombinerNode({ combiner } as any)).toBe(true); }); }); diff --git a/src/utils/__tests__/renderSchema.spec.ts b/src/utils/__tests__/renderSchema.spec.ts index 9a9bae6a..0a5874ef 100644 --- a/src/utils/__tests__/renderSchema.spec.ts +++ b/src/utils/__tests__/renderSchema.spec.ts @@ -15,6 +15,7 @@ describe('renderSchema util', () => { ['combiner-schema.json', ''], ['array-of-objects.json', ''], ['array-of-refs.json', ''], + ['array-of-allofs.json', ''], ['tickets.schema.json', ''], ])('should match %s', (schema, dereferenced) => { expect( diff --git a/src/utils/getArraySubtype.ts b/src/utils/getArraySubtype.ts new file mode 100644 index 00000000..7967bd02 --- /dev/null +++ b/src/utils/getArraySubtype.ts @@ -0,0 +1,21 @@ +import { JSONSchema4TypeName } from 'json-schema'; +import { IArrayNode, JSONSchema4CombinerName } from '../types'; +import { getCombiner } from './getCombiner'; +import { inferType } from './inferType'; + +export function getArraySubtype( + node: IArrayNode, +): JSONSchema4TypeName | JSONSchema4TypeName[] | JSONSchema4CombinerName | string | undefined { + if (!node.items || Array.isArray(node.items)) return; + if ('$ref' in node.items) { + return `$ref( ${node.items.$ref} )`; + } + + const combiner = getCombiner(node.items); + + if (combiner !== undefined) { + return combiner; + } + + return inferType(node.items); +} diff --git a/src/utils/getCombiner.ts b/src/utils/getCombiner.ts new file mode 100644 index 00000000..1ae565ac --- /dev/null +++ b/src/utils/getCombiner.ts @@ -0,0 +1,8 @@ +import { JSONSchema4 } from 'json-schema'; +import { JSONSchema4CombinerName } from '../types'; + +export const getCombiner = (node: JSONSchema4): JSONSchema4CombinerName | void => { + if ('allOf' in node) return 'allOf'; + if ('anyOf' in node) return 'anyOf'; + if ('oneOf' in node) return 'oneOf'; +}; diff --git a/src/utils/isCombiner.ts b/src/utils/isCombiner.ts index 50751c61..0d99b825 100644 --- a/src/utils/isCombiner.ts +++ b/src/utils/isCombiner.ts @@ -1,3 +1,5 @@ -import { ICombinerNode, SchemaNode } from '../types'; +import { JSONSchema4CombinerName } from '../types'; -export const isCombiner = (node: SchemaNode): node is ICombinerNode => 'combiner' in node; +const combinerTypes = ['allOf', 'oneOf', 'anyOf']; + +export const isCombiner = (type: string): type is JSONSchema4CombinerName => combinerTypes.includes(type); diff --git a/src/utils/isRef.ts b/src/utils/isRef.ts deleted file mode 100644 index bf9ab153..00000000 --- a/src/utils/isRef.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { IRefNode, SchemaNode } from '../types'; - -export const isRef = (node: SchemaNode): node is IRefNode => '$ref' in node; diff --git a/src/utils/isSchemaViewerEmpty.ts b/src/utils/isSchemaViewerEmpty.ts index 6ff99d73..7c40150b 100644 --- a/src/utils/isSchemaViewerEmpty.ts +++ b/src/utils/isSchemaViewerEmpty.ts @@ -1,12 +1,11 @@ import { get as _get, isEmpty as _isEmpty } from 'lodash-es'; - -const combinerTypes = ['allOf', 'oneOf', 'anyOf']; +import { isCombiner } from './isCombiner'; export const isSchemaViewerEmpty = (schema: unknown) => { if (typeof schema !== 'object' || schema === null) return true; const objectKeys = Object.keys(schema); - if (objectKeys.length === 1 && combinerTypes.includes(objectKeys[0])) { + if (objectKeys.length === 1 && isCombiner(objectKeys[0])) { return _isEmpty(_get(schema, objectKeys[0], [])); } diff --git a/src/utils/nodes.ts b/src/utils/nodes.ts new file mode 100644 index 00000000..916883c2 --- /dev/null +++ b/src/utils/nodes.ts @@ -0,0 +1,5 @@ +import { ICombinerNode, IRefNode, SchemaNode } from '../types'; + +export const isRefNode = (node: SchemaNode): node is IRefNode => '$ref' in node; + +export const isCombinerNode = (node: SchemaNode): node is ICombinerNode => 'combiner' in node; diff --git a/src/utils/renderSchema.ts b/src/utils/renderSchema.ts index f1fc685b..d7c977e1 100644 --- a/src/utils/renderSchema.ts +++ b/src/utils/renderSchema.ts @@ -2,10 +2,10 @@ import { JSONSchema4 } from 'json-schema'; import { isEmpty as _isEmpty } from 'lodash-es'; import { IArrayNode, IObjectNode, ITreeNodeMeta, SchemaKind, SchemaTreeListNode } from '../types'; import { DIVIDERS } from './dividers'; +import { getArraySubtype } from './getArraySubtype'; import { getPrimaryType } from './getPrimaryType'; -import { inferType } from './inferType'; import { isCombiner } from './isCombiner'; -import { isRef } from './isRef'; +import { isCombinerNode, isRefNode } from './nodes'; import { walk } from './walk'; export type WalkingOptions = { @@ -68,16 +68,13 @@ export const renderSchema: Walker = function*(schema, level = 0, meta = { path: ...meta, ...(parsedSchema.items !== undefined && !Array.isArray(parsedSchema.items) && { - subtype: - '$ref' in parsedSchema.items - ? `$ref( ${parsedSchema.items.$ref} )` - : parsedSchema.items.type || inferType(parsedSchema.items), + subtype: getArraySubtype(parsedSchema as IArrayNode), }), path, }, }; - if (isRef(node)) { + if (isRefNode(node)) { // we expect the schema to be dereferenced // const resolved = lookupRef(path, dereferencedSchema); // if (resolved) { @@ -98,7 +95,7 @@ export const renderSchema: Walker = function*(schema, level = 0, meta = { path: $ref: node.$ref, }, } as SchemaTreeListNode; - } else if (isCombiner(node)) { + } else if (isCombinerNode(node)) { yield { ...baseNode, canHaveChildren: true, @@ -123,7 +120,11 @@ export const renderSchema: Walker = function*(schema, level = 0, meta = { path: ...baseNode, ...('items' in node && !_isEmpty(node.items) && - (baseNode.metadata!.subtype === 'object' || Array.isArray(node.items)) && { canHaveChildren: true }), + (baseNode.metadata!.subtype === 'object' || + Array.isArray(node.items) || + (typeof baseNode.metadata!.subtype === 'string' && isCombiner(baseNode.metadata!.subtype))) && { + canHaveChildren: true, + }), metadata: { ...baseNode.metadata, // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.1.2 @@ -139,7 +140,8 @@ export const renderSchema: Walker = function*(schema, level = 0, meta = { path: }); } } else if (parsedSchema.items) { - switch (baseNode.metadata && baseNode.metadata.subtype) { + const subtype = baseNode.metadata!.subtype; + switch (subtype) { case SchemaKind.Object: yield* getProperties(parsedSchema.items, level, { ...meta, @@ -152,6 +154,13 @@ export const renderSchema: Walker = function*(schema, level = 0, meta = { path: path: [...path, 'items'], }); break; + default: + if (typeof subtype === 'string' && isCombiner(subtype)) { + yield* renderSchema(parsedSchema.items, level + 1, { + ...meta, + path: [...path, 'items'], + }); + } } } diff --git a/src/utils/walk.ts b/src/utils/walk.ts index 95c053a0..f690e26f 100644 --- a/src/utils/walk.ts +++ b/src/utils/walk.ts @@ -5,7 +5,6 @@ import { ICombinerNode, IObjectNode, IRefNode, - JSONSchema4CombinerName, SchemaKind, SchemaNode, } from '../types'; @@ -14,12 +13,7 @@ import { getAnnotations } from './getAnnotations'; import { getPrimaryType } from './getPrimaryType'; import { getValidations } from './getValidations'; import { inferType } from './inferType'; - -const getCombiner = (node: JSONSchema4): JSONSchema4CombinerName | void => { - if ('allOf' in node) return 'allOf'; - if ('anyOf' in node) return 'anyOf'; - if ('oneOf' in node) return 'oneOf'; -}; +import { getCombiner } from './getCombiner'; function assignNodeSpecificFields(base: IBaseNode, node: JSONSchema4) { switch (getPrimaryType(node)) {