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
2 changes: 2 additions & 0 deletions src/components/__tests__/SchemaRow.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Popover } from '@stoplight/ui-kit';
import { shallow } from 'enzyme';
import 'jest-enzyme';
import * as React from 'react';
Expand Down Expand Up @@ -30,6 +31,7 @@ describe('SchemaRow component', () => {
const wrapper = shallow(shallow(<SchemaRow node={node as SchemaTreeListNode} rowOptions={rowOptions} />)
.find(Validations)
.shallow()
.find(Popover)
.prop('content') as React.ReactElement);

expect(wrapper).toHaveText('enum:null,0,false');
Expand Down
31 changes: 31 additions & 0 deletions src/components/__tests__/Validations.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Dictionary } from '@stoplight/types';
import { Popover } from '@stoplight/ui-kit';
import { shallow } from 'enzyme';
import 'jest-enzyme';
import * as React from 'react';

import { getValidations } from '../../utils/getValidations';
import { Validations } from '../shared/Validations';

describe('Validations component', () => {
describe('when property is deprecated', () => {
let validations: Dictionary<unknown>;

beforeEach(() => {
validations = getValidations({ 'x-deprecated': true, type: 'string', format: 'email', minLength: 2 });
});

test('should exclude deprecated from general validations', () => {
const wrapper = shallow(<Validations required={false} validations={validations} />).find(Popover);

expect(shallow(wrapper.prop('target') as React.ReactElement)).toHaveText('optional+2');
expect(shallow(wrapper.prop('content') as React.ReactElement)).toHaveText('format:"email"minLength:2');
});

test('should render deprecated box next to popover', () => {
const wrapper = shallow(<Validations required={false} validations={validations} />).childAt(0);

expect(wrapper).toHaveText('deprecated');
});
});
});
96 changes: 52 additions & 44 deletions src/components/shared/Validations.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import * as React from 'react';

export interface IValidations {
required: boolean;
validations: Dictionary<unknown> | {};
validations: (Dictionary<unknown> | {}) & { deprecated?: boolean };
}

export const Validations: React.FunctionComponent<IValidations> = ({ required, validations }) => {
export const Validations: React.FunctionComponent<IValidations> = ({
required,
validations: { deprecated, ...validations },
}) => {
const validationCount = Object.keys(validations).length;

const requiredElem = (
Expand All @@ -18,49 +21,54 @@ export const Validations: React.FunctionComponent<IValidations> = ({ required, v
</div>
);

return validationCount ? (
<Popover
boundary="window"
interactionKind="hover"
content={
<div className="p-5" style={{ maxHeight: 500, maxWidth: 400 }}>
{Object.keys(validations).map((key, index) => {
const validation = validations[key];
return (
<>
{deprecated ? <span className="ml-2 text-orange-7 dark:text-orange-6">deprecated</span> : null}
{validationCount ? (
<Popover
boundary="window"
interactionKind="hover"
content={
<div className="p-5" style={{ maxHeight: 500, maxWidth: 400 }}>
{Object.keys(validations).map((key, index) => {
const validation = validations[key];

let elem = null;
if (Array.isArray(validation)) {
elem = validation.map((v, i) => (
<div key={i} className="mt-1 mr-1 flex items-center">
<div className="px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded">{String(v)}</div>
{i < validation.length - 1 ? <div>,</div> : null}
</div>
));
} else if (typeof validation === 'object') {
elem = (
<div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}>
{'{...}'}
</div>
);
} else {
elem = (
<div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}>
{JSON.stringify(validation)}
</div>
);
}
let elem = null;
if (Array.isArray(validation)) {
elem = validation.map((v, i) => (
<div key={i} className="mt-1 mr-1 flex items-center">
<div className="px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded">{String(v)}</div>
{i < validation.length - 1 ? <div>,</div> : null}
</div>
));
} else if (typeof validation === 'object') {
elem = (
<div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}>
{'{...}'}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why don't we stringify the validation here too so we can display the properties? Similar to what were doing on L53

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason we did it in the first place is that the object might be large and therefore the text produced wouldn't be readable.

</div>
);
} else {
elem = (
<div className="m-1 px-1 bg-gray-2 dark:bg-gray-8 font-bold text-sm rounded" key={index}>
{JSON.stringify(validation)}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we safe stringify here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. I'd say no, since you won't be able to recognize strings and numbers, i.e. "1" and 1.

Example:

  • JSON.stringify

    • string 2
      image
    • number 2
      image
  • safe stringify

    • string 2
      image
    • number 2
      image

</div>
);
}

return (
<div key={index} className="py-1 flex items-baseline">
<div className="font-medium pr-2">{key}:</div>
<div className="flex-1 flex flex-wrap justify-end">{elem}</div>
</div>
);
})}
</div>
}
target={requiredElem}
/>
) : (
requiredElem
return (
<div key={index} className="py-1 flex items-baseline">
<div className="font-medium pr-2">{key}:</div>
<div className="flex-1 flex flex-wrap justify-end">{elem}</div>
</div>
);
})}
</div>
}
target={requiredElem}
/>
) : (
requiredElem
)}
</>
);
};
20 changes: 20 additions & 0 deletions src/utils/__tests__/getValidations.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { getValidations } from '../getValidations';

describe('getValidations util', () => {
describe('deprecated property', () => {
test('given present x-deprecated, should include its value', () => {
expect(getValidations({ 'x-deprecated': false })).toStrictEqual({ deprecated: false });
expect(getValidations({ 'x-deprecated': false, deprecated: true })).toStrictEqual({ deprecated: false });
expect(getValidations({ 'x-deprecated': true })).toStrictEqual({ deprecated: true });
});

test('given present deprecated, should include its value', () => {
expect(getValidations({ deprecated: false })).toStrictEqual({ deprecated: false });
expect(getValidations({ deprecated: true })).toStrictEqual({ deprecated: true });
});

test('given missing deprecated, should not include anything', () => {
expect(getValidations({})).toStrictEqual({});
});
});
});
17 changes: 15 additions & 2 deletions src/utils/getValidations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Dictionary } from '@stoplight/types';
import { Dictionary, Optional } from '@stoplight/types';
import { JSONSchema4, JSONSchema4TypeName } from 'json-schema';
import { flatMap as _flatMap, pick as _pick } from 'lodash-es';

Expand All @@ -7,7 +7,6 @@ export const COMMON_VALIDATION_TYPES = [
'format', // https://tools.ietf.org/html/draft-fge-json-schema-validation-00#section-7
'default',
'example',
'deprecated',
'nullable',
'discriminator',
'readOnly',
Expand All @@ -23,6 +22,18 @@ const VALIDATION_TYPES = {
array: ['additionalItems', 'minItems', 'maxItems', 'uniqueItems'],
};

function getDeprecatedValue(node: JSONSchema4): Optional<boolean> {
if ('x-deprecated' in node) {
return !!node['x-deprecated'];
}

if ('deprecated' in node) {
return !!node.deprecated;
}

return;
}

function getTypeValidations(type: JSONSchema4TypeName | JSONSchema4TypeName[]): string[] {
if (Array.isArray(type)) {
return _flatMap(type, getTypeValidations);
Expand All @@ -33,8 +44,10 @@ function getTypeValidations(type: JSONSchema4TypeName | JSONSchema4TypeName[]):

export const getValidations = (node: JSONSchema4): Dictionary<unknown> => {
const extraValidations = node.type && getTypeValidations(node.type);
const deprecated = getDeprecatedValue(node);
return {
..._pick(node, COMMON_VALIDATION_TYPES),
...(extraValidations && _pick(node, extraValidations)),
...(deprecated !== void 0 && { deprecated }),
};
};