Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion src/__stories__/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,38 @@ storiesOf('JsonSchemaViewer', module)
},
null,
)}
onError={(error: any) => console.log('You can hook into the onError handler too!', error)}
expanded={boolean('expanded', false)}
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
hideTopBar={boolean('hideTopBar', false)}
onGoToRef={action('onGoToRef')}
mergeAllOf={boolean('mergeAllOf', true)}
/>
))
.add('invalid types property pretty error message', () => (
<JsonSchemaViewer
schema={{
type: 'object',
// @ts-ignore
properties: {
id: {
type: 'string',
},
address: {
type: [
'null',
{
type: 'object',
properties: {
taskId: {
type: 'string',
format: 'uuid',
},
},
},
],
},
},
}}
expanded={boolean('expanded', false)}
defaultExpandedDepth={number('defaultExpandedDepth', 2)}
hideTopBar={boolean('hideTopBar', false)}
Expand Down
2 changes: 1 addition & 1 deletion src/components/JsonSchemaViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@ export class JsonSchemaViewerComponent extends React.PureComponent<IJsonSchemaVi
const JsonSchemaFallbackComponent: FallbackComponent = ({ error }) => {
return (
<div className="p-4">
<b>Error</b>
<b className="text-danger">Error</b>
{error && `: ${error.message}`}
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Dictionary, JsonPath } from '@stoplight/types';
import { JSONSchema4, JSONSchema4TypeName } from 'json-schema';
import * as React from 'react';

export const enum SchemaKind {
export enum SchemaKind {
Any = 'any',
String = 'string',
Number = 'number',
Expand Down
52 changes: 52 additions & 0 deletions src/utils/__tests__/flattenTypes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { flattenTypes } from '../flattenTypes';

describe('flattenTypes util', () => {
it.each(['string', 'number', ['object']])('given valid %s type, returns it', type => {
expect(flattenTypes(type)).toEqual(type);
});

it('returns undefined when no valid type is found', () => {
expect(flattenTypes(2)).toBeUndefined();
expect(flattenTypes(void 0)).toBeUndefined();
expect(flattenTypes('foo')).toBeUndefined();
expect(flattenTypes(['test', 'foo'])).toBeUndefined();
});

it('returns undefined when no valid type is found', () => {
expect(flattenTypes(2)).toBeUndefined();
expect(flattenTypes(void 0)).toBeUndefined();
expect(flattenTypes('foo')).toBeUndefined();
expect(flattenTypes(['test', 'foo'])).toBeUndefined();
expect(flattenTypes({ type: 'bar' })).toBeUndefined();
});

it('deduplicate types', () => {
expect(flattenTypes(['null', 'string', 'string'])).toEqual(['null', 'string']);
});

it('removes invalid types', () => {
expect(flattenTypes(['foo', 'string'])).toEqual(['string']);
expect(flattenTypes(['foo', { type: 'bar' }, 'string'])).toEqual(['string']);
expect(flattenTypes(['foo', 'string'])).toEqual(['string']);
expect(flattenTypes(['number', 2, null])).toEqual(['number']);
});

it('flattens types', () => {
expect(flattenTypes([{ type: 'array' }, { type: 'object' }, { type: 'object' }, { type: 'number' }])).toEqual([
'array',
'object',
'number',
]);
});

it.each([
[{ type: 'array', items: {} }, { type: 'object', properties: {} }],
[{ type: 'string', enum: [] }],
{ type: 'number', minimum: 1 },
{ additionalProperties: 1 },
])('throws when complex type is met', type => {
expect(flattenTypes.bind(null, type)).toThrow(
'The "type" property must be a string, or an array of strings. Objects and array of objects are not valid.',
);
});
});
25 changes: 24 additions & 1 deletion src/utils/__tests__/renderSchema.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,30 @@ describe('renderSchema util', () => {
['tickets.schema.json', ''],
])('should match %s', (schema, dereferenced) => {
expect(
Array.from(renderSchema(JSON.parse(fs.readFileSync(path.resolve(BASE_PATH, schema), 'utf-8')))),
Array.from(renderSchema(JSON.parse(fs.readFileSync(path.resolve(BASE_PATH, schema), 'utf8')))),
).toMatchSnapshot();
});

it('given schema with complex types, throws', () => {
expect(() =>
Array.from(
renderSchema({
type: [
'null',
{
type: 'object',
properties: {
taskId: {
type: 'string',
format: 'uuid',
},
},
},
],
} as any),
),
).toThrow(
'The "type" property must be a string, or an array of strings. Objects and array of objects are not valid.',
);
});
});
54 changes: 54 additions & 0 deletions src/utils/flattenTypes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Optional } from '@stoplight/types';
import { JSONSchema4TypeName } from 'json-schema';
import { isValidType } from './isValidType';

function getTypeFromObject(obj: object): Optional<JSONSchema4TypeName> {
const size = Object.keys(obj).length;

if (size > 1 || !('type' in obj)) {
throw new Error(
'The "type" property must be a string, or an array of strings. Objects and array of objects are not valid.',
);
}

if ('type' in obj && isValidType((obj as { type: string }).type)) {
return (obj as { type: JSONSchema4TypeName }).type;
}

return;
}

function flattenType(type: unknown) {
if (typeof type === 'string') {
return type;
}

if (typeof type !== 'object' || type === null) {
return;
}

return getTypeFromObject(type);
}

export const flattenTypes = (types: unknown): Optional<JSONSchema4TypeName | JSONSchema4TypeName[]> => {
if (typeof types === 'string' && isValidType(types)) {
return types;
}

if (typeof types !== 'object' || types === null) {
return;
}

if (Array.isArray(types)) {
const flattenedTypes: JSONSchema4TypeName[] = [];
for (const type of types) {
const flattened = flattenType(type);
if (!isValidType(flattened) || flattenedTypes.includes(flattened)) continue;
flattenedTypes.push(flattened);
}

return flattenedTypes.length > 0 ? flattenedTypes : void 0;
}

return getTypeFromObject(types);
};
5 changes: 5 additions & 0 deletions src/utils/isValidType.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { JSONSchema4TypeName } from 'json-schema';
import { SchemaKind } from '../types';

export const isValidType = (maybeType: unknown): maybeType is JSONSchema4TypeName =>
typeof maybeType === 'string' && Object.values(SchemaKind).includes(maybeType as SchemaKind);
15 changes: 4 additions & 11 deletions src/utils/walk.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import { JSONSchema4 } from 'json-schema';
import {
IArrayNode,
IBaseNode,
ICombinerNode,
IObjectNode,
IRefNode,
SchemaKind,
SchemaNode,
} from '../types';
import { IArrayNode, IBaseNode, ICombinerNode, IObjectNode, IRefNode, SchemaKind, SchemaNode } from '../types';
import { flattenTypes } from './flattenTypes';
import { generateId } from './generateId';
import { getAnnotations } from './getAnnotations';
import { getCombiner } from './getCombiner';
import { getPrimaryType } from './getPrimaryType';
import { getValidations } from './getValidations';
import { inferType } from './inferType';
import { getCombiner } from './getCombiner';

function assignNodeSpecificFields(base: IBaseNode, node: JSONSchema4) {
switch (getPrimaryType(node)) {
Expand Down Expand Up @@ -46,7 +39,7 @@ function processNode(node: JSONSchema4): SchemaNode | void {
if (type) {
const base: IBaseNode = {
id: generateId(),
type: node.type || inferType(node),
type: flattenTypes(type),
validations: getValidations(node),
annotations: getAnnotations(node),
enum: node.enum,
Expand Down