Skip to content

Commit b1fad8c

Browse files
authored
Merge pull request temando#76 from brendo/issue-13-render-constraints
Support constraints!
2 parents 1433699 + b61cb12 commit b1fad8c

File tree

25 files changed

+915
-49
lines changed

25 files changed

+915
-49
lines changed

docs/schema.md

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Schema support
2+
3+
The `Schema` object in [Open API v3 spec][oaschema] describes several properties
4+
that are shared from JSON Schema, deviations from JSON Schema, or in addition
5+
to JSON Schema. The document descibes this project's support for these
6+
properties.
7+
8+
## Properties
9+
10+
The following properties are supported, and implemented according to the
11+
[JSON Schema Validation spec][jsschema]:
12+
13+
- [x] multipleOf
14+
- [x] maximum
15+
- [x] exclusiveMaximum
16+
- [x] minimum
17+
- [x] exclusiveMinimum
18+
- [x] maxLength
19+
- [x] minLength
20+
- [x] pattern
21+
- [x] maxItems
22+
- [x] minItems
23+
- [x] uniqueItems
24+
- [x] maxProperties
25+
- [x] minProperties
26+
- [x] format
27+
- [x] required
28+
- [x] enum
29+
30+
## Adjusted JSON Schema Properties
31+
32+
The OpenAPI specification also supports several additional properties from JSON
33+
Schema, with some adjustments. This project attempts to honor these adjustments,
34+
with any exceptions outlined below:
35+
36+
- [x] type - Value may be an array, multiple types are supported.
37+
- [x] allOf
38+
- [ ] oneOf
39+
- [ ] anyOf
40+
- [ ] not
41+
- [x] items
42+
- [x] properties
43+
- [ ] additionalProperties
44+
- [x] description
45+
- [x] format
46+
- [x] default
47+
48+
## Fixed Fields
49+
50+
At this time, the project supports no [fixed fields][ff].
51+
52+
[ff]: https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#fixed-fields-20
53+
[jsschema]: http://json-schema.org/latest/json-schema-validation.html#rfc.section.6
54+
[oaschema]: https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#schemaObject

