Skip to content

Commit

Permalink
fix: proper $refs unrolling
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Feb 24, 2020
1 parent 4499ba4 commit ebcad8b
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 61 deletions.
2 changes: 1 addition & 1 deletion src/components/SchemaRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export const SchemaRow: React.FunctionComponent<ISchemaRow> = ({ className, node
node.parent.children[0] !== node && <Divider kind={parentSchemaNode.combiner} />}

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

Expand Down
124 changes: 81 additions & 43 deletions src/components/__tests__/Property.spec.tsx
Original file line number Diff line number Diff line change
@@ -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(<Property node={node} path={[]} />);
metadataStore.set(treeNode, {
schemaNode: walk(schema).next().value,
path: [],
schema,
});

const wrapper = shallow(<Property node={treeNode} />);
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(<Property node={node} path={[]} />);
metadataStore.set(treeNode, {
schemaNode: walk(schema).next().value,
path: [],
schema,
});

const wrapper = shallow(<Property node={treeNode} />);
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(<Property node={node} path={[]} />);
const wrapper = shallow(<Property node={treeNode} />);
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(<Property node={node} path={[]} />);
const wrapper = shallow(<Property node={treeNode} />);
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(<Property node={node} path={[]} />);
const wrapper = shallow(<Property node={treeNode} />);
expect(wrapper.findWhere(el => /^\{\d\}$/.test(el.text())).first()).toHaveText('{0}');
});
});
Expand Down
31 changes: 22 additions & 9 deletions src/components/shared/Property.tsx
Original file line number Diff line number Diff line change
@@ -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;
}

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

export const Property: React.FunctionComponent<IProperty> = ({ 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<IProperty> = ({ 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;

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

return (
<>
{path.length > 1 && (path[path.length - 2] === 'properties' || path[path.length - 2] === 'patternProperties') && (
<div className="mr-2">{path[path.length - 1]}</div>
)}
{path.length > 0 && shouldShowPropertyName(treeNode) && <div className="mr-2">{path[path.length - 1]}</div>}

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

{'$ref' in node && !onGoToRef && !isLocalRef(node.$ref) ? (
{onGoToRef && isExternalRefSchemaNode(node) ? (
<a role="button" className="text-blue-4 ml-2" onClick={handleGoToRef}>
(go to ref)
</a>
Expand Down
31 changes: 23 additions & 8 deletions src/tree/tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
Expand All @@ -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;
}
Expand All @@ -52,20 +51,36 @@ 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) {
if (node.children.length !== 0 || this.visited.has(node)) {
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);
}
Expand Down

0 comments on commit ebcad8b

Please sign in to comment.