Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/core/components/parameter-row.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,26 @@ export default class ParameterRow extends Component {
: null
}

{(() => {
Copy link
Contributor

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.

const numericConstraint = fn.stringifyConstraintNumberRange(schema)

return (
  // ...

  {numericConstraint && <Markdown className="parameter__constraint" source={`<i>Constraints</i>: ${numericConstraint}`}/>}

  // ...
)

// Use the appropriate constraint function based on OpenAPI version
const isOAS31 = specSelectors.isOAS31 && specSelectors.isOAS31()
let numericConstraint = null

if (isOAS31 && fn.jsonSchema202012?.stringifyConstraintNumberRange) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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>}

{
Expand Down
2 changes: 2 additions & 0 deletions src/core/plugins/json-schema-2020-12/fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -525,3 +525,5 @@ export const hasSchemaType = (schema, type) => {

return hasType(schemaType)
}

export { stringifyConstraintNumberRange }
Copy link
Contributor

Choose a reason for hiding this comment

The 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) => {

2 changes: 2 additions & 0 deletions src/core/plugins/json-schema-2020-12/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
getSchemaKeywords,
makeGetExtensionKeywords,
hasSchemaType,
stringifyConstraintNumberRange,
} from "./fn"
import { JSONSchemaPathContext, JSONSchemaLevelContext } from "./context"
import {
Expand Down Expand Up @@ -145,6 +146,7 @@ const JSONSchema202012Plugin = ({ getSystem, fn }) => {
getSchemaKeywords,
getExtensionKeywords: makeGetExtensionKeywords(fnAccessor),
hasSchemaType,
stringifyConstraintNumberRange,
},
},
}
Expand Down
31 changes: 31 additions & 0 deletions src/core/plugins/json-schema-5/fn.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid checking schema?.get every time, we could instead either transform schema to a plain object at the start with immutableToJS or have a helper function that will call this one after using immutableToJS. An example of the second option is here.

// 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
}
3 changes: 2 additions & 1 deletion src/core/plugins/json-schema-5/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import Schemes from "./components/schemes"
import SchemesContainer from "./containers/schemes"
import * as JSONSchemaComponents from "./components/json-schema-components"
import { ModelExtensions } from "./components/model-extensions"
import { getSchemaObjectTypeLabel, hasSchemaType } from "./fn"
import { getSchemaObjectTypeLabel, hasSchemaType, stringifyConstraintNumberRange } from "./fn"

const JSONSchema5Plugin = () => ({
components: {
Expand All @@ -35,6 +35,7 @@ const JSONSchema5Plugin = () => ({
fn: {
hasSchemaType,
getSchemaObjectTypeLabel,
stringifyConstraintNumberRange,
},
})

Expand Down
289 changes: 289 additions & 0 deletions test/unit/components/parameter-constraints.spec.js
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()
})
})
})
Loading