diff --git a/src/__stories__/JsonSchemaViewer.tsx b/src/__stories__/JsonSchemaViewer.tsx index 419b4b29..adcd0bd8 100644 --- a/src/__stories__/JsonSchemaViewer.tsx +++ b/src/__stories__/JsonSchemaViewer.tsx @@ -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', () => ( + { return (
- Error + Error {error && `: ${error.message}`}
); diff --git a/src/types.ts b/src/types.ts index 169ddf5c..14f6b0c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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', diff --git a/src/utils/__tests__/flattenTypes.spec.ts b/src/utils/__tests__/flattenTypes.spec.ts new file mode 100644 index 00000000..691e44f0 --- /dev/null +++ b/src/utils/__tests__/flattenTypes.spec.ts @@ -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.', + ); + }); +}); diff --git a/src/utils/__tests__/renderSchema.spec.ts b/src/utils/__tests__/renderSchema.spec.ts index fee6a010..4c7b7cbd 100644 --- a/src/utils/__tests__/renderSchema.spec.ts +++ b/src/utils/__tests__/renderSchema.spec.ts @@ -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.', + ); + }); }); diff --git a/src/utils/flattenTypes.ts b/src/utils/flattenTypes.ts new file mode 100644 index 00000000..cf3d328a --- /dev/null +++ b/src/utils/flattenTypes.ts @@ -0,0 +1,54 @@ +import { Optional } from '@stoplight/types'; +import { JSONSchema4TypeName } from 'json-schema'; +import { isValidType } from './isValidType'; + +function getTypeFromObject(obj: object): Optional { + 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 => { + 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); +}; diff --git a/src/utils/isValidType.ts b/src/utils/isValidType.ts new file mode 100644 index 00000000..7c86e93e --- /dev/null +++ b/src/utils/isValidType.ts @@ -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); diff --git a/src/utils/walk.ts b/src/utils/walk.ts index 149823ad..88c1a6c2 100644 --- a/src/utils/walk.ts +++ b/src/utils/walk.ts @@ -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)) { @@ -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,