From ebcad8b7dd1025a3e0b6ff7373377f5a91691b82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ro=C5=BCek?= Date: Mon, 24 Feb 2020 22:25:08 +0800 Subject: [PATCH] fix: proper $refs unrolling --- src/components/SchemaRow.tsx | 2 +- src/components/__tests__/Property.spec.tsx | 124 ++++++++++++++------- src/components/shared/Property.tsx | 31 ++++-- src/tree/tree.ts | 31 ++++-- 4 files changed, 127 insertions(+), 61 deletions(-) diff --git a/src/components/SchemaRow.tsx b/src/components/SchemaRow.tsx index 22dbb005..17758a11 100644 --- a/src/components/SchemaRow.tsx +++ b/src/components/SchemaRow.tsx @@ -51,7 +51,7 @@ export const SchemaRow: React.FunctionComponent = ({ className, node node.parent.children[0] !== node && }
- + {description && }
diff --git a/src/components/__tests__/Property.spec.tsx b/src/components/__tests__/Property.spec.tsx index de590880..77e7c79f 100644 --- a/src/components/__tests__/Property.spec.tsx +++ b/src/components/__tests__/Property.spec.tsx @@ -1,86 +1,124 @@ import { shallow } from 'enzyme'; import 'jest-enzyme'; +import { JSONSchema4 } from 'json-schema'; import * as React from 'react'; -import { SchemaNode } from '../../types'; +import { metadataStore } from '../../tree/metadata'; +import { walk } from '../../tree/walk'; +import { SchemaTreeListNode } from '../../types'; import { Property, Types } from '../shared'; describe('Property component', () => { it('should render Types with proper type and subtype', () => { - const node: SchemaNode = { - id: '2', + const treeNode: SchemaTreeListNode = { + id: 'foo', + name: '', + parent: null, + }; + + const schema: JSONSchema4 = { type: 'array', items: { type: 'string', }, - annotations: { - examples: {}, - }, - validations: {}, }; - const wrapper = shallow(); + metadataStore.set(treeNode, { + schemaNode: walk(schema).next().value, + path: [], + schema, + }); + + const wrapper = shallow(); expect(wrapper.find(Types)).toExist(); expect(wrapper.find(Types)).toHaveProp('type', 'array'); expect(wrapper.find(Types)).toHaveProp('subtype', 'string'); }); it('should handle nullish items', () => { - const node = { - id: '1', + const treeNode: SchemaTreeListNode = { + id: 'foo', + name: '', + parent: null, + }; + + const schema: JSONSchema4 = { type: 'array', - items: null, - annotations: { - examples: {}, - }, - validations: {}, - } as SchemaNode; + items: null as any, + }; - const wrapper = shallow(); + metadataStore.set(treeNode, { + schemaNode: walk(schema).next().value, + path: [], + schema, + }); + + const wrapper = shallow(); expect(wrapper).not.toBeEmptyRender(); }); describe('properties counter', () => { it('given missing properties property, should not display the counter', () => { - const node = { - id: '1', + const treeNode: SchemaTreeListNode = { + id: 'foo', + name: '', + parent: null, + }; + + const schema: JSONSchema4 = { type: 'object', - annotations: { - examples: {}, - }, - validations: {}, - } as SchemaNode; + }; + + metadataStore.set(treeNode, { + schemaNode: walk(schema).next().value, + path: [], + schema, + }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.findWhere(el => /^\{\d\}$/.test(el.text()))).not.toExist(); }); it('given nullish properties property, should not display the counter', () => { - const node = { - id: '1', - properties: null, + const treeNode: SchemaTreeListNode = { + id: 'foo', + name: '', + parent: null, + }; + + const schema: JSONSchema4 = { type: 'object', - annotations: { - examples: {}, - }, - validations: {}, - } as SchemaNode; + properties: null as any, + }; + + metadataStore.set(treeNode, { + schemaNode: walk(schema).next().value, + path: [], + schema, + }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.findWhere(el => /^\{\d\}$/.test(el.text()))).not.toExist(); }); it('given object properties property, should display the counter', () => { - const node = { - id: '1', - properties: {}, + const treeNode: SchemaTreeListNode = { + id: 'foo', + name: '', + parent: null, + }; + + const schema: JSONSchema4 = { type: 'object', - annotations: { - examples: {}, - }, - validations: {}, - } as SchemaNode; + properties: {}, + }; + + metadataStore.set(treeNode, { + schemaNode: walk(schema).next().value, + path: [], + schema, + }); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.findWhere(el => /^\{\d\}$/.test(el.text())).first()).toHaveText('{0}'); }); }); diff --git a/src/components/shared/Property.tsx b/src/components/shared/Property.tsx index 3ee227b8..723b5b96 100644 --- a/src/components/shared/Property.tsx +++ b/src/components/shared/Property.tsx @@ -1,16 +1,17 @@ import { isLocalRef } from '@stoplight/json'; -import { JsonPath, Optional } from '@stoplight/types'; +import { Optional } from '@stoplight/types'; import { JSONSchema4 } from 'json-schema'; import { isObject as _isObject, size as _size } from 'lodash'; import * as React from 'react'; -import { GoToRefHandler, IArrayNode, IObjectNode, SchemaKind, SchemaNode } from '../../types'; +import { getNodeMetadata } from '../../tree'; +import { GoToRefHandler, IArrayNode, IObjectNode, SchemaKind, SchemaNode, SchemaTreeListNode } from '../../types'; +import { getPrimaryType } from '../../utils/getPrimaryType'; import { isArrayNodeWithItems, isCombinerNode, isRefNode } from '../../utils/guards'; import { inferType } from '../../utils/inferType'; import { Types } from './Types'; export interface IProperty { - node: SchemaNode; - path: JsonPath; + node: SchemaTreeListNode; onGoToRef?: GoToRefHandler; } @@ -22,7 +23,21 @@ function count(obj: Optional): number | null { return null; } -export const Property: React.FunctionComponent = ({ node, path, onGoToRef }) => { +function shouldShowPropertyName(treeNode: SchemaTreeListNode) { + if (treeNode.parent === null) return false; + try { + return getPrimaryType(getNodeMetadata(treeNode.parent).schema) === SchemaKind.Object; + } catch { + return false; + } +} + +function isExternalRefSchemaNode(schemaNode: SchemaNode) { + return '$ref' in schemaNode && !isLocalRef(schemaNode.$ref); +} + +export const Property: React.FunctionComponent = ({ node: treeNode, onGoToRef }) => { + const { path, schemaNode: node } = getNodeMetadata(treeNode); const type = isRefNode(node) ? '$ref' : isCombinerNode(node) ? node.combiner : node.type; const subtype = isArrayNodeWithItems(node) ? inferType(node.items) : void 0; @@ -50,15 +65,13 @@ export const Property: React.FunctionComponent = ({ node, path, onGoT return ( <> - {path.length > 1 && (path[path.length - 2] === 'properties' || path[path.length - 2] === 'patternProperties') && ( -
{path[path.length - 1]}
- )} + {path.length > 0 && shouldShowPropertyName(treeNode) &&
{path[path.length - 1]}
} {'$ref' in node ? `[${node.$ref}]` : null} - {'$ref' in node && !onGoToRef && !isLocalRef(node.$ref) ? ( + {onGoToRef && isExternalRefSchemaNode(node) ? ( (go to ref) diff --git a/src/tree/tree.ts b/src/tree/tree.ts index 00e2f7e7..922d0651 100644 --- a/src/tree/tree.ts +++ b/src/tree/tree.ts @@ -2,8 +2,7 @@ import { isLocalRef, pointerToPath } from '@stoplight/json'; import { Tree, TreeListParentNode, TreeState } from '@stoplight/tree-list'; import { JsonPath } from '@stoplight/types'; import { JSONSchema4 } from 'json-schema'; -import { get as _get } from 'lodash'; -import { SchemaNode } from '../types'; +import { get as _get, isEqual as _isEqual } from 'lodash'; import { isRefNode } from '../utils/guards'; import { getNodeMetadata, metadataStore } from './metadata'; import { populateTree } from './populateTree'; @@ -13,7 +12,7 @@ export type SchemaTreeOptions = { mergeAllOf: boolean; }; -export { TreeState as SchemaTreeState } +export { TreeState as SchemaTreeState }; export class SchemaTree extends Tree { public expandedDepth: number; @@ -32,7 +31,7 @@ export class SchemaTree extends Tree { const expanded = {}; populateTree(this.schema, this.root, 0, [], { mergeAllOf: this.mergeAllOf, - onNode: (node: SchemaNode, parentTreeNode, level: number): boolean => { + onNode: (node, parentTreeNode, level): boolean => { if (isRefNode(node) && isLocalRef(node.$ref)) { expanded[node.id] = false; } @@ -52,9 +51,22 @@ export class SchemaTree extends Tree { const artificialRoot = Tree.createArtificialRoot(); populateTree(schema, artificialRoot, initialLevel, path, { mergeAllOf: this.mergeAllOf, - onNode: (node: SchemaNode, parentTreeNode, level: number) => level <= initialLevel + 1, + onNode: (node, parentTreeNode, level) => level <= this.expandedDepth + 1 || level <= initialLevel + 1, }); - this.insertTreeFragment((artificialRoot.children[0] as TreeListParentNode).children, parent); + + if (artificialRoot.children.length === 0) { + throw new Error(`Could not expand node ${path.join('.')}`); + } + + // todo: improve walk, i.e. add stepIn so that this is not required + if ( + 'children' in artificialRoot.children[0] && + _isEqual(getNodeMetadata(parent).path, getNodeMetadata(artificialRoot.children[0]).path) + ) { + this.insertTreeFragment(artificialRoot.children[0].children, parent); + } else { + this.insertTreeFragment(artificialRoot.children, parent); + } } public unwrap(node: TreeListParentNode) { @@ -62,10 +74,13 @@ export class SchemaTree extends Tree { return super.unwrap(node); } - const { path, schemaNode, schema } = getNodeMetadata(node); + const metadata = getNodeMetadata(node); + const { path, schemaNode, schema } = metadata; if (isRefNode(schemaNode)) { const refPath = pointerToPath(schemaNode.$ref); - this.populateTreeFragment(node, _get(this.schema, refPath), refPath); // DO NOTE THAT NODES PLACED UNDER THE REF MAY NOT HAVE CORRECT PATHS + const schemaFragment = _get(this.schema, refPath); + this.populateTreeFragment(node, schemaFragment, path); + metadata.schema = schemaFragment; } else { this.populateTreeFragment(node, schema, path); }