Skip to content

Commit d191c63

Browse files
Jakub JankowskiP0lip
authored andcommitted
fix: array local refs
* fix: array local refs * chore: import cleanup * fix: ref node caret * fix: null check * chore: cleanup * test: tree and populateTree tests
1 parent 829e7ed commit d191c63

File tree

8 files changed

+120
-22
lines changed

8 files changed

+120
-22
lines changed

src/components/SchemaRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export const SchemaPropertyRow: typeof SchemaRow = ({ node, onGoToRef, rowOption
5555
style={{
5656
width: ICON_DIMENSION,
5757
height: ICON_DIMENSION,
58-
...(isRefNode(schemaNode)
58+
...(isRefNode(schemaNode) && Tree.getLevel(node) === 0
5959
? {
6060
position: 'relative',
6161
}

src/components/shared/Property.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as React from 'react';
66
import { getSchemaNodeMetadata } from '../../tree/metadata';
77
import { GoToRefHandler, IArrayNode, IObjectNode, SchemaKind, SchemaNode, SchemaTreeListNode } from '../../types';
88
import { getPrimaryType } from '../../utils/getPrimaryType';
9-
import { isArrayNodeWithItems, isCombinerNode, isRefNode } from '../../utils/guards';
9+
import { hasRefItems, isArrayNodeWithItems, isCombinerNode, isRefNode } from '../../utils/guards';
1010
import { inferType } from '../../utils/inferType';
1111
import { Types } from './Types';
1212

@@ -46,7 +46,7 @@ function isExternalRefSchemaNode(schemaNode: SchemaNode) {
4646
export const Property: React.FunctionComponent<IProperty> = ({ node: treeNode, onGoToRef }) => {
4747
const { path, schemaNode: node } = getSchemaNodeMetadata(treeNode);
4848
const type = isRefNode(node) ? '$ref' : isCombinerNode(node) ? node.combiner : node.type;
49-
const subtype = isArrayNodeWithItems(node) ? inferType(node.items) : void 0;
49+
const subtype = isArrayNodeWithItems(node) ? (hasRefItems(node) ? '$ref' : inferType(node.items)) : void 0;
5050

5151
const childrenCount = React.useMemo<number | null>(() => {
5252
if (type === SchemaKind.Object) {
@@ -76,6 +76,7 @@ export const Property: React.FunctionComponent<IProperty> = ({ node: treeNode, o
7676

7777
<Types type={type} subtype={subtype}>
7878
{isRefNode(node) && node.$ref !== null ? `[${node.$ref}]` : null}
79+
{hasRefItems(node) && node.items.$ref !== null ? `[$ref(${node.items.$ref})]` : null}
7980
</Types>
8081

8182
{onGoToRef && isExternalRefSchemaNode(node) ? (

src/components/shared/Types.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ import { JSONSchema4CombinerName } from '../../types';
1010
*/
1111
export interface IType {
1212
type: JSONSchema4TypeName | JSONSchema4CombinerName | 'binary' | '$ref';
13-
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[]>;
13+
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[]> | '$ref';
1414
className?: string;
1515
}
1616

1717
export const Type: React.FunctionComponent<IType> = ({ className, children, type, subtype }) => {
1818
return (
1919
<span className={cn(className, PropertyTypeColors[type], 'truncate')}>
20-
{type === 'array' && subtype && subtype !== 'array' ? `array[${subtype}]` : type}
20+
{type === 'array' && subtype && subtype !== 'array' && subtype !== '$ref' ? `array[${subtype}]` : type}
2121

2222
{children}
2323
</span>
@@ -31,7 +31,7 @@ Type.displayName = 'JsonSchemaViewer.Type';
3131
interface ITypes {
3232
className?: string;
3333
type: Optional<JSONSchema4TypeName | JSONSchema4TypeName[] | JSONSchema4CombinerName | '$ref'>;
34-
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[]>;
34+
subtype: Optional<JSONSchema4TypeName | JSONSchema4TypeName[] | '$ref'>;
3535
}
3636

3737
export const Types: React.FunctionComponent<ITypes> = ({ className, type, subtype, children }) => {

src/tree/__tests__/populateTree.spec.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,4 +86,31 @@ describe('populateTree util', () => {
8686
__ERROR__: 'dd',
8787
});
8888
});
89+
90+
it('processes array with refed items correctly', () => {
91+
const schema: JSONSchema4 = {
92+
type: 'object',
93+
properties: {
94+
user: {
95+
type: 'array',
96+
items: {
97+
$ref: '#/properties/id',
98+
},
99+
},
100+
id: {
101+
type: 'object',
102+
required: ['foo'],
103+
properties: {
104+
foo: {
105+
type: 'string',
106+
},
107+
},
108+
},
109+
},
110+
};
111+
112+
const root = Tree.createArtificialRoot();
113+
populateTree(schema, root, 0, [], null);
114+
expect(((root.children[0] as TreeListParentNode).children[0] as TreeListParentNode).children).toHaveLength(0);
115+
});
89116
});

src/tree/__tests__/tree.spec.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,5 +106,61 @@ describe('SchemaTree', () => {
106106
);
107107
});
108108
});
109+
110+
describe('array with refed items', () => {
111+
let tree: SchemaTree;
112+
let schema: JSONSchema4;
113+
114+
beforeEach(() => {
115+
schema = {
116+
type: 'object',
117+
properties: {
118+
user: {
119+
type: 'array',
120+
items: {
121+
$ref: '#/properties/id',
122+
},
123+
},
124+
id: {
125+
type: 'object',
126+
required: ['foo'],
127+
properties: {
128+
foo: {
129+
type: 'string',
130+
},
131+
},
132+
},
133+
},
134+
};
135+
136+
tree = new SchemaTree(schema, new SchemaTreeState(), {
137+
expandedDepth: 0,
138+
mergeAllOf: false,
139+
resolveRef: void 0,
140+
});
141+
142+
tree.populate();
143+
});
144+
145+
test('upon expanded array, should insert object', () => {
146+
expect(tree.count).toEqual(3);
147+
148+
tree.unwrap(tree.itemAt(1) as TreeListParentNode);
149+
150+
expect(tree.count).toEqual(4);
151+
expect(getNodeMetadata(tree.itemAt(2)!)).toHaveProperty(
152+
'schema',
153+
expect.objectContaining({
154+
type: 'object',
155+
required: ['foo'],
156+
properties: {
157+
foo: {
158+
type: 'string',
159+
},
160+
},
161+
}),
162+
);
163+
});
164+
});
109165
});
110166
});

src/tree/tree.ts

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { JSONSchema4 } from 'json-schema';
55
import { get as _get, isEqual as _isEqual, isObject as _isObject } from 'lodash';
66
import { SchemaTreeListNode } from '../types';
77
import { generateId } from '../utils/generateId';
8-
import { isRefNode } from '../utils/guards';
8+
import { hasRefItems, isRefNode } from '../utils/guards';
99
import { getSchemaNodeMetadata, metadataStore } from './metadata';
1010
import { canStepIn } from './utils/canStepIn';
1111
import { populateTree } from './utils/populateTree';
@@ -44,7 +44,10 @@ export class SchemaTree extends Tree {
4444
populateTree(this.schema, this.root, 0, [], {
4545
mergeAllOf: this.mergeAllOf,
4646
onNode: (fragment, node, parentTreeNode, level): boolean => {
47-
if (isRefNode(node) && node.$ref !== null && isLocalRef(node.$ref)) {
47+
if (
48+
(isRefNode(node) && node.$ref !== null && isLocalRef(node.$ref)) ||
49+
(hasRefItems(node) && node.items.$ref !== null && isLocalRef(node.items.$ref))
50+
) {
4851
expanded[node.id] = false;
4952
}
5053

@@ -109,22 +112,15 @@ export class SchemaTree extends Tree {
109112
if (node.children.length !== 0 || this.visited.has(node)) {
110113
return super.unwrap(node);
111114
}
112-
113115
const metadata = getSchemaNodeMetadata(node);
114116
const { path, schemaNode, schema } = metadata;
115117
try {
116-
if (!isRefNode(schemaNode)) {
118+
if (!isRefNode(schemaNode) && !hasRefItems(schemaNode)) {
117119
this.populateTreeFragment(node, schema, path, true);
118-
} else if (schemaNode.$ref !== null) {
119-
const refPath = pointerToPath(schemaNode.$ref);
120-
const schemaFragment = this.resolveRef
121-
? this.resolveRef(refPath, path, this.schema)
122-
: _get(this.schema, refPath);
123-
if (!_isObject(schemaFragment)) {
124-
throw new ReferenceError(`Could not dereference "${pathToPointer(refPath)}"`);
125-
}
126-
127-
this.populateTreeFragment(node, schemaFragment, path, false);
120+
} else if (isRefNode(schemaNode)) {
121+
this.populateRefFragment(node, path, schemaNode.$ref);
122+
} else if (hasRefItems(schemaNode)) {
123+
this.populateRefFragment(node, path, schemaNode.items.$ref);
128124
} else {
129125
throw new Error(`I do know not how not expand node ${path.join('.')}`);
130126
}
@@ -135,4 +131,17 @@ export class SchemaTree extends Tree {
135131
this.visited.add(node);
136132
return super.unwrap(node);
137133
}
134+
135+
protected populateRefFragment(node: TreeListParentNode, path: JsonPath, ref: string | null) {
136+
if (!ref) {
137+
throw new Error('Unknown $ref value');
138+
}
139+
const refPath = pointerToPath(ref);
140+
const schemaFragment = this.resolveRef ? this.resolveRef(refPath, path, this.schema) : _get(this.schema, refPath);
141+
if (!_isObject(schemaFragment)) {
142+
throw new ReferenceError(`Could not dereference "${pathToPointer(refPath)}"`);
143+
}
144+
145+
this.populateTreeFragment(node, schemaFragment, path, false);
146+
}
138147
}

src/tree/utils/populateTree.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { IArrayNode, IObjectNode, SchemaKind, SchemaNode, SchemaTreeListNode } f
77
import { mergeAllOf } from '../../utils';
88
import { getCombiner } from '../../utils/getCombiner';
99
import { getPrimaryType } from '../../utils/getPrimaryType';
10-
import { isCombinerNode, isRefNode } from '../../utils/guards';
10+
import { hasRefItems, isCombinerNode, isRefNode } from '../../utils/guards';
1111
import { metadataStore } from '../metadata';
1212
import { walk } from './walk';
1313

@@ -90,7 +90,9 @@ function processArray(
9090
path: JsonPath,
9191
options: WalkingOptions | null,
9292
): SchemaTreeListNode {
93-
if (Array.isArray(schema.items)) {
93+
if (hasRefItems(schema) && schema.items.$ref && isLocalRef(schema.items.$ref)) {
94+
(node as TreeListParentNode).children = [];
95+
} else if (Array.isArray(schema.items)) {
9496
const children: SchemaTreeListNode[] = [];
9597
(node as TreeListParentNode).children = children;
9698
for (const [i, property] of schema.items.entries()) {

src/utils/guards.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,7 @@ export const isArrayNodeWithItems = (
99

1010
export const isRefNode = (node: SchemaNode): node is IRefNode => '$ref' in node;
1111

12+
export const hasRefItems = (node: SchemaNode): node is Omit<IArrayNode, 'items'> & { items: Omit<IRefNode, 'id'> } =>
13+
'items' in node && !!node.items && '$ref' in node.items;
14+
1215
export const isCombinerNode = (node: SchemaNode): node is ICombinerNode => 'combiner' in node;

0 commit comments

Comments
 (0)