Skip to content

Commit

Permalink
feat: constrain integer and number formats when generating JSON Schema (
Browse files Browse the repository at this point in the history
#492)

* feat: constrain integer and number formats when generating JSON Schema

* docs: minor code comment fixes
  • Loading branch information
erunion authored Aug 30, 2021
1 parent 229a938 commit 8c05494
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,7 @@ __tests__/
.github/
.husky/
coverage/
.babel*
.eslint*
.prettier*
webpack.*
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
__tests__/cli/__fixtures__/
coverage/
dist/
packages/
18 changes: 18 additions & 0 deletions __tests__/__snapshots__/operation.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ Array [
"properties": Object {
"code": Object {
"format": "int32",
"maximum": 2147483647,
"minimum": -2147483648,
"type": "integer",
},
"message": Object {
Expand All @@ -59,6 +61,8 @@ Array [
"properties": Object {
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
"name": Object {
Expand All @@ -75,14 +79,20 @@ Array [
},
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
"petId": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
"quantity": Object {
"format": "int32",
"maximum": 2147483647,
"minimum": -2147483648,
"type": "integer",
},
"shipDate": Object {
Expand All @@ -108,6 +118,8 @@ Array [
},
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"readOnly": true,
"type": "integer",
},
Expand Down Expand Up @@ -149,6 +161,8 @@ Array [
"properties": Object {
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
"name": Object {
Expand All @@ -167,6 +181,8 @@ Array [
},
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
"lastName": Object {
Expand All @@ -181,6 +197,8 @@ Array [
"userStatus": Object {
"description": "User Status",
"format": "int32",
"maximum": 2147483647,
"minimum": -2147483648,
"type": "integer",
},
"username": Object {
Expand Down
74 changes: 65 additions & 9 deletions __tests__/lib/openapi-to-json-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,11 @@ describe('`enum` support', () => {

describe('`format` support', () => {
it('should support format', () => {
expect(toJSONSchema({ type: 'integer', format: 'int32' })).toStrictEqual({
expect(toJSONSchema({ type: 'integer', format: 'int8' })).toStrictEqual({
type: 'integer',
format: 'int32',
format: 'int8',
minimum: -128,
maximum: 127,
});

// Should support nested objects as well.
Expand All @@ -421,7 +423,57 @@ describe('`format` support', () => {
},
};

expect(toJSONSchema(schema)).toStrictEqual(schema);
expect(toJSONSchema(schema)).toStrictEqual({
type: 'array',
items: {
type: 'integer',
format: 'int8',
minimum: -128,
maximum: 127,
},
});
});

describe('minimum/maximum constraints', () => {
describe.each([
['integer', 'int8', -128, 127],
['integer', 'int16', -32768, 32767],
['integer', 'int32', -2147483648, 2147483647],
['integer', 'int64', 0 - 2 ** 63, 2 ** 63 - 1], // -9223372036854775808 to 9223372036854775807
['integer', 'uint8', 0, 255],
['integer', 'uint16', 0, 65535],
['integer', 'uint32', 0, 4294967295],
['integer', 'uint64', 0, 2 ** 64 - 1], // 0 to 1844674407370955161
['number', 'float', 0 - 2 ** 128, 2 ** 128 - 1], // -3.402823669209385e+38 to 3.402823669209385e+38
['number', 'double', 0 - Number.MAX_VALUE, Number.MAX_VALUE],
])('`%s`', (type, format, min, max) => {
it('should add a `minimum` and `maximum` if not present', () => {
expect(toJSONSchema({ type, format })).toStrictEqual({
type,
format,
minimum: min,
maximum: max,
});
});

it('should alter constraints if present and beyond the allowable points', () => {
expect(toJSONSchema({ type, format, minimum: min ** 19, maximum: max * 2 })).toStrictEqual({
type,
format,
minimum: min,
maximum: max,
});
});

it('should not touch their constraints if they are within their limits', () => {
expect(toJSONSchema({ type, format, minimum: 0, maximum: 100 })).toStrictEqual({
type,
format,
minimum: 0,
maximum: 100,
});
});
});
});
});

Expand Down Expand Up @@ -456,7 +508,7 @@ describe('`additionalProperties` support', () => {
['false', false],
['an empty object', true],
['an object containing a string', { type: 'string' }],
])('should support when set to `%s`', (tc, additionalProperties) => {
])('should support additionalProperties when set to `%s`', (tc, additionalProperties) => {
const schema = {
type: 'array',
items: {
Expand All @@ -474,7 +526,7 @@ describe('`additionalProperties` support', () => {
});
});

it('should support when set to an object containing an array', () => {
it('should support additionalProperties when set to an object that contains an array', () => {
const schema = {
type: 'array',
items: {
Expand All @@ -486,7 +538,7 @@ describe('`additionalProperties` support', () => {
properties: {
id: {
type: 'integer',
format: 'int64',
format: 'int8',
},
},
},
Expand All @@ -503,7 +555,9 @@ describe('`additionalProperties` support', () => {
properties: {
id: {
type: 'integer',
format: 'int64',
format: 'int8',
minimum: -128,
maximum: 127,
},
},
},
Expand Down Expand Up @@ -808,7 +862,7 @@ describe('`example` / `examples` support', () => {
},
price: {
type: 'integer',
format: 'int32',
format: 'int8',
},
},
example: {
Expand Down Expand Up @@ -846,7 +900,9 @@ describe('`example` / `examples` support', () => {
},
price: {
type: 'integer',
format: 'int32',
format: 'int8',
minimum: -128,
maximum: 127,
examples: [1],
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ Array [
"petId": Object {
"description": "Pet id to delete",
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
},
Expand Down Expand Up @@ -220,6 +222,8 @@ Array [
"properties": Object {
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
"name": Object {
Expand All @@ -231,6 +235,8 @@ Array [
},
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"readOnly": true,
"type": "integer",
},
Expand Down Expand Up @@ -260,6 +266,8 @@ Array [
"properties": Object {
"id": Object {
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
"name": Object {
Expand Down Expand Up @@ -293,6 +301,8 @@ Array [
"petId": Object {
"description": "ID of pet that needs to be updated",
"format": "int64",
"maximum": 9223372036854776000,
"minimum": -9223372036854776000,
"type": "integer",
},
},
Expand Down
2 changes: 2 additions & 0 deletions __tests__/operation/get-parameters-as-json-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,8 @@ describe('descriptions', () => {
pathId: {
type: 'integer',
format: 'uint32',
maximum: 4294967295,
minimum: 0,
description: 'Description for the pathId',
},
},
Expand Down
2 changes: 1 addition & 1 deletion __tests__/operation/get-response-as-json-schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ test('it should return a response as JSON Schema', async () => {
schema: {
type: 'object',
properties: {
code: { type: 'integer', format: 'int32' },
code: { type: 'integer', format: 'int32', maximum: 2147483647, minimum: -2147483648 },
type: { type: 'string' },
message: { type: 'string' },
},
Expand Down
49 changes: 49 additions & 0 deletions src/lib/openapi-to-json-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,37 @@ const UNSUPPORTED_SCHEMA_PROPS = [
'deprecated',
];

/**
* List partially sourced from `openapi-schema-to-json-schema`.
*
* @link https://github.com/openapi-contrib/openapi-schema-to-json-schema/blob/master/lib/converters/schema.js#L140-L154
*/
const FORMAT_OPTIONS = {
INT8_MIN: 0 - 2 ** 7, // -128
INT8_MAX: 2 ** 7 - 1, // 127
INT16_MIN: 0 - 2 ** 15, // -32768
INT16_MAX: 2 ** 15 - 1, // 32767
INT32_MIN: 0 - 2 ** 31, // -2147483648
INT32_MAX: 2 ** 31 - 1, // 2147483647
INT64_MIN: 0 - 2 ** 63, // -9223372036854775808
INT64_MAX: 2 ** 63 - 1, // 9223372036854775807

UINT8_MIN: 0,
UINT8_MAX: 2 ** 8 - 1, // 255
UINT16_MIN: 0,
UINT16_MAX: 2 ** 16 - 1, // 65535
UINT32_MIN: 0,
UINT32_MAX: 2 ** 32 - 1, // 4294967295
UINT64_MIN: 0,
UINT64_MAX: 2 ** 64 - 1, // 18446744073709551615

FLOAT_MIN: 0 - 2 ** 128, // -3.402823669209385e+38
FLOAT_MAX: 2 ** 128 - 1, // 3.402823669209385e+38

DOUBLE_MIN: 0 - Number.MAX_VALUE,
DOUBLE_MAX: Number.MAX_VALUE,
};

/**
* Take a string and encode it to be used as a JSON pointer.
*
Expand Down Expand Up @@ -388,6 +419,24 @@ function toJSONSchema(data, opts = {}) {
}
}

// Ensure that number schemas formats have properly constrained min/max attributes according to whatever type of
// `format` and `type` they adhere to.
if ('format' in schema) {
const formatUpper = schema.format.toUpperCase();

if (`${formatUpper}_MIN` in FORMAT_OPTIONS) {
if ((!schema.minimum && schema.minimum !== 0) || schema.minimum < FORMAT_OPTIONS[`${formatUpper}_MIN`]) {
schema.minimum = FORMAT_OPTIONS[`${formatUpper}_MIN`];
}
}

if (`${formatUpper}_MAX` in FORMAT_OPTIONS) {
if ((!schema.maximum && schema.maximum !== 0) || schema.maximum > FORMAT_OPTIONS[`${formatUpper}_MAX`]) {
schema.maximum = FORMAT_OPTIONS[`${formatUpper}_MAX`];
}
}
}

// Users can pass in parameter defaults via JWT User Data: https://docs.readme.com/docs/passing-data-to-jwt
// We're checking to see if the defaults being passed in exist on endpoints via jsonpointer
if (globalDefaults && Object.keys(globalDefaults).length > 0 && currentLocation) {
Expand Down

0 comments on commit 8c05494

Please sign in to comment.