mirror of
https://github.com/Redocly/redoc.git
synced 2024-11-27 11:03:43 +03:00
fix: exclusiveMin/Max shows incorect range (#1799)
* fix: exclusiveMin/Max shows incorect range * cover all number range cases & add unit tests * add more tests * fix maximum value * simplify humanizeNumberRange function * simplify exclusive checks * Update src/utils/openapi.ts Co-authored-by: Roman Hotsiy <gotsijroman@gmail.com> * update test coverage * linting * revert weird prettier changes * add md files to prettier ignore Co-authored-by: Roman Hotsiy <gotsijroman@gmail.com>
This commit is contained in:
parent
4fb5f914fa
commit
b604bd8da8
1
.prettierignore
Normal file
1
.prettierignore
Normal file
|
@ -0,0 +1 @@
|
||||||
|
*.md
|
|
@ -1,11 +1,10 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
<head>
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<title>%PAGE_TITLE%</title>
|
<title>%PAGE_TITLE%</title>
|
||||||
<link rel="icon" href="%PAGE_FAVICON%">
|
<link rel="icon" href="%PAGE_FAVICON%" />
|
||||||
<style>
|
<style>
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
@ -16,12 +15,14 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
<link
|
||||||
</head>
|
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<redoc spec-url="%SPEC_URL%" %REDOC_OPTIONS%></redoc>
|
<redoc spec-url="%SPEC_URL%" %REDOC_OPTIONS%></redoc>
|
||||||
<script src="redoc.standalone.js"></script>
|
<script src="redoc.standalone.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
|
@ -29,6 +29,7 @@ The following options are supported:
|
||||||
### OpenAPI definition
|
### OpenAPI definition
|
||||||
|
|
||||||
You will need an OpenAPI definition. For testing purposes, you can use one of the following sample OpenAPI definitions:
|
You will need an OpenAPI definition. For testing purposes, you can use one of the following sample OpenAPI definitions:
|
||||||
|
|
||||||
- OpenAPI 3.0
|
- OpenAPI 3.0
|
||||||
- [Rebilly Users OpenAPI Definition](https://raw.githubusercontent.com/Rebilly/api-definitions/main/openapi/users.yaml)
|
- [Rebilly Users OpenAPI Definition](https://raw.githubusercontent.com/Rebilly/api-definitions/main/openapi/users.yaml)
|
||||||
- [Swagger Petstore Sample OpenAPI Definition](https://petstore3.swagger.io/api/v3/openapi.json)
|
- [Swagger Petstore Sample OpenAPI Definition](https://petstore3.swagger.io/api/v3/openapi.json)
|
||||||
|
@ -36,7 +37,6 @@ You will need an OpenAPI definition. For testing purposes, you can use one of th
|
||||||
- [Thingful OpenAPI Definition](https://raw.githubusercontent.com/thingful/openapi-spec/master/spec/swagger.yaml)
|
- [Thingful OpenAPI Definition](https://raw.githubusercontent.com/thingful/openapi-spec/master/spec/swagger.yaml)
|
||||||
- [Fitbit Plus OpenAPI Definition](https://raw.githubusercontent.com/TwineHealth/TwineDeveloperDocs/master/spec/swagger.yaml)
|
- [Fitbit Plus OpenAPI Definition](https://raw.githubusercontent.com/TwineHealth/TwineDeveloperDocs/master/spec/swagger.yaml)
|
||||||
|
|
||||||
|
|
||||||
:::info OpenAPI specification
|
:::info OpenAPI specification
|
||||||
For more information on the OpenAPI specification, refer to the [Learning OpenAPI 3](https://redoc.ly/docs/resources/learning-openapi/)
|
For more information on the OpenAPI specification, refer to the [Learning OpenAPI 3](https://redoc.ly/docs/resources/learning-openapi/)
|
||||||
section in the documentation.
|
section in the documentation.
|
||||||
|
|
|
@ -13,9 +13,12 @@ replace the `spec-url` attribute with the URL or local file address to your defi
|
||||||
<head>
|
<head>
|
||||||
<title>Redoc</title>
|
<title>Redoc</title>
|
||||||
<!-- needed for adaptive design -->
|
<!-- needed for adaptive design -->
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Redoc doesn't change outer page styles
|
Redoc doesn't change outer page styles
|
||||||
|
@ -31,14 +34,13 @@ replace the `spec-url` attribute with the URL or local file address to your defi
|
||||||
<!--
|
<!--
|
||||||
Redoc element with link to your OpenAPI definition
|
Redoc element with link to your OpenAPI definition
|
||||||
-->
|
-->
|
||||||
<redoc spec-url='http://petstore.swagger.io/v2/swagger.json'></redoc>
|
<redoc spec-url="http://petstore.swagger.io/v2/swagger.json"></redoc>
|
||||||
<!--
|
<!--
|
||||||
Link to Redoc JavaScript on CDN for rendering standalone element
|
Link to Redoc JavaScript on CDN for rendering standalone element
|
||||||
-->
|
-->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"> </script>
|
<script src="https://cdn.jsdelivr.net/npm/redoc@latest/bundles/redoc.standalone.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
:::attention Running Redoc locally requires an HTTP server
|
:::attention Running Redoc locally requires an HTTP server
|
||||||
|
|
|
@ -28,16 +28,16 @@ export const StyledDropdown = styled(Dropdown)`
|
||||||
width: auto;
|
width: auto;
|
||||||
background: white;
|
background: white;
|
||||||
color: #263238;
|
color: #263238;
|
||||||
font-family: ${(props) => props.theme.typography.headings.fontFamily};
|
font-family: ${props => props.theme.typography.headings.fontFamily};
|
||||||
font-size: 0.929em;
|
font-size: 0.929em;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: border 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
|
transition: border 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
|
||||||
&:hover,
|
&:hover,
|
||||||
&:focus-within {
|
&:focus-within {
|
||||||
border: 1px solid ${(props) => props.theme.colors.primary.main};
|
border: 1px solid ${props => props.theme.colors.primary.main};
|
||||||
color: ${(props) => props.theme.colors.primary.main};
|
color: ${props => props.theme.colors.primary.main};
|
||||||
box-shadow: 0px 0px 0px 1px ${(props) => props.theme.colors.primary.main};
|
box-shadow: 0px 0px 0px 1px ${props => props.theme.colors.primary.main};
|
||||||
}
|
}
|
||||||
.dropdown-selector {
|
.dropdown-selector {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
@ -48,7 +48,7 @@ export const StyledDropdown = styled(Dropdown)`
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
.dropdown-selector-value {
|
.dropdown-selector-value {
|
||||||
font-family: ${(props) => props.theme.typography.headings.fontFamily};
|
font-family: ${props => props.theme.typography.headings.fontFamily};
|
||||||
position: relative;
|
position: relative;
|
||||||
font-size: 0.929em;
|
font-size: 0.929em;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -63,7 +63,7 @@ export const StyledDropdown = styled(Dropdown)`
|
||||||
right: 3px;
|
right: 3px;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
border-color: ${(props) => props.theme.colors.primary.main} transparent transparent;
|
border-color: ${props => props.theme.colors.primary.main} transparent transparent;
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
border-width: 0.35em 0.35em 0;
|
border-width: 0.35em 0.35em 0;
|
||||||
width: 0;
|
width: 0;
|
||||||
|
@ -128,8 +128,8 @@ export const SimpleDropdown = styled(StyledDropdown)`
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
.dropdown-selector-value {
|
.dropdown-selector-value {
|
||||||
color: ${(props) => props.theme.colors.primary.main};
|
color: ${props => props.theme.colors.primary.main};
|
||||||
text-shadow: 0px 0px 0px ${(props) => props.theme.colors.primary.main};
|
text-shadow: 0px 0px 0px ${props => props.theme.colors.primary.main};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -48,10 +48,10 @@ const CallbackTitleWrapper = styled.button`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CallbackName = styled.span<{ deprecated?: boolean }>`
|
const CallbackName = styled.span<{ deprecated?: boolean }>`
|
||||||
text-decoration: ${(props) => (props.deprecated ? 'line-through' : 'none')};
|
text-decoration: ${props => (props.deprecated ? 'line-through' : 'none')};
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const OperationBadgeStyled = styled(OperationBadge)`
|
const OperationBadgeStyled = styled(OperationBadge)`
|
||||||
margin: 0px 5px 0px 0px;
|
margin: 0 5px 0 0;
|
||||||
`;
|
`;
|
||||||
|
|
|
@ -34,11 +34,11 @@ class Json extends React.PureComponent<JsonProps> {
|
||||||
<button onClick={this.collapseAll}> Collapse all </button>
|
<button onClick={this.collapseAll}> Collapse all </button>
|
||||||
</SampleControls>
|
</SampleControls>
|
||||||
<OptionsContext.Consumer>
|
<OptionsContext.Consumer>
|
||||||
{(options) => (
|
{options => (
|
||||||
<PrismDiv
|
<PrismDiv
|
||||||
className={this.props.className}
|
className={this.props.className}
|
||||||
// tslint:disable-next-line
|
// tslint:disable-next-line
|
||||||
ref={(node) => (this.node = node!)}
|
ref={node => (this.node = node!)}
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
|
__html: jsonToHTML(this.props.data, options.jsonSampleExpandLevel),
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -6,8 +6,8 @@ export const jsonStyles = css`
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
font-family: ${(props) => props.theme.typography.code.fontFamily};
|
font-family: ${props => props.theme.typography.code.fontFamily};
|
||||||
font-size: ${(props) => props.theme.typography.code.fontSize};
|
font-size: ${props => props.theme.typography.code.fontSize};
|
||||||
|
|
||||||
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
|
white-space: ${({ theme }) => (theme.typography.code.wrap ? 'pre-wrap' : 'pre')};
|
||||||
contain: content;
|
contain: content;
|
||||||
|
@ -51,8 +51,8 @@ export const jsonStyles = css`
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: ${(props) => props.theme.typography.code.fontFamily};
|
font-family: ${props => props.theme.typography.code.fontFamily};
|
||||||
font-size: ${(props) => props.theme.typography.code.fontSize};
|
font-size: ${props => props.theme.typography.code.fontSize};
|
||||||
padding-right: 6px;
|
padding-right: 6px;
|
||||||
padding-left: 6px;
|
padding-left: 6px;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
|
|
|
@ -42,7 +42,7 @@ export class Operation extends React.Component<OperationProps> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<OptionsContext.Consumer>
|
<OptionsContext.Consumer>
|
||||||
{(options) => (
|
{options => (
|
||||||
<OperationRow>
|
<OperationRow>
|
||||||
<MiddlePanel>
|
<MiddlePanel>
|
||||||
<H2>
|
<H2>
|
||||||
|
|
|
@ -14,13 +14,13 @@ export const StyledResponseTitle = styled(ResponseTitle)`
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
color: ${(props) => props.theme.colors.responses[props.type].color};
|
color: ${props => props.theme.colors.responses[props.type].color};
|
||||||
background-color: ${(props) => props.theme.colors.responses[props.type].backgroundColor};
|
background-color: ${props => props.theme.colors.responses[props.type].backgroundColor};
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: auto;
|
outline: auto;
|
||||||
outline-color: ${(props) => props.theme.colors.responses[props.type].color};
|
outline-color: ${props => props.theme.colors.responses[props.type].color};
|
||||||
}
|
}
|
||||||
${(props) =>
|
${props =>
|
||||||
(props.empty &&
|
(props.empty &&
|
||||||
`
|
`
|
||||||
cursor: default;
|
cursor: default;
|
||||||
|
|
|
@ -7,8 +7,8 @@ import { SecurityRequirementModel } from '../../services/models/SecurityRequirem
|
||||||
import { linksCss } from '../Markdown/styled.elements';
|
import { linksCss } from '../Markdown/styled.elements';
|
||||||
|
|
||||||
const ScopeName = styled.code`
|
const ScopeName = styled.code`
|
||||||
font-size: ${(props) => props.theme.typography.code.fontSize};
|
font-size: ${props => props.theme.typography.code.fontSize};
|
||||||
font-family: ${(props) => props.theme.typography.code.fontFamily};
|
font-family: ${props => props.theme.typography.code.fontFamily};
|
||||||
border: 1px solid ${({ theme }) => theme.colors.border.dark};
|
border: 1px solid ${({ theme }) => theme.colors.border.dark};
|
||||||
margin: 0 3px;
|
margin: 0 3px;
|
||||||
padding: 0.2em;
|
padding: 0.2em;
|
||||||
|
@ -67,12 +67,12 @@ export class SecurityRequirement extends React.PureComponent<SecurityRequirement
|
||||||
return (
|
return (
|
||||||
<SecurityRequirementOrWrap>
|
<SecurityRequirementOrWrap>
|
||||||
{security.schemes.length ? (
|
{security.schemes.length ? (
|
||||||
security.schemes.map((scheme) => {
|
security.schemes.map(scheme => {
|
||||||
return (
|
return (
|
||||||
<SecurityRequirementAndWrap key={scheme.id}>
|
<SecurityRequirementAndWrap key={scheme.id}>
|
||||||
<Link to={scheme.sectionId}>{scheme.id}</Link>
|
<Link to={scheme.sectionId}>{scheme.id}</Link>
|
||||||
{scheme.scopes.length > 0 && ' ('}
|
{scheme.scopes.length > 0 && ' ('}
|
||||||
{scheme.scopes.map((scope) => (
|
{scheme.scopes.map(scope => (
|
||||||
<ScopeName key={scope}>{scope}</ScopeName>
|
<ScopeName key={scope}>{scope}</ScopeName>
|
||||||
))}
|
))}
|
||||||
{scheme.scopes.length > 0 && ') '}
|
{scheme.scopes.length > 0 && ') '}
|
||||||
|
@ -92,7 +92,7 @@ const AuthHeaderColumn = styled.div`
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const SecuritiesColumn = styled.div`
|
const SecuritiesColumn = styled.div`
|
||||||
width: ${(props) => props.theme.schema.defaultDetailsWidth};
|
width: ${props => props.theme.schema.defaultDetailsWidth};
|
||||||
${media.lessThan('small')`
|
${media.lessThan('small')`
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
`}
|
`}
|
||||||
|
|
|
@ -72,7 +72,7 @@ export class RedocNormalizedOptions {
|
||||||
}
|
}
|
||||||
if (typeof value === 'string') {
|
if (typeof value === 'string') {
|
||||||
const res = {};
|
const res = {};
|
||||||
value.split(',').forEach((code) => {
|
value.split(',').forEach(code => {
|
||||||
res[code.trim()] = true;
|
res[code.trim()] = true;
|
||||||
});
|
});
|
||||||
return res;
|
return res;
|
||||||
|
@ -138,7 +138,7 @@ export class RedocNormalizedOptions {
|
||||||
case 'false':
|
case 'false':
|
||||||
return false;
|
return false;
|
||||||
default:
|
default:
|
||||||
return value.split(',').map((ext) => ext.trim());
|
return value.split(',').map(ext => ext.trim());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -266,7 +266,7 @@ export class RedocNormalizedOptions {
|
||||||
this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues);
|
this.maxDisplayedEnumValues = argValueToNumber(raw.maxDisplayedEnumValues);
|
||||||
const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas)
|
const ignoreNamedSchemas = Array.isArray(raw.ignoreNamedSchemas)
|
||||||
? raw.ignoreNamedSchemas
|
? raw.ignoreNamedSchemas
|
||||||
: raw.ignoreNamedSchemas?.split(',').map((s) => s.trim());
|
: raw.ignoreNamedSchemas?.split(',').map(s => s.trim());
|
||||||
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
|
this.ignoreNamedSchemas = new Set(ignoreNamedSchemas);
|
||||||
this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern);
|
this.hideSchemaPattern = argValueToBoolean(raw.hideSchemaPattern);
|
||||||
this.generatedPayloadSamplesMaxDepth =
|
this.generatedPayloadSamplesMaxDepth =
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
pluralizeType,
|
pluralizeType,
|
||||||
serializeParameterValue,
|
serializeParameterValue,
|
||||||
sortByRequired,
|
sortByRequired,
|
||||||
|
humanizeNumberRange,
|
||||||
} from '../';
|
} from '../';
|
||||||
|
|
||||||
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
|
import { FieldModel, OpenAPIParser, RedocNormalizedOptions } from '../../services';
|
||||||
|
@ -410,6 +411,76 @@ describe('Utils', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('openapi humanizeNumberRange', () => {
|
||||||
|
it('should return `>=` when only minimum value present or exclusiveMinimum = false', () => {
|
||||||
|
const expected = '>= 0';
|
||||||
|
expect(humanizeNumberRange({ minimum: 0 })).toEqual(expected);
|
||||||
|
expect(humanizeNumberRange({ minimum: 0, exclusiveMinimum: false })).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `>` when minimum value present and exclusiveMinimum set to true', () => {
|
||||||
|
expect(humanizeNumberRange({ minimum: 0, exclusiveMinimum: true })).toEqual('> 0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `<=` when only maximum value present or exclusiveMinimum = false', () => {
|
||||||
|
const expected = '<= 10';
|
||||||
|
expect(humanizeNumberRange({ maximum: 10 })).toEqual(expected);
|
||||||
|
expect(humanizeNumberRange({ maximum: 10, exclusiveMaximum: false })).toEqual(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return `<` when maximum value present and exclusiveMaximum set to true', () => {
|
||||||
|
expect(humanizeNumberRange({ maximum: 10, exclusiveMaximum: true })).toEqual('< 10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct range for minimum and maximum values and with different exclusive set', () => {
|
||||||
|
expect(humanizeNumberRange({ minimum: 0, maximum: 10 })).toEqual('[ 0 .. 10 ]');
|
||||||
|
expect(
|
||||||
|
humanizeNumberRange({
|
||||||
|
minimum: 0,
|
||||||
|
exclusiveMinimum: true,
|
||||||
|
maximum: 10,
|
||||||
|
exclusiveMaximum: true,
|
||||||
|
}),
|
||||||
|
).toEqual('( 0 .. 10 )');
|
||||||
|
expect(
|
||||||
|
humanizeNumberRange({
|
||||||
|
minimum: 0,
|
||||||
|
maximum: 10,
|
||||||
|
exclusiveMaximum: true,
|
||||||
|
}),
|
||||||
|
).toEqual('[ 0 .. 10 )');
|
||||||
|
expect(
|
||||||
|
humanizeNumberRange({
|
||||||
|
minimum: 0,
|
||||||
|
exclusiveMinimum: true,
|
||||||
|
maximum: 10,
|
||||||
|
}),
|
||||||
|
).toEqual('( 0 .. 10 ]');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct range exclusive values only', () => {
|
||||||
|
expect(humanizeNumberRange({ exclusiveMinimum: 0 })).toEqual('> 0');
|
||||||
|
expect(humanizeNumberRange({ exclusiveMaximum: 10 })).toEqual('< 10');
|
||||||
|
expect(humanizeNumberRange({ exclusiveMinimum: 0, exclusiveMaximum: 10 })).toEqual(
|
||||||
|
'( 0 .. 10 )',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct min value', () => {
|
||||||
|
expect(humanizeNumberRange({ minimum: 5, exclusiveMinimum: 10 })).toEqual('> 5');
|
||||||
|
expect(humanizeNumberRange({ minimum: -5, exclusiveMinimum: -10 })).toEqual('> -10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return correct max value', () => {
|
||||||
|
expect(humanizeNumberRange({ maximum: 10, exclusiveMaximum: 15 })).toEqual('< 15');
|
||||||
|
expect(humanizeNumberRange({ maximum: -10, exclusiveMaximum: -15 })).toEqual('< -10');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined', () => {
|
||||||
|
expect(humanizeNumberRange({})).toEqual(undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('openapi humanizeConstraints', () => {
|
describe('openapi humanizeConstraints', () => {
|
||||||
const itemConstraintSchema = (
|
const itemConstraintSchema = (
|
||||||
min?: number,
|
min?: number,
|
||||||
|
|
|
@ -419,6 +419,29 @@ function humanizeRangeConstraint(
|
||||||
return stringRange;
|
return stringRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function humanizeNumberRange(schema: OpenAPISchema): string | undefined {
|
||||||
|
const minimum =
|
||||||
|
typeof schema.exclusiveMinimum === 'number'
|
||||||
|
? Math.min(schema.exclusiveMinimum, schema.minimum ?? Infinity)
|
||||||
|
: schema.minimum;
|
||||||
|
const maximum =
|
||||||
|
typeof schema.exclusiveMaximum === 'number'
|
||||||
|
? Math.max(schema.exclusiveMaximum, schema.maximum ?? -Infinity)
|
||||||
|
: schema.maximum;
|
||||||
|
const exclusiveMinimum = typeof schema.exclusiveMinimum === 'number' || schema.exclusiveMinimum;
|
||||||
|
const exclusiveMaximum = typeof schema.exclusiveMaximum === 'number' || schema.exclusiveMaximum;
|
||||||
|
|
||||||
|
if (minimum !== undefined && maximum !== undefined) {
|
||||||
|
return `${exclusiveMinimum ? '( ' : '[ '}${minimum} .. ${maximum}${
|
||||||
|
exclusiveMaximum ? ' )' : ' ]'
|
||||||
|
}`;
|
||||||
|
} else if (maximum !== undefined) {
|
||||||
|
return `${exclusiveMaximum ? '< ' : '<= '}${maximum}`;
|
||||||
|
} else if (minimum !== undefined) {
|
||||||
|
return `${exclusiveMinimum ? '> ' : '>= '}${minimum}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function humanizeConstraints(schema: OpenAPISchema): string[] {
|
export function humanizeConstraints(schema: OpenAPISchema): string[] {
|
||||||
const res: string[] = [];
|
const res: string[] = [];
|
||||||
|
|
||||||
|
@ -437,35 +460,7 @@ export function humanizeConstraints(schema: OpenAPISchema): string[] {
|
||||||
res.push(multipleOfConstraint);
|
res.push(multipleOfConstraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
let numberRange;
|
const numberRange = humanizeNumberRange(schema);
|
||||||
if (schema.minimum !== undefined && schema.maximum !== undefined) {
|
|
||||||
numberRange = schema.exclusiveMinimum ? '( ' : '[ ';
|
|
||||||
numberRange += schema.minimum;
|
|
||||||
numberRange += ' .. ';
|
|
||||||
numberRange += schema.maximum;
|
|
||||||
numberRange += schema.exclusiveMaximum ? ' )' : ' ]';
|
|
||||||
} else if (schema.maximum !== undefined) {
|
|
||||||
numberRange = schema.exclusiveMaximum ? '< ' : '<= ';
|
|
||||||
numberRange += schema.maximum;
|
|
||||||
} else if (schema.minimum !== undefined) {
|
|
||||||
numberRange = schema.exclusiveMinimum ? '> ' : '>= ';
|
|
||||||
numberRange += schema.minimum;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof schema.exclusiveMinimum === 'number' || typeof schema.exclusiveMaximum === 'number') {
|
|
||||||
let minimum = 0;
|
|
||||||
let maximum = 0;
|
|
||||||
if (schema.minimum) minimum = schema.minimum;
|
|
||||||
if (typeof schema.exclusiveMinimum === 'number')
|
|
||||||
minimum = minimum <= schema.exclusiveMinimum ? minimum : schema.exclusiveMinimum;
|
|
||||||
|
|
||||||
if (schema.maximum) maximum = schema.maximum;
|
|
||||||
if (typeof schema.exclusiveMaximum === 'number')
|
|
||||||
maximum = maximum > schema.exclusiveMaximum ? maximum : schema.exclusiveMaximum;
|
|
||||||
|
|
||||||
numberRange = `[${minimum} .. ${maximum}]`;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (numberRange !== undefined) {
|
if (numberRange !== undefined) {
|
||||||
res.push(numberRange);
|
res.push(numberRange);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user