Skip to content

Commit

Permalink
feat: display patternProperties (#2008)
Browse files Browse the repository at this point in the history
  • Loading branch information
tatomyr authored May 13, 2022
1 parent 58cd3cb commit 660cc85
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 13 deletions.
23 changes: 21 additions & 2 deletions src/common-elements/fields-layout.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import styled, { extensionsHook, media } from '../styled-components';
import styled, { extensionsHook, media, css } from '../styled-components';
import { deprecatedCss } from './mixins';

export const PropertiesTableCaption = styled.caption`
Expand Down Expand Up @@ -72,7 +72,26 @@ export const PropertyNameCell = styled(PropertyCell)`
${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')};
`;
Expand Down
27 changes: 26 additions & 1 deletion src/common-elements/fields.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { transparentize } from 'polished';

import styled, { extensionsHook } from '../styled-components';
import styled, { extensionsHook, css } from '../styled-components';
import { PropertyNameCell } from './fields-layout';
import { ShelfIcon } from './shelfs';

Expand All @@ -17,6 +17,27 @@ export const ClickablePropertyNameCell = styled(PropertyNameCell)`
&:focus {
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} {
height: ${({ theme }) => theme.schema.arrow.size};
Expand Down Expand Up @@ -56,6 +77,10 @@ export const RequiredLabel = styled(FieldLabel.withComponent('div'))`
line-height: 1;
`;

export const PropertyLabel = styled(RequiredLabel)`
color: ${props => props.theme.colors.primary.light};
`;

export const RecursiveLabel = styled(FieldLabel)`
color: ${({ theme }) => theme.colors.warning.main};
font-size: 13px;
Expand Down
1 change: 1 addition & 0 deletions src/common-elements/shelfs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class IntShelfIcon extends React.PureComponent<{
export const ShelfIcon = styled(IntShelfIcon)`
height: ${props => props.size || '18px'};
width: ${props => props.size || '18px'};
min-width: ${props => props.size || '18px'};
vertical-align: middle;
float: ${props => props.float || ''};
transition: transform 0.2s ease-out;
Expand Down
30 changes: 20 additions & 10 deletions src/components/Fields/Field.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { observer } from 'mobx-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 {
InnerPropertiesWrap,
PropertyBullet,
PropertyCellWithInner,
PropertyDetailsCell,
PropertyNameCell,
} from '../../common-elements/fields-layout';

import { ShelfIcon } from '../../common-elements/';

import { FieldModel } from '../../services/models';
import { Schema, SchemaOptions } from '../Schema/Schema';
import { Schema } from '../Schema/Schema';
import type { SchemaOptions } from '../Schema/Schema';
import type { FieldModel } from '../../services/models';

export interface FieldProps extends SchemaOptions {
className?: string;
Expand Down Expand Up @@ -52,6 +54,14 @@ export class Field extends React.Component<FieldProps> {

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 ? (
<ClickablePropertyNameCell
className={deprecated ? 'deprecated' : ''}
Expand All @@ -64,16 +74,16 @@ export class Field extends React.Component<FieldProps> {
onKeyPress={this.handleKeyPress}
aria-label="expand properties"
>
<span>{name}</span>
<span className="property-name">{name}</span>
<ShelfIcon direction={expanded ? 'down' : 'right'} />
</button>
{required && <RequiredLabel> required </RequiredLabel>}
{labels}
</ClickablePropertyNameCell>
) : (
<PropertyNameCell className={deprecated ? 'deprecated' : undefined} kind={kind} title={name}>
<PropertyBullet />
<span>{name}</span>
{required && <RequiredLabel> required </RequiredLabel>}
<span className="property-name">{name}</span>
{labels}
</PropertyNameCell>
);

Expand Down
32 changes: 32 additions & 0 deletions src/services/__tests__/fixtures/3.1/patternProperties.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
}
}
}
}
}
11 changes: 11 additions & 0 deletions src/services/__tests__/models/Schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,5 +76,16 @@ describe('Models', () => {
expect(schema.fields![1].kind).toEqual('additionalProperties');
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');
});
});
});
26 changes: 26 additions & 0 deletions src/services/models/Schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ function buildFields(
options: RedocNormalizedOptions,
): FieldModel[] {
const props = schema.properties || {};
const patternProps = schema.patternProperties || {};
const additionalProps = schema.additionalProperties || schema.unevaluatedProperties;
const defaults = schema.default;
let fields = Object.keys(props || []).map(fieldName => {
Expand Down Expand Up @@ -402,6 +403,31 @@ function buildFields(
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) {
fields.push(
new FieldModel(
Expand Down
1 change: 1 addition & 0 deletions src/types/open-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ export interface OpenAPISchema {
$ref?: string;
type?: string | string[];
properties?: { [name: string]: OpenAPISchema };
patternProperties?: { [name: string]: OpenAPISchema };
additionalProperties?: boolean | OpenAPISchema;
unevaluatedProperties?: boolean | OpenAPISchema;
description?: string;
Expand Down
1 change: 1 addition & 0 deletions src/utils/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const schemaKeywordTypes = {
additionalProperties: 'object',
unevaluatedProperties: 'object',
properties: 'object',
patternProperties: 'object',
};

export function detectType(schema: OpenAPISchema): string {
Expand Down

0 comments on commit 660cc85

Please sign in to comment.