Skip to content

Commit ebcad8b

Browse files
committed
fix: proper $refs unrolling
1 parent 4499ba4 commit ebcad8b

File tree

4 files changed

+127
-61
lines changed

4 files changed

+127
-61
lines changed

src/components/SchemaRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, node
5151
node.parent.children[0] !== node && <Divider kind={parentSchemaNode.combiner} />}
5252

5353
<div className="flex-1 flex truncate">
54-
<Property node={schemaNode} path={metadata.path} onGoToRef={onGoToRef} />
54+
<Property node={node} onGoToRef={onGoToRef} />
5555
{description && <Description value={description} />}
5656
</div>
5757

src/components/__tests__/Property.spec.tsx

Lines changed: 81 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,86 +1,124 @@
11
import { shallow } from 'enzyme';
22
import 'jest-enzyme';
3+
import { JSONSchema4 } from 'json-schema';
34
import * as React from 'react';
4-
import { SchemaNode } from '../../types';
5+
import { metadataStore } from '../../tree/metadata';
6+
import { walk } from '../../tree/walk';
7+
import { SchemaTreeListNode } from '../../types';
58
import { Property, Types } from '../shared';
69

710
describe('Property component', () => {
811
it('should render Types with proper type and subtype', () => {
9-
const node: SchemaNode = {
10-
id: '2',
12+
const treeNode: SchemaTreeListNode = {
13+
id: 'foo',
14+
name: '',
15+
parent: null,
16+
};
17+
18+
const schema: JSONSchema4 = {
1119
type: 'array',
1220
items: {
1321
type: 'string',
1422
},
15-
annotations: {
16-
examples: {},
17-
},
18-
validations: {},
1923
};
2024

21-
const wrapper = shallow(<Property node={node} path={[]} />);
25+
metadataStore.set(treeNode, {
26+
schemaNode: walk(schema).next().value,
27+
path: [],
28+
schema,
29+
});
30+
31+
const wrapper = shallow(<Property node={treeNode} />);
2232
expect(wrapper.find(Types)).toExist();
2333
expect(wrapper.find(Types)).toHaveProp('type', 'array');
2434
expect(wrapper.find(Types)).toHaveProp('subtype', 'string');
2535
});
2636

2737
it('should handle nullish items', () => {
28-
const node = {
29-
id: '1',
38+
const treeNode: SchemaTreeListNode = {
39+
id: 'foo',
40+
name: '',
41+
parent: null,
42+
};
43+
44+
const schema: JSONSchema4 = {
3045
type: 'array',
31-
items: null,
32-
annotations: {
33-
examples: {},
34-
},
35-
validations: {},
36-
} as SchemaNode;
46+
items: null as any,
47+
};
3748

38-
const wrapper = shallow(<Property node={node} path={[]} />);
49+
metadataStore.set(treeNode, {
50+
schemaNode: walk(schema).next().value,
51+
path: [],
52+
schema,
53+
});
54+
55+
const wrapper = shallow(<Property node={treeNode} />);
3956
expect(wrapper).not.toBeEmptyRender();
4057
});
4158

4259
describe('properties counter', () => {
4360
it('given missing properties property, should not display the counter', () => {
44-
const node = {
45-
id: '1',
61+
const treeNode: SchemaTreeListNode = {
62+
id: 'foo',
63+
name: '',
64+
parent: null,
65+
};
66+
67+
const schema: JSONSchema4 = {
4668
type: 'object',
47-
annotations: {
48-
examples: {},
49-
},
50-
validations: {},
51-
} as SchemaNode;
69+
};
70+
71+
metadataStore.set(treeNode, {
72+
schemaNode: walk(schema).next().value,
73+
path: [],
74+
schema,
75+
});
5276

53-
const wrapper = shallow(<Property node={node} path={[]} />);
77+
const wrapper = shallow(<Property node={treeNode} />);
5478
expect(wrapper.findWhere(el => /^\{\d\}$/.test(el.text()))).not.toExist();
5579
});
5680

5781
it('given nullish properties property, should not display the counter', () => {
58-
const node = {
59-
id: '1',
60-
properties: null,
82+
const treeNode: SchemaTreeListNode = {
83+
id: 'foo',
84+
name: '',
85+
parent: null,
86+
};
87+
88+
const schema: JSONSchema4 = {
6189
type: 'object',
62-
annotations: {
63-
examples: {},
64-
},
65-
validations: {},
66-
} as SchemaNode;
90+
properties: null as any,
91+
};
92+
93+
metadataStore.set(treeNode, {
94+
schemaNode: walk(schema).next().value,
95+
path: [],
96+
schema,
97+
});
6798

68-
const wrapper = shallow(<Property node={node} path={[]} />);
99+
const wrapper = shallow(<Property node={treeNode} />);
69100
expect(wrapper.findWhere(el => /^\{\d\}$/.test(el.text()))).not.toExist();
70101
});
71102

72103
it('given object properties property, should display the counter', () => {
73-
const node = {
74-
id: '1',
75-
properties: {},
104+
const treeNode: SchemaTreeListNode = {
105+
id: 'foo',
106+
name: '',
107+
parent: null,
108+
};
109+
110+
const schema: JSONSchema4 = {
76111
type: 'object',
77-
annotations: {
78-
examples: {},
79-
},
80-
validations: {},
81-
} as SchemaNode;
112+
properties: {},
113+
};
114+
115+
metadataStore.set(treeNode, {
116+
schemaNode: walk(schema).next().value,
117+
path: [],
118+
schema,
119+
});
82120

83-
const wrapper = shallow(<Property node={node} path={[]} />);
121+
const wrapper = shallow(<Property node={treeNode} />);
84122
expect(wrapper.findWhere(el => /^\{\d\}$/.test(el.text())).first()).toHaveText('{0}');
85123
});
86124
});

src/components/shared/Property.tsx

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,17 @@
11
import { isLocalRef } from '@stoplight/json';
2-
import { JsonPath, Optional } from '@stoplight/types';
2+
import { Optional } from '@stoplight/types';
33
import { JSONSchema4 } from 'json-schema';
44
import { isObject as _isObject, size as _size } from 'lodash';
55
import * as React from 'react';
6-
import { GoToRefHandler, IArrayNode, IObjectNode, SchemaKind, SchemaNode } from '../../types';
6+
import { getNodeMetadata } from '../../tree';
7+
import { GoToRefHandler, IArrayNode, IObjectNode, SchemaKind, SchemaNode, SchemaTreeListNode } from '../../types';
8+
import { getPrimaryType } from '../../utils/getPrimaryType';
79
import { isArrayNodeWithItems, isCombinerNode, isRefNode } from '../../utils/guards';
810
import { inferType } from '../../utils/inferType';
911
import { Types } from './Types';
1012

1113
export interface IProperty {
12-
node: SchemaNode;
13-
path: JsonPath;
14+
node: SchemaTreeListNode;
1415
onGoToRef?: GoToRefHandler;
1516
}
1617

@@ -22,7 +23,21 @@ function count(obj: Optional<JSONSchema4 | null>): number | null {
2223
return null;
2324
}
2425

25-
export const Property: React.FunctionComponent<IProperty> = ({ node, path, onGoToRef }) => {
26+
function shouldShowPropertyName(treeNode: SchemaTreeListNode) {
27+
if (treeNode.parent === null) return false;
28+
try {
29+
return getPrimaryType(getNodeMetadata(treeNode.parent).schema) === SchemaKind.Object;
30+
} catch {
31+
return false;
32+
}
33+
}
34+
35+
function isExternalRefSchemaNode(schemaNode: SchemaNode) {
36+
return '$ref' in schemaNode && !isLocalRef(schemaNode.$ref);
37+
}
38+
39+
export const Property: React.FunctionComponent<IProperty> = ({ node: treeNode, onGoToRef }) => {
40+
const { path, schemaNode: node } = getNodeMetadata(treeNode);
2641
const type = isRefNode(node) ? '$ref' : isCombinerNode(node) ? node.combiner : node.type;
2742
const subtype = isArrayNodeWithItems(node) ? inferType(node.items) : void 0;
2843

@@ -50,15 +65,13 @@ export const Property: React.FunctionComponent<IProperty> = ({ node, path, onGoT
5065

5166
return (
5267
<>
53-
{path.length > 1 && (path[path.length - 2] === 'properties' || path[path.length - 2] === 'patternProperties') && (
54-
<div className="mr-2">{path[path.length - 1]}</div>
55-
)}
68+
{path.length > 0 && shouldShowPropertyName(treeNode) && <div className="mr-2">{path[path.length - 1]}</div>}
5669

5770
<Types type={type} subtype={subtype}>
5871
{'$ref' in node ? `[${node.$ref}]` : null}
5972
</Types>
6073

61-
{'$ref' in node && !onGoToRef && !isLocalRef(node.$ref) ? (
74+
{onGoToRef && isExternalRefSchemaNode(node) ? (
6275
<a role="button" className="text-blue-4 ml-2" onClick={handleGoToRef}>
6376
(go to ref)
6477
</a>

src/tree/tree.ts

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@ import { isLocalRef, pointerToPath } from '@stoplight/json';
22
import { Tree, TreeListParentNode, TreeState } from '@stoplight/tree-list';
33
import { JsonPath } from '@stoplight/types';
44
import { JSONSchema4 } from 'json-schema';
5-
import { get as _get } from 'lodash';
6-
import { SchemaNode } from '../types';
5+
import { get as _get, isEqual as _isEqual } from 'lodash';
76
import { isRefNode } from '../utils/guards';
87
import { getNodeMetadata, metadataStore } from './metadata';
98
import { populateTree } from './populateTree';
@@ -13,7 +12,7 @@ export type SchemaTreeOptions = {
1312
mergeAllOf: boolean;
1413
};
1514

16-
export { TreeState as SchemaTreeState }
15+
export { TreeState as SchemaTreeState };
1716

1817
export class SchemaTree extends Tree {
1918
public expandedDepth: number;
@@ -32,7 +31,7 @@ export class SchemaTree extends Tree {
3231
const expanded = {};
3332
populateTree(this.schema, this.root, 0, [], {
3433
mergeAllOf: this.mergeAllOf,
35-
onNode: (node: SchemaNode, parentTreeNode, level: number): boolean => {
34+
onNode: (node, parentTreeNode, level): boolean => {
3635
if (isRefNode(node) && isLocalRef(node.$ref)) {
3736
expanded[node.id] = false;
3837
}
@@ -52,20 +51,36 @@ export class SchemaTree extends Tree {
5251
const artificialRoot = Tree.createArtificialRoot();
5352
populateTree(schema, artificialRoot, initialLevel, path, {
5453
mergeAllOf: this.mergeAllOf,
55-
onNode: (node: SchemaNode, parentTreeNode, level: number) => level <= initialLevel + 1,
54+
onNode: (node, parentTreeNode, level) => level <= this.expandedDepth + 1 || level <= initialLevel + 1,
5655
});
57-
this.insertTreeFragment((artificialRoot.children[0] as TreeListParentNode).children, parent);
56+
57+
if (artificialRoot.children.length === 0) {
58+
throw new Error(`Could not expand node ${path.join('.')}`);
59+
}
60+
61+
// todo: improve walk, i.e. add stepIn so that this is not required
62+
if (
63+
'children' in artificialRoot.children[0] &&
64+
_isEqual(getNodeMetadata(parent).path, getNodeMetadata(artificialRoot.children[0]).path)
65+
) {
66+
this.insertTreeFragment(artificialRoot.children[0].children, parent);
67+
} else {
68+
this.insertTreeFragment(artificialRoot.children, parent);
69+
}
5870
}
5971

6072
public unwrap(node: TreeListParentNode) {
6173
if (node.children.length !== 0 || this.visited.has(node)) {
6274
return super.unwrap(node);
6375
}
6476

65-
const { path, schemaNode, schema } = getNodeMetadata(node);
77+
const metadata = getNodeMetadata(node);
78+
const { path, schemaNode, schema } = metadata;
6679
if (isRefNode(schemaNode)) {
6780
const refPath = pointerToPath(schemaNode.$ref);
68-
this.populateTreeFragment(node, _get(this.schema, refPath), refPath); // DO NOTE THAT NODES PLACED UNDER THE REF MAY NOT HAVE CORRECT PATHS
81+
const schemaFragment = _get(this.schema, refPath);
82+
this.populateTreeFragment(node, schemaFragment, path);
83+
metadata.schema = schemaFragment;
6984
} else {
7085
this.populateTreeFragment(node, schema, path);
7186
}

0 commit comments

Comments
 (0)