diff --git a/src/__fixtures__/array-of-refs.json b/src/__fixtures__/array-of-refs.json new file mode 100644 index 00000000..ff8841f5 --- /dev/null +++ b/src/__fixtures__/array-of-refs.json @@ -0,0 +1,6 @@ +{ + "type": "array", + "items": { + "$ref": "./models/todo-full.json" + } +} diff --git a/src/components/SchemaTree.tsx b/src/components/SchemaTree.tsx index 56ad0df2..2056b13d 100644 --- a/src/components/SchemaTree.tsx +++ b/src/components/SchemaTree.tsx @@ -2,7 +2,7 @@ import { TreeList, TreeListMouseEventHandler, TreeStore } from '@stoplight/tree- import { Omit } from '@stoplight/types'; import { Box, IBox, ThemeZone } from '@stoplight/ui-kit'; import { JSONSchema4 } from 'json-schema'; -import _isEmpty = require('lodash/isEmpty'); +import { isEmpty as _isEmpty } from 'lodash'; import * as React from 'react'; import { useMetadata } from '../hooks'; import { useTheme } from '../theme'; diff --git a/src/components/Type.tsx b/src/components/Type.tsx index f6c76262..8fba1680 100644 --- a/src/components/Type.tsx +++ b/src/components/Type.tsx @@ -2,7 +2,7 @@ import { Box, IBoxCSS } from '@stoplight/ui-kit'; import { JSONSchema4TypeName } from 'json-schema'; import * as React from 'react'; import { IJsonSchemaViewerTheme, useTheme } from '../theme'; -import { JSONSchema4CombinerName } from '../types'; +import { ITreeNodeMeta, JSONSchema4CombinerName } from '../types'; export const Type: React.FunctionComponent = ({ type, subtype, children }) => { const theme = useTheme() as IJsonSchemaViewerTheme; @@ -18,7 +18,7 @@ export const Type: React.FunctionComponent = ({ type, subtype, children } export interface IType { type: JSONSchema4TypeName | JSONSchema4CombinerName | '$ref'; - subtype?: JSONSchema4TypeName | JSONSchema4TypeName[]; + subtype?: ITreeNodeMeta['subtype']; } export const rowStyles = (theme: IJsonSchemaViewerTheme, { type }: IType): IBoxCSS => { diff --git a/src/components/Types.tsx b/src/components/Types.tsx index 0a2593f4..454fd1b2 100644 --- a/src/components/Types.tsx +++ b/src/components/Types.tsx @@ -1,12 +1,12 @@ import { JSONSchema4TypeName } from 'json-schema'; import * as React from 'react'; -import { JSONSchema4CombinerName } from '../types'; +import { ITreeNodeMeta, JSONSchema4CombinerName } from '../types'; import { MutedText } from './common/MutedText'; import { Type } from './Type'; interface ITypes { type?: JSONSchema4TypeName | JSONSchema4TypeName[] | JSONSchema4CombinerName; - subtype?: JSONSchema4TypeName | JSONSchema4TypeName[]; + subtype?: ITreeNodeMeta['subtype']; } export const Types: React.FunctionComponent = ({ type, subtype }) => { diff --git a/src/types.ts b/src/types.ts index 7077e87f..6ab10c52 100644 --- a/src/types.ts +++ b/src/types.ts @@ -56,7 +56,7 @@ export interface ITreeNodeMeta { additional?: IArrayNode['additionalItems'] | IObjectNode['additionalProperties']; path: JsonPath; divider?: string; - subtype?: IBaseNode['type']; + subtype?: IBaseNode['type'] | string; expanded?: boolean; required?: boolean; inheritedFrom?: string; diff --git a/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap b/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap index cc0608c3..2930201e 100644 --- a/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap +++ b/src/utils/__tests__/__snapshots__/renderSchema.spec.ts.snap @@ -49,7 +49,9 @@ Array [ ], "required": false, "subtype": "object", - "type": "array", + "type": Array [ + "array", + ], "validations": Object {}, }, "name": "", @@ -77,6 +79,27 @@ Array [ ] `; +exports[`renderSchema util should match array-of-refs.json 1`] = ` +Array [ + Object { + "id": "random-id", + "level": 0, + "metadata": Object { + "additionalItems": undefined, + "annotations": Object {}, + "enum": undefined, + "id": "random-id", + "items": undefined, + "path": Array [], + "subtype": "$ref( ./models/todo-full.json )", + "type": "array", + "validations": Object {}, + }, + "name": "", + }, +] +`; + exports[`renderSchema util should match combiner-schema.json 1`] = ` Array [ Object { @@ -479,9 +502,11 @@ Array [ "id": "random-id", "level": 1, "metadata": Object { + "additionalItems": undefined, "annotations": Object {}, "enum": undefined, "id": "random-id", + "items": undefined, "name": "items", "path": Array [ "properties", diff --git a/src/utils/__tests__/renderSchema.spec.ts b/src/utils/__tests__/renderSchema.spec.ts index 6042afce..81f999fd 100644 --- a/src/utils/__tests__/renderSchema.spec.ts +++ b/src/utils/__tests__/renderSchema.spec.ts @@ -14,6 +14,7 @@ describe('renderSchema util', () => { ['ref/original.json', 'ref/resolved.json'], ['combiner-schema.json', ''], ['array-of-objects.json', ''], + ['array-of-refs.json', ''], ])('should match %s', (schema, dereferenced) => { expect( Array.from( diff --git a/src/utils/getAnnotations.ts b/src/utils/getAnnotations.ts index 3c423760..dbf0a857 100644 --- a/src/utils/getAnnotations.ts +++ b/src/utils/getAnnotations.ts @@ -1,5 +1,5 @@ import { JSONSchema4 } from 'json-schema'; -import _pick = require('lodash/pick'); +import { pick as _pick } from 'lodash'; import { JSONSchema4Annotations } from '../types'; const ANNOTATIONS: JSONSchema4Annotations[] = ['title', 'description', 'default', 'examples']; diff --git a/src/utils/getMetadata.ts b/src/utils/getMetadata.ts index 51e1e026..639babf7 100644 --- a/src/utils/getMetadata.ts +++ b/src/utils/getMetadata.ts @@ -1,5 +1,5 @@ import { JSONSchema4 } from 'json-schema'; -import _pick = require('lodash/pick'); +import { pick as _pick } from 'lodash'; import { JSONSchema4Metadata } from '../types'; const METADATA: JSONSchema4Metadata[] = ['id', '$schema']; diff --git a/src/utils/getPrimaryType.ts b/src/utils/getPrimaryType.ts new file mode 100644 index 00000000..987635b7 --- /dev/null +++ b/src/utils/getPrimaryType.ts @@ -0,0 +1,21 @@ +import { JSONSchema4 } from 'json-schema'; +import { SchemaKind } from '../types'; +import { inferType } from './inferType'; + +export function getPrimaryType(node: JSONSchema4) { + if (node.type !== undefined) { + if (Array.isArray(node.type)) { + if (node.type.includes(SchemaKind.Object)) { + return SchemaKind.Object; + } + + if (node.type.includes(SchemaKind.Array)) { + return SchemaKind.Array; + } + } + + return node.type; + } + + return inferType(node); +} diff --git a/src/utils/getValidations.ts b/src/utils/getValidations.ts index 2b61b5d7..cf0e1724 100644 --- a/src/utils/getValidations.ts +++ b/src/utils/getValidations.ts @@ -1,7 +1,6 @@ import { Dictionary } from '@stoplight/types'; import { JSONSchema4, JSONSchema4TypeName } from 'json-schema'; -import _flatMap = require('lodash/flatMap'); -import _pick = require('lodash/pick'); +import { flatMap as _flatMap, pick as _pick } from 'lodash'; export const COMMON_VALIDATION_TYPES = [ 'enum', // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.5.1 diff --git a/src/utils/inferType.ts b/src/utils/inferType.ts new file mode 100644 index 00000000..eb7a37c5 --- /dev/null +++ b/src/utils/inferType.ts @@ -0,0 +1,14 @@ +import { JSONSchema4, JSONSchema4TypeName } from 'json-schema'; +import { SchemaKind } from '../types'; + +export function inferType(node: JSONSchema4): JSONSchema4TypeName | undefined { + if ('properties' in node) { + return SchemaKind.Object; + } + + if ('items' in node) { + return SchemaKind.Array; + } + + return; +} diff --git a/src/utils/isSchemaViewerEmpty.ts b/src/utils/isSchemaViewerEmpty.ts index f590f675..1c9cde81 100644 --- a/src/utils/isSchemaViewerEmpty.ts +++ b/src/utils/isSchemaViewerEmpty.ts @@ -1,6 +1,5 @@ import { ISchema } from '@stoplight/types'; -import get = require('lodash/get'); -import isEmpty = require('lodash/isEmpty'); +import { get as _get, isEmpty as _isEmpty } from 'lodash'; const combinerTypes = ['allOf', 'oneOf', 'anyOf']; @@ -8,7 +7,7 @@ export const isSchemaViewerEmpty = (schema: ISchema) => { const objectKeys = Object.keys(schema); if (objectKeys.length === 1 && combinerTypes.includes(objectKeys[0])) { - return isEmpty(get(schema, objectKeys[0], [])); + return _isEmpty(_get(schema, objectKeys[0], [])); } return false; diff --git a/src/utils/lookupRef.ts b/src/utils/lookupRef.ts index ae4fb4e8..7d2e4b43 100644 --- a/src/utils/lookupRef.ts +++ b/src/utils/lookupRef.ts @@ -1,6 +1,6 @@ import { JsonPath } from '@stoplight/types'; import { JSONSchema4 } from 'json-schema'; -import _get = require('lodash/get'); +import { get as _get } from 'lodash'; export const lookupRef = (path: JsonPath, dereferencedSchema?: JSONSchema4): JSONSchema4 | null => { if (dereferencedSchema === undefined) { diff --git a/src/utils/renderSchema.ts b/src/utils/renderSchema.ts index 1756a823..3398bb99 100644 --- a/src/utils/renderSchema.ts +++ b/src/utils/renderSchema.ts @@ -1,7 +1,8 @@ import { JSONSchema4 } from 'json-schema'; -import _isEmpty = require('lodash/isEmpty'); +import { isEmpty as _isEmpty } from 'lodash'; import { IArrayNode, IObjectNode, ITreeNodeMeta, SchemaKind, SchemaTreeListNode } from '../types'; import { DIVIDERS } from './dividers'; +import { getPrimaryType } from './getPrimaryType'; import { isCombiner } from './isCombiner'; import { isRef } from './isRef'; import { lookupRef } from './lookupRef'; @@ -51,7 +52,10 @@ export const renderSchema: Walker = function*(schema, dereferencedSchema, level metadata: { ...node, ...meta, - ...(schema.items !== undefined && !Array.isArray(schema.items) && { subtype: schema.items.type }), + ...(schema.items !== undefined && + !Array.isArray(schema.items) && { + subtype: '$ref' in schema.items ? `$ref( ${schema.items.$ref} )` : schema.items.type, + }), path, }, }; @@ -90,62 +94,69 @@ export const renderSchema: Walker = function*(schema, dereferencedSchema, level }); } } - } else if (node.type === SchemaKind.Array) { - yield { - ...baseNode, - ...('items' in node && - !_isEmpty(node.items) && - !('subtype' in baseNode.metadata!) && { canHaveChildren: true }), - metadata: { - ...baseNode.metadata, - // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.1.2 - ...(!('subtype' in baseNode) && - (node as IArrayNode).additionalItems && { additional: (node as IArrayNode).additionalItems }), - }, - } as SchemaTreeListNode; - if (Array.isArray(schema.items)) { - for (const [i, property] of schema.items.entries()) { - yield* renderSchema(property, dereferencedSchema, level + 1, { - path: [...path, 'items', i], + } else { + switch (getPrimaryType(node)) { + case SchemaKind.Array: + yield { + ...baseNode, + ...('items' in node && + !_isEmpty(node.items) && + !('subtype' in baseNode.metadata!) && { canHaveChildren: true }), + metadata: { + ...baseNode.metadata, + // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-5.3.1.2 + ...(!('subtype' in baseNode) && + (node as IArrayNode).additionalItems && { additional: (node as IArrayNode).additionalItems }), + }, + } as SchemaTreeListNode; + + if (Array.isArray(schema.items)) { + for (const [i, property] of schema.items.entries()) { + yield* renderSchema(property, dereferencedSchema, level + 1, { + path: [...path, 'items', i], + }); + } + } else if (schema.items) { + switch (baseNode.metadata && baseNode.metadata.subtype) { + case SchemaKind.Object: + yield* getProperties(schema.items, dereferencedSchema, level + 1, { + ...meta, + path: [...path, 'items'], + }); + break; + case SchemaKind.Array: + yield* renderSchema(schema.items, dereferencedSchema, level + 1, { + path, + }); + break; + } + } + + break; + case SchemaKind.Object: + yield { + ...baseNode, + ...('properties' in node && !_isEmpty(node.properties) && { canHaveChildren: true }), + metadata: { + ...baseNode.metadata, + ...((node as IObjectNode).additionalProperties && { + additional: (node as IObjectNode).additionalProperties, + }), + }, + } as SchemaTreeListNode; + + yield* getProperties(schema, dereferencedSchema, level, { + path: [...path, 'properties'], }); - } - } else if (schema.items) { - switch (baseNode.metadata && baseNode.metadata.subtype) { - case SchemaKind.Object: - yield* getProperties(schema.items, dereferencedSchema, level + 1, { - ...meta, - path: [...path, 'items'], - }); - break; - case SchemaKind.Array: - yield* renderSchema(schema.items, dereferencedSchema, level + 1, { - path, - }); - break; - } - } - } else if ('properties' in node) { - // special case :P, it's - yield { - ...baseNode, - ...('properties' in node && !_isEmpty(node.properties) && { canHaveChildren: true }), - metadata: { - ...baseNode.metadata, - ...((node as IObjectNode).additionalProperties && { - additional: (node as IObjectNode).additionalProperties, - }), - }, - } as SchemaTreeListNode; - yield* getProperties(schema, dereferencedSchema, level, { - path: [...path, 'properties'], - }); + yield* getPatternProperties(schema, dereferencedSchema, level, { + path: [...path, 'patternProperties'], + }); - yield* getPatternProperties(schema, dereferencedSchema, level, { - path: [...path, 'patternProperties'], - }); - } else { - yield baseNode; + break; + default: + yield baseNode; + } } } }; diff --git a/src/utils/walk.ts b/src/utils/walk.ts index 4e1b6e27..f71483d4 100644 --- a/src/utils/walk.ts +++ b/src/utils/walk.ts @@ -11,7 +11,9 @@ import { } from '../types'; import { assignId } from './assignId'; 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'; @@ -20,7 +22,7 @@ const getCombiner = (node: JSONSchema4): JSONSchema4CombinerName | void => { }; function assignNodeSpecificFields(base: IBaseNode, node: JSONSchema4) { - switch (getType(node)) { + switch (getPrimaryType(node)) { case SchemaKind.Array: (base as IArrayNode).items = node.array; (base as IArrayNode).additionalItems = node.additionalItems; @@ -33,41 +35,9 @@ function assignNodeSpecificFields(base: IBaseNode, node: JSONSchema4) { } } -function getType(node: JSONSchema4) { - if (Array.isArray(node.type)) { - return node.type.length === 1 ? node.type[0] : node.type; - } - - return node.type; -} - function processNode(node: JSONSchema4): SchemaNode | void { const combiner = getCombiner(node); - if (node.type !== undefined && combiner === undefined) { - const base: IBaseNode = { - id: assignId(node), - type: getType(node), - validations: getValidations(node), - annotations: getAnnotations(node), - enum: node.enum, - }; - - if (Array.isArray(base.type)) { - if (base.type.includes('object')) { - // special case :P - assignNodeSpecificFields(base, { - ...node, - type: 'object', - }); - } - } else { - assignNodeSpecificFields(base, node); - } - - return base; - } - if ('enum' in node) { return { id: assignId(node), @@ -84,7 +54,19 @@ function processNode(node: JSONSchema4): SchemaNode | void { } as IRefNode; } - if (combiner !== undefined) { + if (combiner === undefined) { + const base: IBaseNode = { + id: assignId(node), + type: node.type || inferType(node), + validations: getValidations(node), + annotations: getAnnotations(node), + enum: node.enum, + }; + + assignNodeSpecificFields(base, node); + + return base; + } else { return { id: assignId(node), combiner,