Skip to content

Commit

Permalink
feat: support dictionaries (#249)
Browse files Browse the repository at this point in the history
  • Loading branch information
P0lip committed Jan 24, 2024
1 parent 1c11c12 commit b76157a
Show file tree
Hide file tree
Showing 15 changed files with 408 additions and 165 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
},
"dependencies": {
"@stoplight/json": "^3.20.1",
"@stoplight/json-schema-tree": "^3.0.0",
"@stoplight/json-schema-tree": "^4.0.0",
"@stoplight/react-error-boundary": "^2.0.0",
"@types/json-schema": "^7.0.7",
"classnames": "^2.2.6",
Expand Down
30 changes: 17 additions & 13 deletions src/__fixtures__/formats-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@
"type": "object",
"properties": {
"date-of-birth": {
"type": [
"number",
"string",
"array"
],
"type": ["number", "string", "array"],
"format": "date-time",
"items": {}
},
Expand All @@ -23,20 +19,28 @@
"format": "int32"
},
"size": {
"type": [
"number",
"string"
],
"type": ["number", "string"],
"format": "byte"
},
"notype": {
"format": "date-time"
},
"array-of-integers": {
"type": "array",
"items": {
"type": "integer",
"format": "int32"
}
},
"map-of-ids": {
"type": "object",
"additionalProperties": {
"type": "integer",
"format": "int32"
}
},
"permissions": {
"type": [
"string",
"object"
],
"type": ["string", "object"],
"format": "password",
"properties": {
"ids": {
Expand Down
117 changes: 116 additions & 1 deletion src/__tests__/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'jest-enzyme';

import { mount, ReactWrapper } from 'enzyme';
import { JSONSchema4 } from 'json-schema';
import { JSONSchema4, JSONSchema7 } from 'json-schema';
import * as React from 'react';

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

it('given dictionary with defined properties, should not render them', () => {
const schema: JSONSchema7 = {
type: ['object', 'null'],
properties: {
id: {
type: 'string',
readOnly: true,
},
description: {
type: 'string',
writeOnly: true,
},
},
additionalProperties: {
type: 'string',
},
};

expect(dumpDom(<JsonSchemaViewer schema={schema} defaultExpandedDepth={Infinity} />)).toMatchInlineSnapshot(`
"<div class=\\"\\" id=\\"mosaic-provider-react-aria-0-1\\">
<div data-overlay-container=\\"true\\">
<div class=\\"JsonSchemaViewer\\">
<div></div>
<div data-id=\\"bf8b96e78f11d\\" data-test=\\"schema-row\\">
<div>
<div>
<div>
<div>
<span data-test=\\"property-type\\">dictionary[string, string]</span>
<span>or</span>
<span data-test=\\"property-type\\">null</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
"
`);
});

it('should not render true/false additionalProperties', () => {
const schema: JSONSchema7 = {
type: 'object',
properties: {
id: {
type: 'string',
},
},
additionalProperties: true,
};

const additionalTrue = dumpDom(<JsonSchemaViewer schema={schema} defaultExpandedDepth={Infinity} />);
const additionalFalse = dumpDom(
<JsonSchemaViewer schema={{ ...schema, additionalProperties: false }} defaultExpandedDepth={Infinity} />,
);
expect(additionalTrue).toEqual(additionalFalse);
expect(additionalTrue).toMatchInlineSnapshot(`
"<div class=\\"\\" id=\\"mosaic-provider-react-aria-0-1\\">
<div data-overlay-container=\\"true\\">
<div class=\\"JsonSchemaViewer\\">
<div></div>
<div data-level=\\"0\\">
<div data-id=\\"8074f410d9775\\" data-test=\\"schema-row\\">
<div>
<div>
<div>
<div data-test=\\"property-name-id\\">id</div>
<span data-test=\\"property-type\\">string</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
"
`);
});

it('should not render additionalItems', () => {
const schema: JSONSchema7 = {
type: 'array',
additionalItems: {
type: 'object',
properties: {
id: {
type: 'string',
},
},
},
};

expect(dumpDom(<JsonSchemaViewer schema={schema} defaultExpandedDepth={Infinity} />)).toMatchInlineSnapshot(`
"<div class=\\"\\" id=\\"mosaic-provider-react-aria-0-1\\">
<div data-overlay-container=\\"true\\">
<div class=\\"JsonSchemaViewer\\">
<div></div>
<div data-id=\\"bf8b96e78f11d\\" data-test=\\"schema-row\\">
<div>
<div>
<div><span data-test=\\"property-type\\">array</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
"
`);
});

describe('top level descriptions', () => {
const schema: JSONSchema4 = {
description: 'This is a description that should be rendered',
Expand Down
17 changes: 3 additions & 14 deletions src/components/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import {
isRegularNode,
RootNode,
SchemaNode,
SchemaTree as JsonSchemaTree,
SchemaTreeRefDereferenceFn,
} from '@stoplight/json-schema-tree';
Expand All @@ -13,7 +12,8 @@ import { useUpdateAtom } from 'jotai/utils';
import * as React from 'react';

import { JSVOptions, JSVOptionsContextProvider } from '../contexts';
import type { JSONSchema } from '../types';
import { shouldNodeBeIncluded } from '../tree/utils';
import { JSONSchema } from '../types';
import { PathCrumbs } from './PathCrumbs';
import { TopLevelSchemaRow } from './SchemaRow';
import { hoveredNodeAtom } from './SchemaRow/state';
Expand Down Expand Up @@ -114,20 +114,9 @@ const JsonSchemaViewerInner = ({
});

let nodeCount = 0;
const shouldNodeBeIncluded = (node: SchemaNode) => {
if (!isRegularNode(node)) return true;

const { validations } = node;

if (!!validations.writeOnly === !!validations.readOnly) {
return true;
}

return !((viewMode === 'read' && !!validations.writeOnly) || (viewMode === 'write' && !!validations.readOnly));
};

jsonSchemaTree.walker.hookInto('filter', node => {
if (shouldNodeBeIncluded(node)) {
if (shouldNodeBeIncluded(node, viewMode)) {
nodeCount++;
return true;
}
Expand Down
10 changes: 0 additions & 10 deletions src/components/shared/Format.tsx

This file was deleted.

78 changes: 35 additions & 43 deletions src/components/shared/Types.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
isBooleanishNode,
isReferenceNode,
isRegularNode,
RegularNode,
Expand All @@ -9,10 +10,8 @@ import {
import { Box } from '@stoplight/mosaic';
import * as React from 'react';

import { COMMON_JSON_SCHEMA_AND_OAS_FORMATS } from '../../consts';
import { isPrimitiveArray } from '../../tree';
import { printName } from '../../utils';
import { Format } from './Format';
import { getApplicableFormats } from '../../utils/getApplicableFormats';

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

function getFormats(schemaNode: RegularNode): Partial<Record<SchemaNodeKind, string>> {
const formats: Partial<Record<SchemaNodeKind, string>> = {};

if (isPrimitiveArray(schemaNode) && schemaNode.children[0].format !== null) {
formats.array = schemaNode.children[0].format;
}

if (schemaNode.format === null) {
return formats;
}

const types = getTypes(schemaNode);

for (const type of types) {
if (!(type in COMMON_JSON_SCHEMA_AND_OAS_FORMATS)) continue;

if (COMMON_JSON_SCHEMA_AND_OAS_FORMATS[type].includes(schemaNode.format)) {
formats[type] = schemaNode.format;
return formats;
}
}

formats.string = schemaNode.format;
return formats;
}

export const Types: React.FunctionComponent<{ schemaNode: SchemaNode }> = ({ schemaNode }) => {
if (isReferenceNode(schemaNode)) {
return (
Expand All @@ -67,32 +40,51 @@ export const Types: React.FunctionComponent<{ schemaNode: SchemaNode }> = ({ sch
);
}

if (isBooleanishNode(schemaNode)) {
return (
<Box as="span" textOverflow="truncate" color="muted" data-test="property-type">
{schemaNode.fragment ? 'any' : 'never'}
</Box>
);
}

if (!isRegularNode(schemaNode)) {
return null;
}

const formats = getApplicableFormats(schemaNode);
const types = getTypes(schemaNode);
const formats = getFormats(schemaNode);

if (types.length === 0) {
return formats.string !== void 0 ? <Format format={formats.string} /> : null;
}

const rendered = types.map((type, i, { length }) => (
<React.Fragment key={type}>
return (
<Box as="span" textOverflow="truncate" color="muted" data-test="property-type">
{shouldRenderName(type) ? printName(schemaNode) ?? type : type}
{formats === null ? 'any' : `<${formats[1]}>`}
</Box>
);
}

const rendered = types.map((type, i, { length }) => {
let printedName;
if (shouldRenderName(type)) {
printedName = printName(schemaNode);
}

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

{i < length - 1 && (
<Box as="span" key={`${i}-sep`} color="muted">
{' or '}
return (
<React.Fragment key={type}>
<Box as="span" textOverflow="truncate" color="muted" data-test="property-type">
{printedName}
</Box>
)}
</React.Fragment>
));

{i < length - 1 && (
<Box as="span" key={`${i}-sep`} color="muted">
{' or '}
</Box>
)}
</React.Fragment>
);
});

return rendered.length > 1 ? <Box textOverflow="truncate">{rendered}</Box> : <>{rendered}</>;
};
Expand Down
41 changes: 0 additions & 41 deletions src/components/shared/__tests__/Format.spec.tsx

This file was deleted.

Loading

0 comments on commit b76157a

Please sign in to comment.