Skip to content

Commit b76157a

Browse files
authored
feat: support dictionaries (#249)
1 parent 1c11c12 commit b76157a

File tree

15 files changed

+408
-165
lines changed

15 files changed

+408
-165
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
},
4747
"dependencies": {
4848
"@stoplight/json": "^3.20.1",
49-
"@stoplight/json-schema-tree": "^3.0.0",
49+
"@stoplight/json-schema-tree": "^4.0.0",
5050
"@stoplight/react-error-boundary": "^2.0.0",
5151
"@types/json-schema": "^7.0.7",
5252
"classnames": "^2.2.6",

src/__fixtures__/formats-schema.json

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
33
"type": "object",
44
"properties": {
55
"date-of-birth": {
6-
"type": [
7-
"number",
8-
"string",
9-
"array"
10-
],
6+
"type": ["number", "string", "array"],
117
"format": "date-time",
128
"items": {}
139
},
@@ -23,20 +19,28 @@
2319
"format": "int32"
2420
},
2521
"size": {
26-
"type": [
27-
"number",
28-
"string"
29-
],
22+
"type": ["number", "string"],
3023
"format": "byte"
3124
},
3225
"notype": {
3326
"format": "date-time"
3427
},
28+
"array-of-integers": {
29+
"type": "array",
30+
"items": {
31+
"type": "integer",
32+
"format": "int32"
33+
}
34+
},
35+
"map-of-ids": {
36+
"type": "object",
37+
"additionalProperties": {
38+
"type": "integer",
39+
"format": "int32"
40+
}
41+
},
3542
"permissions": {
36-
"type": [
37-
"string",
38-
"object"
39-
],
43+
"type": ["string", "object"],
4044
"format": "password",
4145
"properties": {
4246
"ids": {

src/__tests__/index.spec.tsx

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import 'jest-enzyme';
22

33
import { mount, ReactWrapper } from 'enzyme';
4-
import { JSONSchema4 } from 'json-schema';
4+
import { JSONSchema4, JSONSchema7 } from 'json-schema';
55
import * as React from 'react';
66

77
import { JsonSchemaViewer } from '../components';
@@ -165,6 +165,121 @@ describe('HTML Output', () => {
165165
expect(dumpDom(<JsonSchemaViewer schema={schema} />)).toMatchSnapshot();
166166
});
167167

168+
it('given dictionary with defined properties, should not render them', () => {
169+
const schema: JSONSchema7 = {
170+
type: ['object', 'null'],
171+
properties: {
172+
id: {
173+
type: 'string',
174+
readOnly: true,
175+
},
176+
description: {
177+
type: 'string',
178+
writeOnly: true,
179+
},
180+
},
181+
additionalProperties: {
182+
type: 'string',
183+
},
184+
};
185+
186+
expect(dumpDom(<JsonSchemaViewer schema={schema} defaultExpandedDepth={Infinity} />)).toMatchInlineSnapshot(`
187+
"<div class=\\"\\" id=\\"mosaic-provider-react-aria-0-1\\">
188+
<div data-overlay-container=\\"true\\">
189+
<div class=\\"JsonSchemaViewer\\">
190+
<div></div>
191+
<div data-id=\\"bf8b96e78f11d\\" data-test=\\"schema-row\\">
192+
<div>
193+
<div>
194+
<div>
195+
<div>
196+
<span data-test=\\"property-type\\">dictionary[string, string]</span>
197+
<span>or</span>
198+
<span data-test=\\"property-type\\">null</span>
199+
</div>
200+
</div>
201+
</div>
202+
</div>
203+
</div>
204+
</div>
205+
</div>
206+
</div>
207+
"
208+
`);
209+
});
210+
211+
it('should not render true/false additionalProperties', () => {
212+
const schema: JSONSchema7 = {
213+
type: 'object',
214+
properties: {
215+
id: {
216+
type: 'string',
217+
},
218+
},
219+
additionalProperties: true,
220+
};
221+
222+
const additionalTrue = dumpDom(<JsonSchemaViewer schema={schema} defaultExpandedDepth={Infinity} />);
223+
const additionalFalse = dumpDom(
224+
<JsonSchemaViewer schema={{ ...schema, additionalProperties: false }} defaultExpandedDepth={Infinity} />,
225+
);
226+
expect(additionalTrue).toEqual(additionalFalse);
227+
expect(additionalTrue).toMatchInlineSnapshot(`
228+
"<div class=\\"\\" id=\\"mosaic-provider-react-aria-0-1\\">
229+
<div data-overlay-container=\\"true\\">
230+
<div class=\\"JsonSchemaViewer\\">
231+
<div></div>
232+
<div data-level=\\"0\\">
233+
<div data-id=\\"8074f410d9775\\" data-test=\\"schema-row\\">
234+
<div>
235+
<div>
236+
<div>
237+
<div data-test=\\"property-name-id\\">id</div>
238+
<span data-test=\\"property-type\\">string</span>
239+
</div>
240+
</div>
241+
</div>
242+
</div>
243+
</div>
244+
</div>
245+
</div>
246+
</div>
247+
"
248+
`);
249+
});
250+
251+
it('should not render additionalItems', () => {
252+
const schema: JSONSchema7 = {
253+
type: 'array',
254+
additionalItems: {
255+
type: 'object',
256+
properties: {
257+
id: {
258+
type: 'string',
259+
},
260+
},
261+
},
262+
};
263+
264+
expect(dumpDom(<JsonSchemaViewer schema={schema} defaultExpandedDepth={Infinity} />)).toMatchInlineSnapshot(`
265+
"<div class=\\"\\" id=\\"mosaic-provider-react-aria-0-1\\">
266+
<div data-overlay-container=\\"true\\">
267+
<div class=\\"JsonSchemaViewer\\">
268+
<div></div>
269+
<div data-id=\\"bf8b96e78f11d\\" data-test=\\"schema-row\\">
270+
<div>
271+
<div>
272+
<div><span data-test=\\"property-type\\">array</span></div>
273+
</div>
274+
</div>
275+
</div>
276+
</div>
277+
</div>
278+
</div>
279+
"
280+
`);
281+
});
282+
168283
describe('top level descriptions', () => {
169284
const schema: JSONSchema4 = {
170285
description: 'This is a description that should be rendered',

src/components/JsonSchemaViewer.tsx

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
isRegularNode,
33
RootNode,
4-
SchemaNode,
54
SchemaTree as JsonSchemaTree,
65
SchemaTreeRefDereferenceFn,
76
} from '@stoplight/json-schema-tree';
@@ -13,7 +12,8 @@ import { useUpdateAtom } from 'jotai/utils';
1312
import * as React from 'react';
1413

1514
import { JSVOptions, JSVOptionsContextProvider } from '../contexts';
16-
import type { JSONSchema } from '../types';
15+
import { shouldNodeBeIncluded } from '../tree/utils';
16+
import { JSONSchema } from '../types';
1717
import { PathCrumbs } from './PathCrumbs';
1818
import { TopLevelSchemaRow } from './SchemaRow';
1919
import { hoveredNodeAtom } from './SchemaRow/state';
@@ -114,20 +114,9 @@ const JsonSchemaViewerInner = ({
114114
});
115115

116116
let nodeCount = 0;
117-
const shouldNodeBeIncluded = (node: SchemaNode) => {
118-
if (!isRegularNode(node)) return true;
119-
120-
const { validations } = node;
121-
122-
if (!!validations.writeOnly === !!validations.readOnly) {
123-
return true;
124-
}
125-
126-
return !((viewMode === 'read' && !!validations.writeOnly) || (viewMode === 'write' && !!validations.readOnly));
127-
};
128117

129118
jsonSchemaTree.walker.hookInto('filter', node => {
130-
if (shouldNodeBeIncluded(node)) {
119+
if (shouldNodeBeIncluded(node, viewMode)) {
131120
nodeCount++;
132121
return true;
133122
}

src/components/shared/Format.tsx

Lines changed: 0 additions & 10 deletions
This file was deleted.

src/components/shared/Types.tsx

Lines changed: 35 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
isBooleanishNode,
23
isReferenceNode,
34
isRegularNode,
45
RegularNode,
@@ -9,10 +10,8 @@ import {
910
import { Box } from '@stoplight/mosaic';
1011
import * as React from 'react';
1112

12-
import { COMMON_JSON_SCHEMA_AND_OAS_FORMATS } from '../../consts';
13-
import { isPrimitiveArray } from '../../tree';
1413
import { printName } from '../../utils';
15-
import { Format } from './Format';
14+
import { getApplicableFormats } from '../../utils/getApplicableFormats';
1615

1716
function shouldRenderName(type: SchemaNodeKind | SchemaCombinerName | '$ref'): boolean {
1817
return type === SchemaNodeKind.Array || type === SchemaNodeKind.Object || type === '$ref';
@@ -32,32 +31,6 @@ function getTypes(schemaNode: RegularNode): Array<SchemaNodeKind | SchemaCombine
3231
);
3332
}
3433

35-
function getFormats(schemaNode: RegularNode): Partial<Record<SchemaNodeKind, string>> {
36-
const formats: Partial<Record<SchemaNodeKind, string>> = {};
37-
38-
if (isPrimitiveArray(schemaNode) && schemaNode.children[0].format !== null) {
39-
formats.array = schemaNode.children[0].format;
40-
}
41-
42-
if (schemaNode.format === null) {
43-
return formats;
44-
}
45-
46-
const types = getTypes(schemaNode);
47-
48-
for (const type of types) {
49-
if (!(type in COMMON_JSON_SCHEMA_AND_OAS_FORMATS)) continue;
50-
51-
if (COMMON_JSON_SCHEMA_AND_OAS_FORMATS[type].includes(schemaNode.format)) {
52-
formats[type] = schemaNode.format;
53-
return formats;
54-
}
55-
}
56-
57-
formats.string = schemaNode.format;
58-
return formats;
59-
}
60-
6134
export const Types: React.FunctionComponent<{ schemaNode: SchemaNode }> = ({ schemaNode }) => {
6235
if (isReferenceNode(schemaNode)) {
6336
return (
@@ -67,32 +40,51 @@ export const Types: React.FunctionComponent<{ schemaNode: SchemaNode }> = ({ sch
6740
);
6841
}
6942

43+
if (isBooleanishNode(schemaNode)) {
44+
return (
45+
<Box as="span" textOverflow="truncate" color="muted" data-test="property-type">
46+
{schemaNode.fragment ? 'any' : 'never'}
47+
</Box>
48+
);
49+
}
50+
7051
if (!isRegularNode(schemaNode)) {
7152
return null;
7253
}
7354

55+
const formats = getApplicableFormats(schemaNode);
7456
const types = getTypes(schemaNode);
75-
const formats = getFormats(schemaNode);
7657

7758
if (types.length === 0) {
78-
return formats.string !== void 0 ? <Format format={formats.string} /> : null;
79-
}
80-
81-
const rendered = types.map((type, i, { length }) => (
82-
<React.Fragment key={type}>
59+
return (
8360
<Box as="span" textOverflow="truncate" color="muted" data-test="property-type">
84-
{shouldRenderName(type) ? printName(schemaNode) ?? type : type}
61+
{formats === null ? 'any' : `<${formats[1]}>`}
8562
</Box>
63+
);
64+
}
65+
66+
const rendered = types.map((type, i, { length }) => {
67+
let printedName;
68+
if (shouldRenderName(type)) {
69+
printedName = printName(schemaNode);
70+
}
8671

87-
{type in formats ? <Format format={formats[type]} /> : null}
72+
printedName ??= type + (formats === null || formats[0] !== type ? '' : `<${formats[1]}>`);
8873

89-
{i < length - 1 && (
90-
<Box as="span" key={`${i}-sep`} color="muted">
91-
{' or '}
74+
return (
75+
<React.Fragment key={type}>
76+
<Box as="span" textOverflow="truncate" color="muted" data-test="property-type">
77+
{printedName}
9278
</Box>
93-
)}
94-
</React.Fragment>
95-
));
79+
80+
{i < length - 1 && (
81+
<Box as="span" key={`${i}-sep`} color="muted">
82+
{' or '}
83+
</Box>
84+
)}
85+
</React.Fragment>
86+
);
87+
});
9688

9789
return rendered.length > 1 ? <Box textOverflow="truncate">{rendered}</Box> : <>{rendered}</>;
9890
};

src/components/shared/__tests__/Format.spec.tsx

Lines changed: 0 additions & 41 deletions
This file was deleted.

0 commit comments

Comments
 (0)