-
Notifications
You must be signed in to change notification settings - Fork 9.2k
feat: display numeric constraints for path and query parameters #10586
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
base: master
Are you sure you want to change the base?
Changes from all commits
f22d157
cd7ecc3
eae494f
6a7b1a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -352,6 +352,26 @@ export default class ParameterRow extends Component { | |
: null | ||
} | ||
|
||
{(() => { | ||
// Use the appropriate constraint function based on OpenAPI version | ||
const isOAS31 = specSelectors.isOAS31 && specSelectors.isOAS31() | ||
let numericConstraint = null | ||
|
||
if (isOAS31 && fn.jsonSchema202012?.stringifyConstraintNumberRange) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can be avoided by overriding the function for OpenAPI 3.1 here, the same way it was done for these functions. The existing wrapper should identify if our specification is in version 3.1.x and use the correct function. |
||
// For OpenAPI 3.1 (JSON Schema 2020-12) | ||
// Transform Immutable schema to plain object for the function | ||
const plainSchema = schema?.toJS ? schema.toJS() : schema | ||
numericConstraint = fn.jsonSchema202012.stringifyConstraintNumberRange(plainSchema) | ||
} else if (fn.stringifyConstraintNumberRange) { | ||
// For OpenAPI 2.0/3.0 (JSON Schema Draft 5) | ||
numericConstraint = fn.stringifyConstraintNumberRange(schema) | ||
} | ||
|
||
return numericConstraint ? ( | ||
<Markdown className="parameter__constraint" source={`<i>Constraints</i> : ${numericConstraint}`}/> | ||
) : null | ||
})()} | ||
|
||
{(isFormData && !isFormDataSupported) && <div>Error: your browser does not support FormData</div>} | ||
|
||
{ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -525,3 +525,5 @@ export const hasSchemaType = (schema, type) => { | |
|
||
return hasType(schemaType) | ||
} | ||
|
||
export { stringifyConstraintNumberRange } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We can instead simply export where the function is defined: export const stringifyConstraintNumberRange = (schema) => { |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -49,3 +49,34 @@ const getType = (schema, processedSchemas = new WeakSet()) => { | |
|
||
export const getSchemaObjectTypeLabel = (schema) => | ||
getType(immutableToJS(schema)) | ||
|
||
// For OpenAPI 2.0 and 3.0 (JSON Schema Draft 4/5) | ||
// where exclusiveMinimum and exclusiveMaximum are booleans | ||
export const stringifyConstraintNumberRange = (schema) => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To avoid checking |
||
// Handle both plain objects and Immutable Maps | ||
const minimum = schema?.get ? schema.get("minimum") : schema?.minimum | ||
const maximum = schema?.get ? schema.get("maximum") : schema?.maximum | ||
const exclusiveMinimum = schema?.get ? schema.get("exclusiveMinimum") : schema?.exclusiveMinimum | ||
const exclusiveMaximum = schema?.get ? schema.get("exclusiveMaximum") : schema?.exclusiveMaximum | ||
|
||
const hasMinimum = typeof minimum === "number" | ||
const hasMaximum = typeof maximum === "number" | ||
const isMinExclusive = exclusiveMinimum === true | ||
const isMaxExclusive = exclusiveMaximum === true | ||
|
||
if (hasMinimum && hasMaximum) { | ||
const minSymbol = isMinExclusive ? "(" : "[" | ||
const maxSymbol = isMaxExclusive ? ")" : "]" | ||
return `${minSymbol}${minimum}, ${maximum}${maxSymbol}` | ||
} | ||
if (hasMinimum) { | ||
const minSymbol = isMinExclusive ? ">" : "≥" | ||
return `${minSymbol} ${minimum}` | ||
} | ||
if (hasMaximum) { | ||
const maxSymbol = isMaxExclusive ? "<" : "≤" | ||
return `${maxSymbol} ${maximum}` | ||
} | ||
|
||
return null | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,289 @@ | ||
import React from "react" | ||
import { shallow } from "enzyme" | ||
import { fromJS } from "immutable" | ||
import ParameterRow from "core/components/parameter-row" | ||
|
||
describe("ParameterRow constraints", () => { | ||
const defaultProps = { | ||
onChange: jest.fn(), | ||
param: fromJS({ name: "test", in: "query" }), | ||
rawParam: fromJS({ name: "test", in: "query" }), | ||
getComponent: () => () => null, | ||
fn: {}, | ||
isExecute: false, | ||
onChangeConsumes: jest.fn(), | ||
specSelectors: { | ||
isOAS3: () => false, | ||
isOAS31: () => false, | ||
parameterWithMetaByIdentity: () => fromJS({}), | ||
contentTypeValues: () => fromJS({}), | ||
consumesOptionsFor: () => fromJS([]), | ||
parameterInclusionSettingFor: () => false, | ||
}, | ||
specActions: {}, | ||
pathMethod: ["get", "/test"], | ||
getConfigs: () => ({ showExtensions: false, showCommonExtensions: false }), | ||
specPath: fromJS([]), | ||
oas3Actions: {}, | ||
oas3Selectors: { | ||
activeExamplesMember: () => null, | ||
}, | ||
} | ||
|
||
describe("OpenAPI 2.0/3.0 constraints (JSON Schema Draft 5)", () => { | ||
it("should display minimum constraint", () => { | ||
const schema = fromJS({ | ||
type: "number", | ||
minimum: 1, | ||
}) | ||
const props = { | ||
...defaultProps, | ||
fn: { | ||
...defaultProps.fn, | ||
stringifyConstraintNumberRange: jest.fn(() => "≥ 1"), | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "number"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "number"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(props.fn.stringifyConstraintNumberRange).toHaveBeenCalledWith(schema) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(1) | ||
}) | ||
|
||
it("should display maximum constraint", () => { | ||
const schema = fromJS({ | ||
type: "number", | ||
maximum: 100, | ||
}) | ||
const props = { | ||
...defaultProps, | ||
fn: { | ||
...defaultProps.fn, | ||
stringifyConstraintNumberRange: jest.fn(() => "≤ 100"), | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "number"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "number"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(props.fn.stringifyConstraintNumberRange).toHaveBeenCalledWith(schema) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(1) | ||
}) | ||
|
||
it("should display range constraint", () => { | ||
const schema = fromJS({ | ||
type: "number", | ||
minimum: 1, | ||
maximum: 100, | ||
}) | ||
const props = { | ||
...defaultProps, | ||
fn: { | ||
...defaultProps.fn, | ||
stringifyConstraintNumberRange: jest.fn(() => "[1, 100]"), | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "number"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "number"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(props.fn.stringifyConstraintNumberRange).toHaveBeenCalledWith(schema) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(1) | ||
}) | ||
|
||
it("should display exclusive constraints (boolean style)", () => { | ||
const schema = fromJS({ | ||
type: "number", | ||
minimum: 0, | ||
exclusiveMinimum: true, | ||
maximum: 100, | ||
exclusiveMaximum: true, | ||
}) | ||
const props = { | ||
...defaultProps, | ||
fn: { | ||
...defaultProps.fn, | ||
stringifyConstraintNumberRange: jest.fn(() => "(0, 100)"), | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "number"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "number"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(props.fn.stringifyConstraintNumberRange).toHaveBeenCalledWith(schema) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(1) | ||
}) | ||
|
||
it("should not display constraints when none exist", () => { | ||
const schema = fromJS({ | ||
type: "string", | ||
}) | ||
const props = { | ||
...defaultProps, | ||
fn: { | ||
...defaultProps.fn, | ||
stringifyConstraintNumberRange: jest.fn(() => null), | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "string"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "string"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(props.fn.stringifyConstraintNumberRange).toHaveBeenCalledWith(schema) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(0) | ||
}) | ||
}) | ||
|
||
describe("OpenAPI 3.1 constraints (JSON Schema 2020-12)", () => { | ||
it("should use jsonSchema202012 function for OAS 3.1", () => { | ||
const schema = fromJS({ | ||
type: "number", | ||
exclusiveMinimum: 0, | ||
exclusiveMaximum: 100, | ||
}) | ||
const props = { | ||
...defaultProps, | ||
specSelectors: { | ||
...defaultProps.specSelectors, | ||
isOAS31: () => true, | ||
}, | ||
fn: { | ||
...defaultProps.fn, | ||
jsonSchema202012: { | ||
stringifyConstraintNumberRange: jest.fn(() => "(0, 100)"), | ||
}, | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "number"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "number"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(props.fn.jsonSchema202012.stringifyConstraintNumberRange).toHaveBeenCalledWith(schema.toJS()) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(1) | ||
}) | ||
|
||
it("should handle immutable to plain object conversion for OAS 3.1", () => { | ||
const schema = fromJS({ | ||
type: "number", | ||
minimum: 1, | ||
maximum: 100, | ||
}) | ||
const props = { | ||
...defaultProps, | ||
specSelectors: { | ||
...defaultProps.specSelectors, | ||
isOAS31: () => true, | ||
}, | ||
fn: { | ||
...defaultProps.fn, | ||
jsonSchema202012: { | ||
stringifyConstraintNumberRange: jest.fn(() => "[1, 100]"), | ||
}, | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "number"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "number"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
const wrapper = shallow(<ParameterRow {...props} />) | ||
// Verify the schema was converted to plain object | ||
const passedSchema = props.fn.jsonSchema202012.stringifyConstraintNumberRange.mock.calls[0][0] | ||
expect(passedSchema.toJS).toBeUndefined() // Should be plain object, not Immutable | ||
expect(passedSchema.minimum).toBe(1) | ||
expect(passedSchema.maximum).toBe(100) | ||
}) | ||
}) | ||
|
||
describe("fallback behavior", () => { | ||
it("should not crash when constraint functions are not available", () => { | ||
const schema = fromJS({ | ||
type: "number", | ||
minimum: 1, | ||
}) | ||
const props = { | ||
...defaultProps, | ||
fn: { | ||
...defaultProps.fn, | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => "number"), | ||
getSchemaObjectTypeLabel: jest.fn(() => "number"), | ||
// No stringifyConstraintNumberRange function | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
schema: schema.toJS(), | ||
}), | ||
} | ||
|
||
expect(() => { | ||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(0) | ||
}).not.toThrow() | ||
}) | ||
|
||
it("should handle null schema gracefully", () => { | ||
const props = { | ||
...defaultProps, | ||
fn: { | ||
...defaultProps.fn, | ||
stringifyConstraintNumberRange: jest.fn(() => null), | ||
getSampleSchema: jest.fn(), | ||
getSchemaObjectType: jest.fn(() => null), | ||
getSchemaObjectTypeLabel: jest.fn(() => "unknown"), | ||
}, | ||
param: fromJS({ | ||
name: "test", | ||
in: "query", | ||
}), | ||
} | ||
|
||
expect(() => { | ||
const wrapper = shallow(<ParameterRow {...props} />) | ||
expect(wrapper.find('Markdown[className="parameter__constraint"]')).toHaveLength(0) | ||
}).not.toThrow() | ||
}) | ||
}) | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's instead define the
numericConstraint
before the return, e.g.