Skip to content

Commit 24eb14c

Browse files
authored
fix: handle invalid types more gracefully (#60)
* fix: handle invalid types more gracefully * chore: tweak error message
1 parent ede888a commit 24eb14c

File tree

8 files changed

+173
-15
lines changed

8 files changed

+173
-15
lines changed

src/__stories__/JsonSchemaViewer.tsx

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,38 @@ storiesOf('JsonSchemaViewer', module)
137137
},
138138
null,
139139
)}
140-
onError={(error: any) => console.log('You can hook into the onError handler too!', error)}
140+
expanded={boolean('expanded', false)}
141+
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
142+
hideTopBar={boolean('hideTopBar', false)}
143+
onGoToRef={action('onGoToRef')}
144+
mergeAllOf={boolean('mergeAllOf', true)}
145+
/>
146+
))
147+
.add('invalid types property pretty error message', () => (
148+
<JsonSchemaViewer
149+
schema={{
150+
type: 'object',
151+
// @ts-ignore
152+
properties: {
153+
id: {
154+
type: 'string',
155+
},
156+
address: {
157+
type: [
158+
'null',
159+
{
160+
type: 'object',
161+
properties: {
162+
taskId: {
163+
type: 'string',
164+
format: 'uuid',
165+
},
166+
},
167+
},
168+
],
169+
},
170+
},
171+
}}
141172
expanded={boolean('expanded', false)}
142173
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
143174
hideTopBar={boolean('hideTopBar', false)}

src/components/JsonSchemaViewer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
208208
const JsonSchemaFallbackComponent: FallbackComponent = ({ error }) => {
209209
return (
210210
<div className="p-4">
211-
<b>Error</b>
211+
<b className="text-danger">Error</b>
212212
{error && `: ${error.message}`}
213213
</div>
214214
);

src/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Dictionary, JsonPath } from '@stoplight/types';
33
import { JSONSchema4, JSONSchema4TypeName } from 'json-schema';
44
import * as React from 'react';
55

6-
export const enum SchemaKind {
6+
export enum SchemaKind {
77
Any = 'any',
88
String = 'string',
99
Number = 'number',
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { flattenTypes } from '../flattenTypes';
2+
3+
describe('flattenTypes util', () => {
4+
it.each(['string', 'number', ['object']])('given valid %s type, returns it', type => {
5+
expect(flattenTypes(type)).toEqual(type);
6+
});
7+
8+
it('returns undefined when no valid type is found', () => {
9+
expect(flattenTypes(2)).toBeUndefined();
10+
expect(flattenTypes(void 0)).toBeUndefined();
11+
expect(flattenTypes('foo')).toBeUndefined();
12+
expect(flattenTypes(['test', 'foo'])).toBeUndefined();
13+
});
14+
15+
it('returns undefined when no valid type is found', () => {
16+
expect(flattenTypes(2)).toBeUndefined();
17+
expect(flattenTypes(void 0)).toBeUndefined();
18+
expect(flattenTypes('foo')).toBeUndefined();
19+
expect(flattenTypes(['test', 'foo'])).toBeUndefined();
20+
expect(flattenTypes({ type: 'bar' })).toBeUndefined();
21+
});
22+
23+
it('deduplicate types', () => {
24+
expect(flattenTypes(['null', 'string', 'string'])).toEqual(['null', 'string']);
25+
});
26+
27+
it('removes invalid types', () => {
28+
expect(flattenTypes(['foo', 'string'])).toEqual(['string']);
29+
expect(flattenTypes(['foo', { type: 'bar' }, 'string'])).toEqual(['string']);
30+
expect(flattenTypes(['foo', 'string'])).toEqual(['string']);
31+
expect(flattenTypes(['number', 2, null])).toEqual(['number']);
32+
});
33+
34+
it('flattens types', () => {
35+
expect(flattenTypes([{ type: 'array' }, { type: 'object' }, { type: 'object' }, { type: 'number' }])).toEqual([
36+
'array',
37+
'object',
38+
'number',
39+
]);
40+
});
41+
42+
it.each([
43+
[{ type: 'array', items: {} }, { type: 'object', properties: {} }],
44+
[{ type: 'string', enum: [] }],
45+
{ type: 'number', minimum: 1 },
46+
{ additionalProperties: 1 },
47+
])('throws when complex type is met', type => {
48+
expect(flattenTypes.bind(null, type)).toThrow(
49+
'The "type" property must be a string, or an array of strings. Objects and array of objects are not valid.',
50+
);
51+
});
52+
});

src/utils/__tests__/renderSchema.spec.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,30 @@ describe('renderSchema util', () => {
1919
['tickets.schema.json', ''],
2020
])('should match %s', (schema, dereferenced) => {
2121
expect(
22-
Array.from(renderSchema(JSON.parse(fs.readFileSync(path.resolve(BASE_PATH, schema), 'utf-8')))),
22+
Array.from(renderSchema(JSON.parse(fs.readFileSync(path.resolve(BASE_PATH, schema), 'utf8')))),
2323
).toMatchSnapshot();
2424
});
25+
26+
it('given schema with complex types, throws', () => {
27+
expect(() =>
28+
Array.from(
29+
renderSchema({
30+
type: [
31+
'null',
32+
{
33+
type: 'object',
34+
properties: {
35+
taskId: {
36+
type: 'string',
37+
format: 'uuid',
38+
},
39+
},
40+
},
41+
],
42+
} as any),
43+
),
44+
).toThrow(
45+
'The "type" property must be a string, or an array of strings. Objects and array of objects are not valid.',
46+
);
47+
});
2548
});

src/utils/flattenTypes.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { Optional } from '@stoplight/types';
2+
import { JSONSchema4TypeName } from 'json-schema';
3+
import { isValidType } from './isValidType';
4+
5+
function getTypeFromObject(obj: object): Optional<JSONSchema4TypeName> {
6+
const size = Object.keys(obj).length;
7+
8+
if (size > 1 || !('type' in obj)) {
9+
throw new Error(
10+
'The "type" property must be a string, or an array of strings. Objects and array of objects are not valid.',
11+
);
12+
}
13+
14+
if ('type' in obj && isValidType((obj as { type: string }).type)) {
15+
return (obj as { type: JSONSchema4TypeName }).type;
16+
}
17+
18+
return;
19+
}
20+
21+
function flattenType(type: unknown) {
22+
if (typeof type === 'string') {
23+
return type;
24+
}
25+
26+
if (typeof type !== 'object' || type === null) {
27+
return;
28+
}
29+
30+
return getTypeFromObject(type);
31+
}
32+
33+
export const flattenTypes = (types: unknown): Optional<JSONSchema4TypeName | JSONSchema4TypeName[]> => {
34+
if (typeof types === 'string' && isValidType(types)) {
35+
return types;
36+
}
37+
38+
if (typeof types !== 'object' || types === null) {
39+
return;
40+
}
41+
42+
if (Array.isArray(types)) {
43+
const flattenedTypes: JSONSchema4TypeName[] = [];
44+
for (const type of types) {
45+
const flattened = flattenType(type);
46+
if (!isValidType(flattened) || flattenedTypes.includes(flattened)) continue;
47+
flattenedTypes.push(flattened);
48+
}
49+
50+
return flattenedTypes.length > 0 ? flattenedTypes : void 0;
51+
}
52+
53+
return getTypeFromObject(types);
54+
};

src/utils/isValidType.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { JSONSchema4TypeName } from 'json-schema';
2+
import { SchemaKind } from '../types';
3+
4+
export const isValidType = (maybeType: unknown): maybeType is JSONSchema4TypeName =>
5+
typeof maybeType === 'string' && Object.values(SchemaKind).includes(maybeType as SchemaKind);

src/utils/walk.ts

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,12 @@
11
import { JSONSchema4 } from 'json-schema';
2-
import {
3-
IArrayNode,
4-
IBaseNode,
5-
ICombinerNode,
6-
IObjectNode,
7-
IRefNode,
8-
SchemaKind,
9-
SchemaNode,
10-
} from '../types';
2+
import { IArrayNode, IBaseNode, ICombinerNode, IObjectNode, IRefNode, SchemaKind, SchemaNode } from '../types';
3+
import { flattenTypes } from './flattenTypes';
114
import { generateId } from './generateId';
125
import { getAnnotations } from './getAnnotations';
6+
import { getCombiner } from './getCombiner';
137
import { getPrimaryType } from './getPrimaryType';
148
import { getValidations } from './getValidations';
159
import { inferType } from './inferType';
16-
import { getCombiner } from './getCombiner';
1710

1811
function assignNodeSpecificFields(base: IBaseNode, node: JSONSchema4) {
1912
switch (getPrimaryType(node)) {
@@ -46,7 +39,7 @@ function processNode(node: JSONSchema4): SchemaNode | void {
4639
if (type) {
4740
const base: IBaseNode = {
4841
id: generateId(),
49-
type: node.type || inferType(node),
42+
type: flattenTypes(type),
5043
validations: getValidations(node),
5144
annotations: getAnnotations(node),
5245
enum: node.enum,

0 commit comments

Comments
 (0)