feat: display patternProperties (#2008)

This commit is contained in:
Andrew Tatomyr 2022-05-13 14:53:03 +03:00 committed by GitHub
parent 58cd3cb782
commit 660cc857bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 139 additions and 13 deletions

View File

@ -1,4 +1,4 @@
import styled, { extensionsHook, media } from '../styled-components'; import styled, { extensionsHook, media, css } from '../styled-components';
import { deprecatedCss } from './mixins'; import { deprecatedCss } from './mixins';
export const PropertiesTableCaption = styled.caption` export const PropertiesTableCaption = styled.caption`
@ -72,7 +72,26 @@ export const PropertyNameCell = styled(PropertyCell)`
${deprecatedCss}; ${deprecatedCss};
} }
${({ kind }) => (kind !== 'field' ? 'font-style: italic' : '')}; ${({ kind }) =>
kind === 'patternProperties' &&
css`
> span.property-name {
display: inline-table;
white-space: break-spaces;
margin-right: 20px;
::before,
::after {
content: '/';
filter: opacity(0.2);
}
}
`}
${({ kind = '' }) =>
['field', 'additionalProperties', 'patternProperties'].includes(kind)
? ''
: 'font-style: italic'};
${extensionsHook('PropertyNameCell')}; ${extensionsHook('PropertyNameCell')};
`; `;

View File

@ -1,6 +1,6 @@
import { transparentize } from 'polished'; import { transparentize } from 'polished';
import styled, { extensionsHook } from '../styled-components'; import styled, { extensionsHook, css } from '../styled-components';
import { PropertyNameCell } from './fields-layout'; import { PropertyNameCell } from './fields-layout';
import { ShelfIcon } from './shelfs'; import { ShelfIcon } from './shelfs';
@ -17,6 +17,27 @@ export const ClickablePropertyNameCell = styled(PropertyNameCell)`
&:focus { &:focus {
font-weight: ${({ theme }) => theme.typography.fontWeightBold}; font-weight: ${({ theme }) => theme.typography.fontWeightBold};
} }
${({ kind }) =>
kind === 'patternProperties' &&
css`
display: inline-flex;
margin-right: 20px;
> span.property-name {
white-space: break-spaces;
text-align: left;
::before,
::after {
content: '/';
filter: opacity(0.2);
}
}
> svg {
align-self: center;
}
`}
} }
${ShelfIcon} { ${ShelfIcon} {
height: ${({ theme }) => theme.schema.arrow.size}; height: ${({ theme }) => theme.schema.arrow.size};
@ -56,6 +77,10 @@ export const RequiredLabel = styled(FieldLabel.withComponent('div'))`
line-height: 1; line-height: 1;
`; `;
export const PropertyLabel = styled(RequiredLabel)`
color: ${props => props.theme.colors.primary.light};
`;
export const RecursiveLabel = styled(FieldLabel)` export const RecursiveLabel = styled(FieldLabel)`
color: ${({ theme }) => theme.colors.warning.main}; color: ${({ theme }) => theme.colors.warning.main};
font-size: 13px; font-size: 13px;

View File

@ -37,6 +37,7 @@ class IntShelfIcon extends React.PureComponent<{
export const ShelfIcon = styled(IntShelfIcon)` export const ShelfIcon = styled(IntShelfIcon)`
height: ${props => props.size || '18px'}; height: ${props => props.size || '18px'};
width: ${props => props.size || '18px'}; width: ${props => props.size || '18px'};
min-width: ${props => props.size || '18px'};
vertical-align: middle; vertical-align: middle;
float: ${props => props.float || ''}; float: ${props => props.float || ''};
transition: transform 0.2s ease-out; transition: transform 0.2s ease-out;

View File

@ -1,9 +1,12 @@
import { observer } from 'mobx-react'; import { observer } from 'mobx-react';
import * as React from 'react'; import * as React from 'react';
import { ClickablePropertyNameCell, RequiredLabel } from '../../common-elements/fields'; import {
ClickablePropertyNameCell,
PropertyLabel,
RequiredLabel,
} from '../../common-elements/fields';
import { FieldDetails } from './FieldDetails'; import { FieldDetails } from './FieldDetails';
import { import {
InnerPropertiesWrap, InnerPropertiesWrap,
PropertyBullet, PropertyBullet,
@ -11,11 +14,10 @@ import {
PropertyDetailsCell, PropertyDetailsCell,
PropertyNameCell, PropertyNameCell,
} from '../../common-elements/fields-layout'; } from '../../common-elements/fields-layout';
import { ShelfIcon } from '../../common-elements/'; import { ShelfIcon } from '../../common-elements/';
import { Schema } from '../Schema/Schema';
import { FieldModel } from '../../services/models'; import type { SchemaOptions } from '../Schema/Schema';
import { Schema, SchemaOptions } from '../Schema/Schema'; import type { FieldModel } from '../../services/models';
export interface FieldProps extends SchemaOptions { export interface FieldProps extends SchemaOptions {
className?: string; className?: string;
@ -52,6 +54,14 @@ export class Field extends React.Component<FieldProps> {
const expanded = field.expanded === undefined ? expandByDefault : field.expanded; const expanded = field.expanded === undefined ? expandByDefault : field.expanded;
const labels = (
<>
{kind === 'additionalProperties' && <PropertyLabel>additional property</PropertyLabel>}
{kind === 'patternProperties' && <PropertyLabel>pattern property</PropertyLabel>}
{required && <RequiredLabel>required</RequiredLabel>}
</>
);
const paramName = withSubSchema ? ( const paramName = withSubSchema ? (
<ClickablePropertyNameCell <ClickablePropertyNameCell
className={deprecated ? 'deprecated' : ''} className={deprecated ? 'deprecated' : ''}
@ -64,16 +74,16 @@ export class Field extends React.Component<FieldProps> {
onKeyPress={this.handleKeyPress} onKeyPress={this.handleKeyPress}
aria-label="expand properties" aria-label="expand properties"
> >
<span>{name}</span> <span className="property-name">{name}</span>
<ShelfIcon direction={expanded ? 'down' : 'right'} /> <ShelfIcon direction={expanded ? 'down' : 'right'} />
</button> </button>
{required && <RequiredLabel> required </RequiredLabel>} {labels}
</ClickablePropertyNameCell> </ClickablePropertyNameCell>
) : ( ) : (
<PropertyNameCell className={deprecated ? 'deprecated' : undefined} kind={kind} title={name}> <PropertyNameCell className={deprecated ? 'deprecated' : undefined} kind={kind} title={name}>
<PropertyBullet /> <PropertyBullet />
<span>{name}</span> <span className="property-name">{name}</span>
{required && <RequiredLabel> required </RequiredLabel>} {labels}
</PropertyNameCell> </PropertyNameCell>
); );

View File

@ -0,0 +1,32 @@
{
"openapi": "3.1.0",
"info": {
"title": "Schema definition with unevaluatedProperties",
"version": "1.0.0"
},
"servers": [
{
"url": "example.com"
}
],
"components": {
"schemas": {
"Patterns": {
"type": "object",
"patternProperties": {
"^S_\\w+\\.[1-9]{2,4}$": {
"type": "string"
},
"^O_\\w+\\.[1-9]{2,4}$": {
"type": "object",
"properties": {
"x-nestedProperty": {
"type": "string"
}
}
}
}
}
}
}
}

View File

@ -76,5 +76,16 @@ describe('Models', () => {
expect(schema.fields![1].kind).toEqual('additionalProperties'); expect(schema.fields![1].kind).toEqual('additionalProperties');
expect(schema.fields![1].schema.type).toEqual('boolean'); expect(schema.fields![1].schema.type).toEqual('boolean');
}); });
test('schemaDefinition should resolve patternProperties', () => {
const spec = require('../fixtures/3.1/patternProperties.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Patterns, '', opts);
expect(schema.fields).toHaveLength(2);
expect(schema.fields![0].kind).toEqual('patternProperties');
expect(schema.fields![0].schema.type).toEqual('string');
expect(schema.fields![1].kind).toEqual('patternProperties');
expect(schema.fields![1].schema.type).toEqual('object');
});
}); });
}); });

View File

@ -364,6 +364,7 @@ function buildFields(
options: RedocNormalizedOptions, options: RedocNormalizedOptions,
): FieldModel[] { ): FieldModel[] {
const props = schema.properties || {}; const props = schema.properties || {};
const patternProps = schema.patternProperties || {};
const additionalProps = schema.additionalProperties || schema.unevaluatedProperties; const additionalProps = schema.additionalProperties || schema.unevaluatedProperties;
const defaults = schema.default; const defaults = schema.default;
let fields = Object.keys(props || []).map(fieldName => { let fields = Object.keys(props || []).map(fieldName => {
@ -402,6 +403,31 @@ function buildFields(
fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined); fields = sortByRequired(fields, !options.sortPropsAlphabetically ? schema.required : undefined);
} }
fields.push(
...Object.keys(patternProps).map(fieldName => {
let field = patternProps[fieldName];
if (!field) {
console.warn(
`Field "${fieldName}" is invalid, skipping.\n Field must be an object but got ${typeof field} at "${$ref}"`,
);
field = {};
}
return new FieldModel(
parser,
{
name: fieldName,
required: false,
schema: field,
kind: 'patternProperties',
},
`${$ref}/patternProperties/${fieldName}`,
options,
);
}),
);
if (typeof additionalProps === 'object' || additionalProps === true) { if (typeof additionalProps === 'object' || additionalProps === true) {
fields.push( fields.push(
new FieldModel( new FieldModel(

View File

@ -113,6 +113,7 @@ export interface OpenAPISchema {
$ref?: string; $ref?: string;
type?: string | string[]; type?: string | string[];
properties?: { [name: string]: OpenAPISchema }; properties?: { [name: string]: OpenAPISchema };
patternProperties?: { [name: string]: OpenAPISchema };
additionalProperties?: boolean | OpenAPISchema; additionalProperties?: boolean | OpenAPISchema;
unevaluatedProperties?: boolean | OpenAPISchema; unevaluatedProperties?: boolean | OpenAPISchema;
description?: string; description?: string;

View File

@ -99,6 +99,7 @@ const schemaKeywordTypes = {
additionalProperties: 'object', additionalProperties: 'object',
unevaluatedProperties: 'object', unevaluatedProperties: 'object',
properties: 'object', properties: 'object',
patternProperties: 'object',
}; };
export function detectType(schema: OpenAPISchema): string { export function detectType(schema: OpenAPISchema): string {