src/components/BodySchema/BodySchema.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,20 @@ export default class BodySchema extends Component {
3838
if (properties.length === iterator) {
3939
isLast = true;
4040
}
41-
if (property.type === 'array' && expandedProp.indexOf(property.name) !== -1 && property.properties !== undefined) {
41+
42+
if (property.type.includes('array') && expandedProp.includes(property.name) && property.properties !== undefined) {
4243
return createFragment({
4344
property: this.renderPropertyRow(property, isLast, true),
4445
subset: this.renderSubsetProperties(property, true)
4546
});
46-
} else if (property.type === 'array' && property.properties !== undefined) {
47+
} else if (property.type.includes('array') && property.properties !== undefined) {
4748
return this.renderPropertyRow(property, isLast, false);
48-
} else if (property.type === 'object' && expandedProp.indexOf(property.name) !== -1 && property.properties !== undefined) {
49+
} else if (property.type.includes('object') && expandedProp.includes(property.name) && property.properties !== undefined) {
4950
return createFragment({
5051
property: this.renderPropertyRow(property, isLast, true),
5152
subset: this.renderSubsetProperties(property)
5253
});
53-
} else if (property.type === 'object' && property.properties !== undefined) {
54+
} else if (property.type.includes('object') && property.properties !== undefined) {
5455
return this.renderPropertyRow(property, isLast, false);
5556
} else {
5657
return this.renderPropertyRow(property, isLast);
@@ -71,6 +72,7 @@ export default class BodySchema extends Component {
7172
description={property.description}
7273
enumValues={property.enum}
7374
defaultValue={property.defaultValue}
75+
constraints={property.constraints}
7476
onClick={this.onClick.bind(this, property.name)}
7577
isRequired={property.required}
7678
isOpen={isOpen}
@@ -110,7 +112,7 @@ export default class BodySchema extends Component {
110112

111113
onClick(propertyName) {
112114
const { expandedProp } = this.state;
113-
if (expandedProp.indexOf(propertyName) !== -1) {
115+
if (expandedProp.includes(propertyName)) {
114116
const newExpanded = expandedProp.filter((prop) => prop !== propertyName);
115117
this.setState({ expandedProp: newExpanded });
116118
} else {

src/components/Method/Method.js

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,7 @@ export default class Method extends Component {
7575
return (
7676
<div className="method-responses">
7777
<h4>Responses</h4>
78-
{responses.map((response) => {
79-
return (
80-
<Response
81-
key={response.code}
82-
response={response}
83-
/>
84-
);
85-
})}
78+
{responses.map((r) => <Response key={r.code} response={r} />)}
8679
</div>
8780
);
8881
}

src/components/Property/Property.js

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@ import PropTypes from 'prop-types';
44

55
import Description from '../Description/Description';
66
import Indicator from '../Indicator/Indicator';
7+
import PropertyConstraints from '../PropertyConstraints/PropertyConstraints';
78

89
import './Property.scss';
910

1011
export default class Property extends Component {
1112
render() {
12-
const { name, type, description, isRequired, enumValues, defaultValue, onClick, isOpen, isLast } = this.props;
13+
const {
14+
name, type, description, constraints, isRequired, enumValues, defaultValue, onClick, isOpen, isLast
15+
} = this.props;
1316

1417
let subtype;
1518
if (type.includes('array')) {
@@ -20,6 +23,7 @@ export default class Property extends Component {
2023
if (isOpen !== undefined) {
2124
isClickable = true;
2225
}
26+
2327
let status;
2428
if (isOpen) {
2529
status = 'open';
@@ -36,15 +40,17 @@ export default class Property extends Component {
3640
'property--isclickable': isClickable
3741
})}>
3842
<span>{name}</span>
39-
{isClickable &&
40-
<Indicator className="property-indicator" status={status}/>
41-
}
43+
{isClickable && <Indicator className="property-indicator" status={status}/>}
4244
</td>
4345
<td className="property-info">
44-
<span>{type.join(', ')}</span>{subtype && <span> of {subtype}</span>}
45-
{isRequired && <span className="property-required">Required</span>}
46+
<span className="property-type">
47+
{!subtype ? type.join(', ') : <span className="property-subtype">{subtype}[]</span>}
48+
{!subtype && constraints && constraints.format &&
49+
<span className="property-format">&lt;{constraints.format}&gt;</span>}
50+
</span>
51+
<PropertyConstraints constraints={constraints} type={type} isRequired={isRequired} />
4652
{enumValues && this.renderEnumValues(enumValues)}
47-
{defaultValue && this.renderDefaultValue(defaultValue)}
53+
{defaultValue !== undefined && this.renderDefaultValue(defaultValue)}
4854
{description && <Description description={description}/>}
4955
</td>
5056
</tr>
@@ -81,9 +87,8 @@ export default class Property extends Component {
8187
}
8288

8389
return (
84-
<div>
85-
<span>Default: </span>
86-
<span className="default">{displayValue}</span>
90+
<div className="default">
91+
Default: <span>{displayValue}</span>
8792
</div>
8893
);
8994
}
@@ -93,7 +98,24 @@ Property.propTypes = {
9398
name: PropTypes.string.isRequired,
9499
type: PropTypes.arrayOf(PropTypes.string).isRequired,
95100
subtype: PropTypes.string,
101+
title: PropTypes.string,
96102
description: PropTypes.string,
103+
constraints: PropTypes.shape({
104+
format: PropTypes.string,
105+
exclusiveMinimum: PropTypes.number,
106+
exclusiveMaximum: PropTypes.number,
107+
maximum: PropTypes.number,
108+
maxItems: PropTypes.number,
109+
maxLength: PropTypes.number,
110+
maxProperties: PropTypes.number,
111+
minimum: PropTypes.number,
112+
minItems: PropTypes.number,
113+
minLength: PropTypes.number,
114+
minProperties: PropTypes.number,
115+
multipleOf: PropTypes.number,
116+
pattern: PropTypes.string,
117+
uniqueItems: PropTypes.bool
118+
}),
97119
enumValues: PropTypes.array,
98120
defaultValue: PropTypes.any,
99121
isRequired: PropTypes.bool,
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import React, { PureComponent } from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import { getConstraintHints as getArrayHints } from './../../parser/open-api/constraints/array';
5+
import { getConstraintHints as getNumericHints } from './../../parser/open-api/constraints/numeric';
6+
import { getConstraintHints as getObjectHints } from './../../parser/open-api/constraints/object';
7+
import { getConstraintHints as getStringHints } from './../../parser/open-api/constraints/string';
8+
9+
export default class PropertyConstraints extends PureComponent {
10+
render() {
11+
const { type, isRequired, constraints } = this.props;
12+
13+
return (
14+
<span className="property-constraints">
15+
{isRequired && <span className="property-required">required</span>}
16+
{constraints && ['number', 'integer'].some(t => type.includes(t)) && this.renderConstraints(constraints, 'numeric')}
17+
{constraints && type.includes('string') && this.renderConstraints(constraints, 'string')}
18+
{constraints && type.includes('array') && this.renderConstraints(constraints, 'array')}
19+
{constraints && type.includes('object') && this.renderConstraints(constraints, 'object')}
20+
</span>
21+
);
22+
}
23+
24+
/**
25+
* Renders validation hints for the given constraints and type.
26+
*
27+
* @param {object} constraints
28+
* @param {string} type
29+
*/
30+
renderConstraints(constraints, type) {
31+
let validations = [];
32+
33+
switch (type) {
34+
case 'numeric':
35+
validations = getNumericHints(constraints);
36+
break;
37+
case 'object':
38+
validations = getObjectHints(constraints);
39+
break;
40+
case 'array':
41+
validations = getArrayHints(constraints);
42+
break;
43+
case 'string':
44+
default:
45+
validations = getStringHints(constraints);
46+
}
47+
48+
if (!validations.length) {
49+
return null;
50+
}
51+
52+
return (
53+
<span>
54+
{validations.map(constraint =>
55+
<span key={constraint} className={`${type}-constraints`}>{constraint}</span>
56+
)}
57+
</span>
58+
);
59+
}
60+
}
61+
62+
PropertyConstraints.propTypes = {
63+
type: PropTypes.arrayOf(PropTypes.string).isRequired,
64+
isRequired: PropTypes.bool.isRequired,
65+
constraints: PropTypes.shape({
66+
format: PropTypes.string,
67+
exclusiveMinimum: PropTypes.number,
68+
exclusiveMaximum: PropTypes.number,
69+
maximum: PropTypes.number,
70+
maxItems: PropTypes.number,
71+
maxLength: PropTypes.number,
72+
maxProperties: PropTypes.number,
73+
minimum: PropTypes.number,
74+
minItems: PropTypes.number,
75+
minLength: PropTypes.number,
76+
minProperties: PropTypes.number,
77+
multipleOf: PropTypes.number,
78+
pattern: PropTypes.string,
79+
uniqueItems: PropTypes.bool
80+
})
81+
};
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* Returns an array of hints that relate to the constraints for an array,
3+
* `maxItems`, `minItems` and `uniqueItems`.
4+
*
5+
* @param {object} constraints
6+
* @return {array}
7+
*/
8+
export function getConstraintHints(constraints) {
9+
if (!constraints) {
10+
return [];
11+
}
12+
13+
const { maxItems, minItems, uniqueItems } = constraints;
14+
const validations = [];
15+
16+
if (uniqueItems) {
17+
validations.push('unique items');
18+
}
19+
20+
if (maxItems !== undefined && minItems !== undefined) {
21+
// Be succint if the minItems is the same maxItems
22+
// ie. value can only be of `x` length.
23+
if (maxItems === minItems) {
24+
validations.push(`${minItems} items`);
25+
} else {
26+
validations.push(`${minItems}-${maxItems} items`);
27+
}
28+
} else if (minItems !== undefined) {
29+
validations.push(`at least ${minItems} items`);
30+
} else if (maxItems !== undefined) {
31+
validations.push(`at most ${maxItems} items`);
32+
}
33+
34+
return validations;
35+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// https://github.com/OAI/OpenAPI-Specification/blob/OpenAPI.next/versions/3.0.md#schema-object
2+
export const VALIDATION_KEYWORDS = [
3+
'format',
4+
'exclusiveMaximum',
5+
'exclusiveMinimum',
6+
'maximum',
7+
'maxItems',
8+
'maxLength',
9+
'maxProperties',
10+
'minimum',
11+
'minItems',
12+
'minLength',
13+
'minProperties',
14+
'multipleOf',
15+
'pattern',
16+
'uniqueItems'
17+
];
18+
19+
/**
20+
* Determines if the given property contains any validation keywords
21+
*
22+
* @param {Object} property
23+
* @return {Boolean}
24+
*/
25+
export function hasConstraints(property) {
26+
return Object.keys(property).some(
27+
(key) => VALIDATION_KEYWORDS.includes(key)
28+
);
29+
}
30+
31+
/**
32+
* Given a property, extract all the constraints from it and return a new
33+
* object with those constraints.
34+
*
35+
* @param {Object} property
36+
* @return {Object}
37+
*/
38+
export function getConstraints(property) {
39+
return Object.keys(property).reduce((constraints, key) => {
40+
if (VALIDATION_KEYWORDS.includes(key)) {
41+
constraints[key] = property[key];
42+
}
43+
44+
return constraints;
45+
}, {});
46+
}

0 commit comments

Comments
 (0)