Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support conditional operators #1939

Merged
merged 10 commits into from
May 17, 2022
57 changes: 54 additions & 3 deletions demo/openapi-3-1.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,33 @@ components:
schemas:
ApiResponse:
type: object
patternProperties:
^S_\\w+\\.[1-9]{2,4}$:
description: The measured skill for hunting
if:
x-displayName: fieldName === 'status'
else:
minLength: 1
maxLength: 10
then:
format: url
type: string
enum:
- success
- failed
^O_\\w+\\.[1-9]{2,4}$:
type: object
properties:
nestedProperty:
type: [string, boolean]
description: The measured skill for hunting
default: lazy
example: adventurous
enum:
- clueless
- lazy
- adventurous
- aggressive
properties:
code:
type: integer
Expand All @@ -975,7 +1002,7 @@ components:
- type: object
properties:
huntingSkill:
type: string
type: [string, boolean]
description: The measured skill for hunting
default: lazy
example: adventurous
Expand Down Expand Up @@ -1100,14 +1127,23 @@ components:
photoUrls:
description: The list of URL to a cute photos featuring pet
type: [string, integer, 'null', array]
minItems: 1
maxItems: 20
xml:
name: photoUrl
wrapped: true
items:
type: string
format: url
if:
x-displayName: isString
type: string
then:
minItems: 1
maxItems: 10
else:
x-displayName: notString
type: [integer, 'null', array]
minItems: 1
maxItems: 20
friend:
$ref: '#/components/schemas/Pet'
tags:
Expand All @@ -1131,6 +1167,12 @@ components:
petType:
description: Type of a pet
type: string
huntingSkill:
type: [integer]
enum:
- 0
- 1
- 2
xml:
name: Pet
Tag:
Expand Down Expand Up @@ -1198,6 +1240,15 @@ components:
type: string
contentEncoding: base64
contentMediaType: image/png
if:
title: userStatus === 10
properties:
userStatus:
enum: [10]
then:
required: ['phone']
else:
required: []
xml:
name: User
requestBodies:
Expand Down
24 changes: 22 additions & 2 deletions src/components/Fields/Field.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,10 @@ import {
PropertyNameCell,
} from '../../common-elements/fields-layout';
import { ShelfIcon } from '../../common-elements/';
import styled from '../../styled-components';
import { DiscriminatorDropdown } from '../Schema';
import { Schema } from '../Schema/Schema';

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

Expand All @@ -28,6 +31,7 @@ export interface FieldProps extends SchemaOptions {
expandByDefault?: boolean;

renderDiscriminatorSwitch?: (opts: FieldProps) => JSX.Element;
renderConditionalSwitch?: (opts: FieldProps) => JSX.Element | undefined;
}

@observer
Expand All @@ -48,7 +52,7 @@ export class Field extends React.Component<FieldProps> {
};

