Skip to content

Commit bb25c59

Browse files
feat(common): add support for post/put body schemas in openapi import (hoppscotch#5322)
Co-authored-by: jamesgeorge007 <[email protected]>
1 parent 3994d9e commit bb25c59

File tree

6 files changed

+640
-35
lines changed

6 files changed

+640
-35
lines changed

packages/hoppscotch-common/src/helpers/functional/json.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@ export const safeParseJSON: SafeParseJSON = (str, convertToArray = false) =>
2626
return data
2727
})
2828

29+
/**
30+
* Generates a prettified JSON representation of an object
31+
* @param obj The object to get the representation of
32+
* @returns The prettified JSON string of the object
33+
*/
34+
export const prettyPrintJSON = (obj: unknown): O.Option<string> =>
35+
O.tryCatch(() => JSON.stringify(obj, null, "\t"))
36+
2937
/**
3038
* Checks if given string is a JSON string
3139
* @param str Raw string to be checked
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
import { OpenAPIV2 } from "openapi-types"
2+
import * as O from "fp-ts/Option"
3+
import { pipe, flow } from "fp-ts/function"
4+
import * as A from "fp-ts/Array"
5+
import { prettyPrintJSON } from "~/helpers/functional/json"
6+
7+
type PrimitiveSchemaType = "string" | "integer" | "number" | "boolean"
8+
9+
type SchemaType = "array" | "object" | PrimitiveSchemaType
10+
11+
type PrimitiveRequestBodyExample = number | string | boolean
12+
13+
type RequestBodyExample =
14+
| { [name: string]: RequestBodyExample }
15+
| Array<RequestBodyExample>
16+
| PrimitiveRequestBodyExample
17+
18+
const getPrimitiveTypePlaceholder = (
19+
schemaType: PrimitiveSchemaType
20+
): PrimitiveRequestBodyExample => {
21+
switch (schemaType) {
22+
case "string":
23+
return "string"
24+
case "integer":
25+
case "number":
26+
return 1
27+
case "boolean":
28+
return true
29+
}
30+
}
31+
32+
const getSchemaTypeFromSchemaObject = (
33+
schema: OpenAPIV2.SchemaObject
34+
): O.Option<SchemaType> =>
35+
pipe(
36+
schema.type,
37+
O.fromNullable,
38+
O.map(
39+
(schemaType) =>
40+
(Array.isArray(schemaType) ? schemaType[0] : schemaType) as SchemaType
41+
)
42+
)
43+
44+
const isSchemaTypePrimitive = (
45+
schemaType: string
46+
): schemaType is PrimitiveSchemaType =>
47+
["string", "integer", "number", "boolean"].includes(schemaType)
48+
49+
const isSchemaTypeArray = (schemaType: string): schemaType is "array" =>
50+
schemaType === "array"
51+
52+
const isSchemaTypeObject = (schemaType: string): schemaType is "object" =>
53+
schemaType === "object"
54+
55+
const getSampleEnumValueOrPlaceholder = (
56+
schema: OpenAPIV2.SchemaObject
57+
): RequestBodyExample =>
58+
pipe(
59+
schema.enum,
60+
O.fromNullable,
61+
O.map((enums) => enums[0] as RequestBodyExample),
62+
O.altW(() =>
63+
pipe(
64+
schema,
65+
getSchemaTypeFromSchemaObject,
66+
O.filter(isSchemaTypePrimitive),
67+
O.map(getPrimitiveTypePlaceholder)
68+
)
69+
),
70+
O.getOrElseW(() => "")
71+
)
72+
73+
const generateExampleArrayFromOpenAPIV2ItemsObject = (
74+
items: OpenAPIV2.ItemsObject
75+
): RequestBodyExample => {
76+
// Guard against undefined items
77+
if (!items || !items.type) {
78+
return []
79+
}
80+
81+
// ItemsObject can not hold type "object"
82+
// https://swagger.io/specification/v2/#itemsObject
83+
84+
// TODO : Handle array of objects
85+
// https://stackoverflow.com/questions/60490974/how-to-define-an-array-of-objects-in-openapi-2-0
86+
87+
return pipe(
88+
items,
89+
O.fromPredicate(
90+
flow((items) => items.type as SchemaType, isSchemaTypePrimitive)
91+
),
92+
O.map(flow(getSampleEnumValueOrPlaceholder, (arrayItem) => [arrayItem])),
93+
O.getOrElse(() =>
94+
// If the type is not primitive, it is "array"
95+
// items property is required if type is array
96+
items.items
97+
? [
98+
generateExampleArrayFromOpenAPIV2ItemsObject(
99+
items.items as OpenAPIV2.ItemsObject
100+
),
101+
]
102+
: []
103+
)
104+
)
105+
}
106+
107+
const generateRequestBodyExampleFromOpenAPIV2BodySchema = (
108+
schema: OpenAPIV2.SchemaObject
109+
): RequestBodyExample => {
110+
if (schema.example) return schema.example as RequestBodyExample
111+
112+
const primitiveTypeExample = pipe(
113+
schema,
114+
O.fromPredicate(
115+
flow(
116+
getSchemaTypeFromSchemaObject,
117+
O.map(isSchemaTypePrimitive),
118+
O.getOrElseW(() => false) // No schema type found in the schema object, assume non-primitive
119+
)
120+
),
121+
O.map(getSampleEnumValueOrPlaceholder) // Use enum or placeholder to populate primitive field
122+
)
123+
124+
if (O.isSome(primitiveTypeExample)) return primitiveTypeExample.value
125+
126+
const arrayTypeExample = pipe(
127+
schema,
128+
O.fromPredicate(
129+
flow(
130+
getSchemaTypeFromSchemaObject,
131+
O.map(isSchemaTypeArray),
132+
O.getOrElseW(() => false) // No schema type found in the schema object, assume type to be different from array
133+
)
134+
),
135+
O.map((schema) => schema.items as OpenAPIV2.ItemsObject),
136+
O.filter((items) => items != null), // Filter out null/undefined items
137+
O.map(generateExampleArrayFromOpenAPIV2ItemsObject)
138+
)
139+
140+
if (O.isSome(arrayTypeExample)) return arrayTypeExample.value
141+
142+
return pipe(
143+
schema,
144+
O.fromPredicate(
145+
flow(
146+
getSchemaTypeFromSchemaObject,
147+
O.map(isSchemaTypeObject),
148+
O.getOrElseW(() => false)
149+
)
150+
),
151+
O.chain((schema) =>
152+
pipe(
153+
schema.properties,
154+
O.fromNullable,
155+
O.map(
156+
(properties) =>
157+
Object.entries(properties) as [string, OpenAPIV2.SchemaObject][]
158+
)
159+
)
160+
),
161+
O.getOrElseW(() => [] as [string, OpenAPIV2.SchemaObject][]),
162+
A.reduce(
163+
{} as { [name: string]: RequestBodyExample },
164+
(aggregatedExample, property) => {
165+
const example = generateRequestBodyExampleFromOpenAPIV2BodySchema(
166+
property[1]
167+
)
168+
aggregatedExample[property[0]] = example
169+
return aggregatedExample
170+
}
171+
)
172+
)
173+
}
174+
175+
export const generateRequestBodyExampleFromOpenAPIV2Body = (
176+
op: OpenAPIV2.OperationObject
177+
): string =>
178+
pipe(
179+
(op.parameters ?? []) as OpenAPIV2.Parameter[],
180+
A.findFirst((param) => param.in === "body"),
181+
O.map(
182+
flow(
183+
(parameter) => parameter.schema,
184+
generateRequestBodyExampleFromOpenAPIV2BodySchema
185+
)
186+
),
187+
O.chain(prettyPrintJSON),
188+
O.getOrElse(() => "")
189+
)
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { OpenAPIV3 } from "openapi-types"
2+
import { pipe } from "fp-ts/function"
3+
import * as O from "fp-ts/Option"
4+
5+
type SchemaType =
6+
| OpenAPIV3.ArraySchemaObjectType
7+
| OpenAPIV3.NonArraySchemaObjectType
8+
9+
type PrimitiveSchemaType = Exclude<SchemaType, "array" | "object">
10+
11+
type PrimitiveRequestBodyExample = string | number | boolean | null
12+
13+
type RequestBodyExample =
14+
| PrimitiveRequestBodyExample
15+
| Array<RequestBodyExample>
16+
| { [name: string]: RequestBodyExample }
17+
18+
const isSchemaTypePrimitive = (
19+
schemaType: SchemaType
20+
): schemaType is PrimitiveSchemaType =>
21+
!["array", "object"].includes(schemaType)
22+
23+
const getPrimitiveTypePlaceholder = (
24+
primitiveType: PrimitiveSchemaType
25+
): PrimitiveRequestBodyExample => {
26+
switch (primitiveType) {
27+
case "number":
28+
return 0.0
29+
case "integer":
30+
return 0
31+
case "string":
32+
return "string"
33+
case "boolean":
34+
return true
35+
}
36+
}
37+
38+
// Use carefully, call only when type is primitive
39+
// TODO(agarwal): Use Enum values, if any
40+
const generatePrimitiveRequestBodyExample = (
41+
schemaObject: OpenAPIV3.NonArraySchemaObject
42+
): RequestBodyExample =>
43+
getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType)
44+
45+
// Use carefully, call only when type is object
46+
const generateObjectRequestBodyExample = (
47+
schemaObject: OpenAPIV3.NonArraySchemaObject
48+
): RequestBodyExample =>
49+
pipe(
50+
schemaObject.properties,
51+
O.fromNullable,
52+
O.map(Object.entries),
53+
O.getOrElseW(() => [] as [string, OpenAPIV3.SchemaObject][]),
54+
(entries) =>
55+
entries.reduce(
56+
(acc, [key, propSchema]) => ({
57+
...acc,
58+
[key]: generateRequestBodyExampleFromSchemaObject(
59+
propSchema as OpenAPIV3.SchemaObject
60+
),
61+
}),
62+
{} as Record<string, RequestBodyExample>
63+
)
64+
)
65+
66+
const generateArrayRequestBodyExample = (
67+
schemaObject: OpenAPIV3.ArraySchemaObject
68+
): RequestBodyExample => [
69+
generateRequestBodyExampleFromSchemaObject(
70+
schemaObject.items as OpenAPIV3.SchemaObject
71+
),
72+
]
73+
74+
const generateRequestBodyExampleFromSchemaObject = (
75+
schemaObject: OpenAPIV3.SchemaObject
76+
): RequestBodyExample => {
77+
// TODO: Handle schema objects with allof
78+
if (schemaObject.example) return schemaObject.example as RequestBodyExample
79+
80+
// If request body can be oneof or allof several schema, choose the first schema to generate an example
81+
if (schemaObject.oneOf)
82+
return generateRequestBodyExampleFromSchemaObject(
83+
schemaObject.oneOf[0] as OpenAPIV3.SchemaObject
84+
)
85+
if (schemaObject.anyOf)
86+
return generateRequestBodyExampleFromSchemaObject(
87+
schemaObject.anyOf[0] as OpenAPIV3.SchemaObject
88+
)
89+
90+
if (!schemaObject.type) return ""
91+
92+
if (isSchemaTypePrimitive(schemaObject.type))
93+
return generatePrimitiveRequestBodyExample(
94+
schemaObject as OpenAPIV3.NonArraySchemaObject
95+
)
96+
97+
if (schemaObject.type === "object")
98+
return generateObjectRequestBodyExample(
99+
schemaObject as OpenAPIV3.NonArraySchemaObject
100+
)
101+
102+
return generateArrayRequestBodyExample(
103+
schemaObject as OpenAPIV3.ArraySchemaObject
104+
)
105+
}
106+
107+
export const generateRequestBodyExampleFromMediaObject = (
108+
mediaObject: OpenAPIV3.MediaTypeObject
109+
): RequestBodyExample => {
110+
// First check for direct example
111+
if (mediaObject.example) return mediaObject.example as RequestBodyExample
112+
113+
// Then check for examples object (OpenAPI v3 format)
114+
if (mediaObject.examples) {
115+
const firstExample = Object.values(mediaObject.examples)[0]
116+
if (
117+
firstExample &&
118+
typeof firstExample === "object" &&
119+
"value" in firstExample
120+
) {
121+
return firstExample.value as RequestBodyExample
122+
}
123+
// Fallback if examples doesn't have the expected structure
124+
return Object.values(mediaObject.examples)[0] as RequestBodyExample
125+
}
126+
127+
// Fallback to generating from schema
128+
return mediaObject.schema
129+
? generateRequestBodyExampleFromSchemaObject(
130+
mediaObject.schema as OpenAPIV3.SchemaObject
131+
)
132+
: ""
133+
}

0 commit comments

Comments
 (0)