Skip to content

Commit 6edc287

Browse files
committed
feat: display expanding errors
1 parent f923926 commit 6edc287

File tree

5 files changed

+190
-65
lines changed

5 files changed

+190
-65
lines changed

src/components/SchemaRow.tsx

Lines changed: 57 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { IRowRendererOptions, Tree } from '@stoplight/tree-list';
22
import cn from 'classnames';
33
import * as React from 'react';
44

5-
import { getNodeMetadata, metadataStore } from '../tree/metadata';
5+
import { getNodeMetadata, getSchemaNodeMetadata } from '../tree/metadata';
66
import { GoToRefHandler, SchemaKind, SchemaTreeListNode } from '../types';
77
import { getPrimaryType } from '../utils/getPrimaryType';
88
import { Caret, Description, Divider, Property, Validations } from './shared';
@@ -21,12 +21,12 @@ const ROW_OFFSET = 7;
2121
function isRequired(treeNode: SchemaTreeListNode) {
2222
if (treeNode.parent === null) return false;
2323
try {
24-
const { path } = getNodeMetadata(treeNode);
24+
const { path } = getSchemaNodeMetadata(treeNode);
2525
if (path.length === 0) {
2626
return false;
2727
}
2828

29-
const { schema } = getNodeMetadata(treeNode.parent);
29+
const { schema } = getSchemaNodeMetadata(treeNode.parent);
3030

3131
return (
3232
getPrimaryType(schema) === SchemaKind.Object &&
@@ -38,13 +38,60 @@ function isRequired(treeNode: SchemaTreeListNode) {
3838
}
3939
}
4040

41-
export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, node, rowOptions, onGoToRef }) => {
42-
const metadata = getNodeMetadata(node);
41+
export const SchemaPropertyRow: typeof SchemaRow = ({ node, onGoToRef, rowOptions }) => {
42+
const metadata = getSchemaNodeMetadata(node);
4343
const { schemaNode } = metadata;
4444

45-
const parentSchemaNode = (node.parent !== null && metadataStore.get(node.parent)?.schemaNode) || null;
45+
const parentSchemaNode =
46+
(node.parent !== null && Tree.getLevel(node.parent) >= 0 && getSchemaNodeMetadata(node.parent)?.schemaNode) || null;
4647
const description = 'annotations' in schemaNode ? schemaNode.annotations.description : null;
4748

49+
return (
50+
<>
51+
{'children' in node && Tree.getLevel(node) > 0 && (
52+
<Caret
53+
isExpanded={!!rowOptions.isExpanded}
54+
style={{
55+
left: ICON_DIMENSION * -1 + ROW_OFFSET / -2,
56+
width: ICON_DIMENSION,
57+
height: ICON_DIMENSION,
58+
}}
59+
size={ICON_SIZE}
60+
/>
61+
)}
62+
63+
{node.parent !== null &&
64+
node.parent.children.length > 0 &&
65+
parentSchemaNode !== null &&
66+
'combiner' in parentSchemaNode &&
67+
node.parent.children[0] !== node && <Divider kind={parentSchemaNode.combiner} />}
68+
69+
<div className="flex-1 flex truncate">
70+
<Property node={node} onGoToRef={onGoToRef} />
71+
{description && <Description value={description} />}
72+
</div>
73+
74+
<Validations
75+
required={isRequired(node)}
76+
validations={{
77+
...('annotations' in schemaNode &&
78+
schemaNode.annotations.default && { default: schemaNode.annotations.default }),
79+
...('validations' in schemaNode && schemaNode.validations),
80+
}}
81+
/>
82+
</>
83+
);
84+
};
85+
SchemaPropertyRow.displayName = 'JsonSchemaViewer.SchemaPropertyRow';
86+
87+
export const SchemaErrorRow: React.FunctionComponent<{ message: string }> = ({ message }) => (
88+
<span className="text-red-5 dark:text-red-4">{message}</span>
89+
);
90+
SchemaErrorRow.displayName = 'JsonSchemaViewer.SchemaErrorRow';
91+
92+
export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, node, rowOptions, onGoToRef }) => {
93+
const metadata = getNodeMetadata(node);
94+
4895
return (
4996
<div className={cn('px-2 flex-1 w-full', className)}>
5097
<div
@@ -53,37 +100,11 @@ export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, node
53100
marginLeft: ICON_DIMENSION * Tree.getLevel(node), // offset for spacing
54101
}}
55102
>
56-
{'children' in node && Tree.getLevel(node) > 0 && (
57-
<Caret
58-
isExpanded={!!rowOptions.isExpanded}
59-
style={{
60-
left: ICON_DIMENSION * -1 + ROW_OFFSET / -2,
61-
width: ICON_DIMENSION,
62-
height: ICON_DIMENSION,
63-
}}
64-
size={ICON_SIZE}
65-
/>
103+
{'schema' in metadata ? (
104+
<SchemaPropertyRow node={node} onGoToRef={onGoToRef} rowOptions={rowOptions} />
105+
) : (
106+
<SchemaErrorRow message={metadata.error} />
66107
)}
67-
68-
{node.parent !== null &&
69-
node.parent.children.length > 0 &&
70-
parentSchemaNode !== null &&
71-
'combiner' in parentSchemaNode &&
72-
node.parent.children[0] !== node && <Divider kind={parentSchemaNode.combiner} />}
73-
74-
<div className="flex-1 flex truncate">
75-
<Property node={node} onGoToRef={onGoToRef} />
76-
{description && <Description value={description} />}
77-
</div>
78-
79-
<Validations
80-
required={isRequired(node)}
81-
validations={{
82-
...('annotations' in schemaNode &&
83-
schemaNode.annotations.default && { default: schemaNode.annotations.default }),
84-
...('validations' in schemaNode && schemaNode.validations),
85-
}}
86-
/>
87108
</div>
88109
</div>
89110
);

src/components/__tests__/SchemaRow.spec.tsx

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import * as React from 'react';
77
import { SchemaTree } from '../../tree';
88
import { metadataStore } from '../../tree/metadata';
99
import { SchemaKind, SchemaTreeListNode } from '../../types';
10-
import { SchemaRow } from '../SchemaRow';
10+
import { SchemaErrorRow, SchemaPropertyRow, SchemaRow } from '../SchemaRow';
1111
import { Validations } from '../shared/Validations';
1212

1313
describe('SchemaRow component', () => {
@@ -40,6 +40,8 @@ describe('SchemaRow component', () => {
4040

4141
const wrapper = shallow(
4242
shallow(<SchemaRow node={node} rowOptions={rowOptions} />)
43+
.find(SchemaPropertyRow)
44+
.shallow()
4345
.find(Validations)
4446
.shallow()
4547
.find(Popover)
@@ -49,6 +51,52 @@ describe('SchemaRow component', () => {
4951
expect(wrapper).toHaveText('enum:null,0,false');
5052
});
5153

54+
describe('expanding errors', () => {
55+
describe('$refs', () => {
56+
let tree: SchemaTree;
57+
58+
beforeEach(() => {
59+
const schema: JSONSchema4 = {
60+
type: 'object',
61+
properties: {
62+
user: {
63+
$ref: '#/properties/foo',
64+
},
65+
},
66+
};
67+
68+
tree = new SchemaTree(schema, new TreeState(), {
69+
expandedDepth: Infinity,
70+
mergeAllOf: false,
71+
resolveRef: void 0,
72+
});
73+
74+
tree.populate();
75+
});
76+
77+
test('given no custom resolver, should render a generic error message', () => {
78+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
79+
const wrapper = shallow(<SchemaRow node={tree.itemAt(2)!} rowOptions={{}} />)
80+
.find(SchemaErrorRow)
81+
.shallow();
82+
expect(wrapper).toHaveText(`Could not dereference "#/properties/foo"`);
83+
});
84+
85+
test('given a custom resolver, should render a message thrown by it', () => {
86+
const message = "I don't know how to resolve it. Sorry";
87+
tree.resolveRef = () => {
88+
throw new Error(message);
89+
};
90+
91+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
92+
const wrapper = shallow(<SchemaRow node={tree.itemAt(2)!} rowOptions={{}} />)
93+
.find(SchemaErrorRow)
94+
.shallow();
95+
expect(wrapper).toHaveText(message);
96+
});
97+
});
98+
});
99+
52100
describe('required property', () => {
53101
let tree: SchemaTree;
54102

@@ -85,24 +133,34 @@ describe('SchemaRow component', () => {
85133
});
86134

87135
test('should preserve the required validation', () => {
88-
const wrapper = shallow(<SchemaRow node={tree.itemAt(6)!} rowOptions={{}} />);
136+
const wrapper = shallow(<SchemaRow node={tree.itemAt(6)!} rowOptions={{}} />)
137+
.find(SchemaPropertyRow)
138+
.shallow();
89139
expect(wrapper.find(Validations)).toHaveProp('required', true);
90140
});
91141

92142
test('should preserve the optional validation', () => {
93-
const wrapper = shallow(<SchemaRow node={tree.itemAt(7)!} rowOptions={{}} />);
143+
const wrapper = shallow(<SchemaRow node={tree.itemAt(7)!} rowOptions={{}} />)
144+
.find(SchemaPropertyRow)
145+
.shallow();
146+
94147
expect(wrapper.find(Validations)).toHaveProp('required', false);
95148
});
96149

97150
describe('given a referenced object', () => {
98151
test('should preserve the required validation', () => {
99-
const wrapper = shallow(<SchemaRow node={tree.itemAt(3)!} rowOptions={{}} />);
152+
const wrapper = shallow(<SchemaRow node={tree.itemAt(3)!} rowOptions={{}} />)
153+
.find(SchemaPropertyRow)
154+
.shallow();
100155

101156
expect(wrapper.find(Validations)).toHaveProp('required', true);
102157
});
103158

104159
test('should preserve the optional validation', () => {
105-
const wrapper = shallow(<SchemaRow node={tree.itemAt(4)!} rowOptions={{}} />);
160+
const wrapper = shallow(<SchemaRow node={tree.itemAt(4)!} rowOptions={{}} />)
161+
.find(SchemaPropertyRow)
162+
.shallow();
163+
106164
expect(wrapper.find(Validations)).toHaveProp('required', false);
107165
});
108166
});

src/components/shared/Property.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ 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 { getNodeMetadata } from '../../tree';
6+
import { getSchemaNodeMetadata } from '../../tree/metadata';
77
import { GoToRefHandler, IArrayNode, IObjectNode, SchemaKind, SchemaNode, SchemaTreeListNode } from '../../types';
88
import { getPrimaryType } from '../../utils/getPrimaryType';
99
import { isArrayNodeWithItems, isCombinerNode, isRefNode } from '../../utils/guards';
@@ -26,7 +26,7 @@ function count(obj: Optional<JSONSchema4 | null>): number | null {
2626
function shouldShowPropertyName(treeNode: SchemaTreeListNode) {
2727
if (treeNode.parent === null) return false;
2828
try {
29-
const { schema } = getNodeMetadata(treeNode.parent);
29+
const { schema } = getSchemaNodeMetadata(treeNode.parent);
3030
let type = getPrimaryType(schema);
3131

3232
if (type === SchemaKind.Array && schema.items) {
@@ -44,7 +44,7 @@ function isExternalRefSchemaNode(schemaNode: SchemaNode) {
4444
}
4545

4646
export const Property: React.FunctionComponent<IProperty> = ({ node: treeNode, onGoToRef }) => {
47-
const { path, schemaNode: node } = getNodeMetadata(treeNode);
47+
const { path, schemaNode: node } = getSchemaNodeMetadata(treeNode);
4848
const type = isRefNode(node) ? '$ref' : isCombinerNode(node) ? node.combiner : node.type;
4949
const subtype = isArrayNodeWithItems(node) ? inferType(node.items) : void 0;
5050

src/tree/metadata.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,36 @@ import { JsonPath } from '@stoplight/types';
33
import { JSONSchema4 } from 'json-schema';
44
import { SchemaNode, SchemaTreeListNode } from '../types';
55

6-
export interface ITreeNodeMeta {
6+
export interface ITreeNodeMetaSchema {
77
path: JsonPath;
88
schemaNode: SchemaNode;
99
schema: JSONSchema4;
1010
}
1111

12-
export const metadataStore = new WeakMap<SchemaTreeListNode, ITreeNodeMeta>();
12+
export interface ITreeNodeMetaError {
13+
path: JsonPath;
14+
error: string;
15+
}
16+
17+
export type TreeNodeMeta = ITreeNodeMetaSchema | ITreeNodeMetaError;
1318

14-
export const getNodeMetadata = (node: TreeListNode): ITreeNodeMeta => {
19+
export const metadataStore = new WeakMap<SchemaTreeListNode, TreeNodeMeta>();
20+
21+
export const getNodeMetadata = (node: TreeListNode): TreeNodeMeta => {
1522
const metadata = metadataStore.get(node);
1623
if (metadata === void 0) {
1724
throw new Error('Missing metadata');
1825
}
1926

2027
return metadata;
2128
};
29+
30+
export const getSchemaNodeMetadata = (node: TreeListNode): ITreeNodeMetaSchema => {
31+
const metadata = getNodeMetadata(node);
32+
33+
if (!('schema' in metadata)) {
34+
throw new TypeError('Schema node expected');
35+
}
36+
37+
return metadata;
38+
};

0 commit comments

Comments
 (0)