render() {
const { className, field, isLast, expandByDefault } = this.props;
const { className = '', field, isLast, expandByDefault } = this.props;
const { name, deprecated, required, kind } = field;
const withSubSchema = !field.schema.isPrimitive && !field.schema.isCircular;

Expand Down Expand Up @@ -87,12 +91,24 @@ export class Field extends React.Component<FieldProps> {
</PropertyNameCell>
);

const renderConditionalSwitch = field.schema?.oneOf
? () =>
field.schema.conditionalEnum && (
<ConditionalWrapper>
<DiscriminatorDropdown
parent={field.schema}
enumValues={field.schema.conditionalEnum}
/>
</ConditionalWrapper>
)
: undefined;

AlexVarchuk marked this conversation as resolved.
Show resolved Hide resolved
return (
<>
<tr className={isLast ? 'last ' + className : className}>
{paramName}
<PropertyDetailsCell>
<FieldDetails {...this.props} />
<FieldDetails {...this.props} renderConditionalSwitch={renderConditionalSwitch} />
</PropertyDetailsCell>
</tr>
{expanded && withSubSchema && (
Expand All @@ -114,3 +130,7 @@ export class Field extends React.Component<FieldProps> {
);
}
}

const ConditionalWrapper = styled.div`
display: block;
`;
14 changes: 10 additions & 4 deletions src/components/Fields/FieldDetails.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react';
import { observer } from 'mobx-react';

import {
RecursiveLabel,
Expand All @@ -24,11 +25,15 @@ import { OptionsContext } from '../OptionsProvider';
import { Pattern } from './Pattern';
import { ArrayItemDetails } from './ArrayItemDetails';

function FieldDetailsComponent(props: FieldProps) {
export const FieldDetailsComponent = observer((props: FieldProps) => {
const { enumSkipQuotes, hideSchemaTitles } = React.useContext(OptionsContext);

const { showExamples, field, renderDiscriminatorSwitch } = props;
const { schema, description, deprecated, extensions, in: _in, const: _const } = field;
const { showExamples, field, renderDiscriminatorSwitch, renderConditionalSwitch } = props;
const { description, deprecated, extensions, in: _in, const: _const } = field;
const schema =
field.schema?.oneOf && renderConditionalSwitch
? field.schema.oneOf[field.schema?.activeOneOf]
: field.schema;
const isArrayType = schema.type === 'array';

const rawDefault = enumSkipQuotes || _in === 'header'; // having quotes around header field default values is confusing and inappropriate
Expand All @@ -54,6 +59,7 @@ function FieldDetailsComponent(props: FieldProps) {
return (
<div>
<div>
{(renderConditionalSwitch && renderConditionalSwitch(props)) || null}
<TypePrefix>{schema.typePrefix}</TypePrefix>
<TypeName>{schema.displayType}</TypeName>
{schema.displayFormat && (
Expand Down Expand Up @@ -107,6 +113,6 @@ function FieldDetailsComponent(props: FieldProps) {
{(_const && <FieldDetail label={l('const') + ':'} value={_const} />) || null}
</div>
);
}
});

export const FieldDetails = React.memo<FieldProps>(FieldDetailsComponent);
10 changes: 5 additions & 5 deletions src/components/JsonViewer/JsonViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,20 +27,20 @@ class Json extends React.PureComponent<JsonProps> {
}

renderInner = ({ renderCopyButton }) => {
const showFoldingButtons = this.props.data && Object.values(this.props.data).some(
(value) => typeof value === 'object' && value !== null,
);
const showFoldingButtons =
this.props.data &&
Object.values(this.props.data).some(value => typeof value === 'object' && value !== null);

return (
<JsonViewerWrap>
<SampleControls>
{renderCopyButton()}
{showFoldingButtons &&
{showFoldingButtons && (
<>
<button onClick={this.expandAll}> Expand all </button>
<button onClick={this.collapseAll}> Collapse all </button>
</>
}
)}
</SampleControls>
<OptionsContext.Consumer>
{options => (
Expand Down
14 changes: 13 additions & 1 deletion src/services/OpenAPIParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,19 @@ export class OpenAPIParser {
}

if (subSchema.type !== undefined) {
receiver.type = subSchema.type;
if (Array.isArray(subSchema.type) && Array.isArray(receiver.type)) {
receiver.type = subSchema.type.concat(...receiver.type);
AlexVarchuk marked this conversation as resolved.
Show resolved Hide resolved
} else {
receiver.type = subSchema.type;
}
}

if (subSchema.enum !== undefined) {
if (Array.isArray(subSchema.enum) && Array.isArray(receiver.enum)) {
receiver.enum = subSchema.enum.concat(...receiver.enum);
AlexVarchuk marked this conversation as resolved.
Show resolved Hide resolved
} else {
receiver.enum = subSchema.enum;
}
}

if (subSchema.properties !== undefined) {
Expand Down
40 changes: 40 additions & 0 deletions src/services/__tests__/fixtures/3.1/conditionalField.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"openapi": "3.1.0",
"info": {
"title": "Schema definition field with conditional operators",
"version": "1.0.0"
},
"components": {
"schemas": {
"Test": {
"type": "object",
"properties": {
"test": {
"type": ["string", "integer", "null", "array"],
"minItems": 1,
"maxItems": 20,
"items": {
"type": "string",
"format": "url"
},
"if": {
"x-displayName": "isString",
"type": "string"
},
"then": {
"type": "string",
"minItems": 1,
"maxItems": 20
},
"else": {
"x-displayName": "notString",
"minItems": 1,
"maxItems": 10,
"pattern": "\\d+"
}
}
}
}
}
}
}
40 changes: 40 additions & 0 deletions src/services/__tests__/fixtures/3.1/conditionalSchema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"openapi": "3.1.0",
"info": {
"title": "Schema definition with conditional operators",
"version": "1.0.0"
},
"components": {
"schemas": {
"Test": {
"type": "object",
"properties": {
"test": {
"description": "The list of URL to a cute photos featuring pet",
"type": ["string", "integer", "null", "array"],
"minItems": 1,
"maxItems": 20,
"items": {
"type": "string",
"format": "url"
}
}
},
"if": {
"title": "=== 10",
"properties": {
"test": {
"enum": [10]
}
}
},
"then": {
"maxItems": 2
},
"else": {
"maxItems": 20
}
}
}
}
}
27 changes: 27 additions & 0 deletions src/services/__tests__/models/Schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,33 @@ describe('Models', () => {
expect(schema.pointer).toBe('#/components/schemas/Child');
});

test('schemaDefinition should resolve schema with conditional operators', () => {
const spec = require('../fixtures/3.1/conditionalSchema.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts);
expect(schema.conditionalEnum).toHaveLength(2);
expect(schema.oneOf).toHaveLength(2);

expect(schema.oneOf![0].schema.title).toBe('=== 10');
expect(schema.oneOf![1].schema.title).toBe('else');

expect(schema.oneOf![0].schema).toMatchSnapshot();
expect(schema.oneOf![1].schema).toMatchSnapshot();
});

test('schemaDefinition should resolve field with conditional operators', () => {
const spec = require('../fixtures/3.1/conditionalField.json');
parser = new OpenAPIParser(spec, undefined, opts);
const schema = new SchemaModel(parser, spec.components.schemas.Test, '', opts);
expect(schema.fields).toHaveLength(1);
expect(schema.fields && schema.fields[0].schema.oneOf).toHaveLength(2);
expect(schema.fields && schema.fields[0].schema.oneOf![0].schema.title).toBe('isString');
expect(schema.fields && schema.fields[0].schema.oneOf![1].schema.title).toBe('notString');

expect(schema.fields && schema.fields[0].schema.oneOf![0].schema).toMatchSnapshot();
expect(schema.fields && schema.fields[0].schema.oneOf![1].schema).toMatchSnapshot();
});

test('schemaDefinition should resolve unevaluatedProperties in properties', () => {
const spec = require('../fixtures/3.1/unevaluatedProperties.json');
parser = new OpenAPIParser(spec, undefined, opts);
Expand Down
Loading