Compare commits

...

17 Commits

Author SHA1 Message Date
Andrew Bastin
0d78e6d209 Merge pull request #2243 from RishabhAgarwal-2001/2087-openapi 2022-06-29 22:14:42 +05:30
Andrew Bastin
172e459872 Merge branch 'orphan-pr/2243' into 2087-openapi 2022-06-29 22:14:14 +05:30
Rishabh Agarwal
5233c36904 refactor: comments address 2022-05-17 15:24:36 +05:30
Rishabh Agarwal
6417ece710 Merge branch '2087-openapi' of https://github.com/RishabhAgarwal-2001/hoppscotch into 2087-openapi 2022-05-17 14:42:02 +05:30
Andrew Bastin
a3d92c862c refactor: rename to prettyPrintJSON and add jsdoc 2022-05-03 18:05:39 +05:30
Rishabh Agarwal
f84b67ec39 feat: handle oneof and anyof 2022-05-03 18:04:34 +05:30
Rishabh Agarwal
3afd2c1cf2 refactor: address comments 2022-05-03 18:04:34 +05:30
Rishabh Agarwal
ed49c0b72c refactor: examplev2 created and lintfix 2022-05-03 18:04:33 +05:30
Rishabh Agarwal
bc2f81ff25 feat: v3 requests example working 2022-05-03 18:04:33 +05:30
Rishabh Agarwal
721a201e7a refactor: removed unecessary const 2022-05-03 18:04:33 +05:30
Rishabh Agarwal
b0071e6859 feat: oasv2 example generation complete 2022-05-03 18:04:33 +05:30
Rishabh Agarwal
3a41d79c2f feat: handle oneof and anyof 2022-04-27 23:48:26 +05:30
Rishabh Agarwal
2fd9eb0767 refactor: address comments 2022-04-18 19:29:45 +05:30
Rishabh Agarwal
2aef9c5691 refactor: examplev2 created and lintfix 2022-04-18 15:45:52 +05:30
Rishabh Agarwal
14e9d19ae6 feat: v3 requests example working 2022-04-15 12:06:51 +05:30
Rishabh Agarwal
a04b634089 refactor: removed unecessary const 2022-04-08 11:15:39 +05:30
Rishabh Agarwal
2244d38439 feat: oasv2 example generation complete 2022-04-08 11:06:25 +05:30
5 changed files with 464 additions and 7 deletions

View File

@@ -9,6 +9,14 @@ import { flow } from "fp-ts/function"
export const safeParseJSON = (str: string): O.Option<object> =>
O.tryCatch(() => JSON.parse(str))
/**
* Generates a prettified JSON representation of an object
* @param obj The object to get the representation of
* @returns The prettified JSON string of the object
*/
export const prettyPrintJSON = (obj: unknown): O.Option<string> =>
O.tryCatch(() => JSON.stringify(obj, null, "\t"))
/**
* Checks if given string is a JSON string
* @param str Raw string to be checked

View File

@@ -0,0 +1,185 @@
import { OpenAPIV2 } from "openapi-types"
import * as O from "fp-ts/Option"
import { pipe, flow } from "fp-ts/function"
import * as A from "fp-ts/Array"
import { prettyPrintJSON } from "~/helpers/functional/json"
type PrimitiveSchemaType = "string" | "integer" | "number" | "boolean"
type SchemaType = "array" | "object" | PrimitiveSchemaType
type PrimitiveRequestBodyExample = number | string | boolean
type RequestBodyExample =
| { [name: string]: RequestBodyExample }
| Array<RequestBodyExample>
| PrimitiveRequestBodyExample
const getPrimitiveTypePlaceholder = (
schemaType: PrimitiveSchemaType
): PrimitiveRequestBodyExample => {
switch (schemaType) {
case "string":
return "string"
case "integer":
case "number":
return 1
case "boolean":
return true
}
}
const getSchemaTypeFromSchemaObject = (
schema: OpenAPIV2.SchemaObject
): O.Option<SchemaType> =>
pipe(
schema.type,
O.fromNullable,
O.map(
(schemaType) =>
(Array.isArray(schemaType) ? schemaType[0] : schemaType) as SchemaType
)
)
const isSchemaTypePrimitive = (
schemaType: string
): schemaType is PrimitiveSchemaType =>
["string", "integer", "number", "boolean"].includes(schemaType)
const isSchemaTypeArray = (schemaType: string): schemaType is "array" =>
schemaType === "array"
const isSchemaTypeObject = (schemaType: string): schemaType is "object" =>
schemaType === "object"
const getSampleEnumValueOrPlaceholder = (
schema: OpenAPIV2.SchemaObject
): RequestBodyExample =>
pipe(
schema.enum,
O.fromNullable,
O.map((enums) => enums[0] as RequestBodyExample),
O.altW(() =>
pipe(
schema,
getSchemaTypeFromSchemaObject,
O.filter(isSchemaTypePrimitive),
O.map(getPrimitiveTypePlaceholder)
)
),
O.getOrElseW(() => "")
)
const generateExampleArrayFromOpenAPIV2ItemsObject = (
items: OpenAPIV2.ItemsObject
): RequestBodyExample =>
// ItemsObject can not hold type "object"
// https://swagger.io/specification/v2/#itemsObject
// TODO : Handle array of objects
// https://stackoverflow.com/questions/60490974/how-to-define-an-array-of-objects-in-openapi-2-0
pipe(
items,
O.fromPredicate(
flow((items) => items.type as SchemaType, isSchemaTypePrimitive)
),
O.map(
flow(getSampleEnumValueOrPlaceholder, (arrayItem) => [
arrayItem,
arrayItem,
])
),
O.getOrElse(() =>
// If the type is not primitive, it is "array"
// items property is required if type is array
[
generateExampleArrayFromOpenAPIV2ItemsObject(
items.items as OpenAPIV2.ItemsObject
),
]
)
)
const generateRequestBodyExampleFromOpenAPIV2BodySchema = (
schema: OpenAPIV2.SchemaObject
): RequestBodyExample => {
if (schema.example) return schema.example as RequestBodyExample
const primitiveTypeExample = pipe(
schema,
O.fromPredicate(
flow(
getSchemaTypeFromSchemaObject,
O.map(isSchemaTypePrimitive),
O.getOrElseW(() => false) // No schema type found in the schema object, assume non-primitive
)
),
O.map(getSampleEnumValueOrPlaceholder) // Use enum or placeholder to populate primitive field
)
if (O.isSome(primitiveTypeExample)) return primitiveTypeExample.value
const arrayTypeExample = pipe(
schema,
O.fromPredicate(
flow(
getSchemaTypeFromSchemaObject,
O.map(isSchemaTypeArray),
O.getOrElseW(() => false) // No schema type found in the schema object, assume type to be different from array
)
),
O.map((schema) => schema.items as OpenAPIV2.ItemsObject),
O.map(generateExampleArrayFromOpenAPIV2ItemsObject)
)
if (O.isSome(arrayTypeExample)) return arrayTypeExample.value
return pipe(
schema,
O.fromPredicate(
flow(
getSchemaTypeFromSchemaObject,
O.map(isSchemaTypeObject),
O.getOrElseW(() => false)
)
),
O.chain((schema) =>
pipe(
schema.properties,
O.fromNullable,
O.map(
(properties) =>
Object.entries(properties) as [string, OpenAPIV2.SchemaObject][]
)
)
),
O.getOrElseW(() => [] as [string, OpenAPIV2.SchemaObject][]),
A.reduce(
{} as { [name: string]: RequestBodyExample },
(aggregatedExample, property) => {
const example = generateRequestBodyExampleFromOpenAPIV2BodySchema(
property[1]
)
aggregatedExample[property[0]] = example
return aggregatedExample
}
)
)
}
export const generateRequestBodyExampleFromOpenAPIV2Body = (
op: OpenAPIV2.OperationObject
): string =>
pipe(
(op.parameters ?? []) as OpenAPIV2.Parameter[],
A.findFirst((param) => param.in === "body"),
O.map(
flow(
(parameter) => parameter.schema,
generateRequestBodyExampleFromOpenAPIV2BodySchema
)
),
O.chain(prettyPrintJSON),
O.getOrElse(() => "")
)

View File

@@ -0,0 +1,109 @@
import { OpenAPIV3 } from "openapi-types"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import { tupleToRecord } from "~/helpers/functional/record"
type SchemaType =
| OpenAPIV3.ArraySchemaObjectType
| OpenAPIV3.NonArraySchemaObjectType
type PrimitiveSchemaType = Exclude<SchemaType, "array" | "object">
type PrimitiveRequestBodyExample = string | number | boolean | null
type RequestBodyExample =
| PrimitiveRequestBodyExample
| Array<RequestBodyExample>
| { [name: string]: RequestBodyExample }
const isSchemaTypePrimitive = (
schemaType: SchemaType
): schemaType is PrimitiveSchemaType =>
!["array", "object"].includes(schemaType)
const getPrimitiveTypePlaceholder = (
primitiveType: PrimitiveSchemaType
): PrimitiveRequestBodyExample => {
switch (primitiveType) {
case "number":
return 0.0
case "integer":
return 0
case "string":
return "string"
case "boolean":
return true
}
}
// Use carefully, call only when type is primitive
// TODO(agarwal): Use Enum values, if any
const generatePrimitiveRequestBodyExample = (
schemaObject: OpenAPIV3.NonArraySchemaObject
): RequestBodyExample =>
getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType)
// Use carefully, call only when type is object
const generateObjectRequestBodyExample = (
schemaObject: OpenAPIV3.NonArraySchemaObject
): RequestBodyExample =>
pipe(
schemaObject.properties,
O.fromNullable,
O.map(Object.entries),
O.getOrElseW(() => [] as [string, OpenAPIV3.SchemaObject][]),
tupleToRecord
)
const generateArrayRequestBodyExample = (
schemaObject: OpenAPIV3.ArraySchemaObject
): RequestBodyExample => [
generateRequestBodyExampleFromSchemaObject(
schemaObject.items as OpenAPIV3.SchemaObject
),
]
const generateRequestBodyExampleFromSchemaObject = (
schemaObject: OpenAPIV3.SchemaObject
): RequestBodyExample => {
// TODO: Handle schema objects with allof
if (schemaObject.example) return schemaObject.example as RequestBodyExample
// If request body can be oneof or allof several schema, choose the first schema to generate an example
if (schemaObject.oneOf)
return generateRequestBodyExampleFromSchemaObject(
schemaObject.oneOf[0] as OpenAPIV3.SchemaObject
)
if (schemaObject.anyOf)
return generateRequestBodyExampleFromSchemaObject(
schemaObject.anyOf[0] as OpenAPIV3.SchemaObject
)
if (!schemaObject.type) return ""
if (isSchemaTypePrimitive(schemaObject.type))
return generatePrimitiveRequestBodyExample(
schemaObject as OpenAPIV3.NonArraySchemaObject
)
if (schemaObject.type === "object")
return generateObjectRequestBodyExample(
schemaObject as OpenAPIV3.NonArraySchemaObject
)
return generateArrayRequestBodyExample(
schemaObject as OpenAPIV3.ArraySchemaObject
)
}
export const generateRequestBodyExampleFromMediaObject = (
mediaObject: OpenAPIV3.MediaTypeObject
): RequestBodyExample => {
if (mediaObject.example) return mediaObject.example as RequestBodyExample
if (mediaObject.examples) return mediaObject.examples[0] as RequestBodyExample
return mediaObject.schema
? generateRequestBodyExampleFromSchemaObject(
mediaObject.schema as OpenAPIV3.SchemaObject
)
: ""
}

View File

@@ -0,0 +1,129 @@
import { OpenAPIV3_1 as OpenAPIV31 } from "openapi-types"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
type MixedArraySchemaType = (
| OpenAPIV31.ArraySchemaObjectType
| OpenAPIV31.NonArraySchemaObjectType
)[]
type SchemaType =
| OpenAPIV31.ArraySchemaObjectType
| OpenAPIV31.NonArraySchemaObjectType
| MixedArraySchemaType
type PrimitiveSchemaType = Exclude<
OpenAPIV31.NonArraySchemaObjectType,
"object"
>
type PrimitiveRequestBodyExample = string | number | boolean | null
type RequestBodyExample =
| PrimitiveRequestBodyExample
| Array<RequestBodyExample>
| { [name: string]: RequestBodyExample }
const isSchemaTypePrimitive = (
schemaType: SchemaType
): schemaType is PrimitiveSchemaType =>
!Array.isArray(schemaType) && !["array", "object"].includes(schemaType)
const getPrimitiveTypePlaceholder = (
primitiveType: PrimitiveSchemaType
): PrimitiveRequestBodyExample => {
switch (primitiveType) {
case "number":
return 0.0
case "integer":
return 0
case "string":
return "string"
case "boolean":
return true
}
return null
}
// Use carefully, the schema type should necessarily be primitive
// TODO(agarwal): Use Enum values, if any
const generatePrimitiveRequestBodyExample = (
schemaObject: OpenAPIV31.NonArraySchemaObject
): RequestBodyExample =>
getPrimitiveTypePlaceholder(schemaObject.type as PrimitiveSchemaType)
// Use carefully, the schema type should necessarily be object
const generateObjectRequestBodyExample = (
schemaObject: OpenAPIV31.NonArraySchemaObject
): RequestBodyExample =>
pipe(
schemaObject.properties,
O.fromNullable,
O.map(
(properties) =>
Object.entries(properties) as [string, OpenAPIV31.SchemaObject][]
),
O.getOrElseW(() => [] as [string, OpenAPIV31.SchemaObject][]),
A.reduce(
{} as { [name: string]: RequestBodyExample },
(aggregatedExample, property) => {
aggregatedExample[property[0]] =
generateRequestBodyExampleFromSchemaObject(property[1])
return aggregatedExample
}
)
)
// Use carefully, the schema type should necessarily be mixed array
const generateMixedArrayRequestBodyEcample = (
schemaObject: OpenAPIV31.SchemaObject
): RequestBodyExample =>
pipe(
schemaObject,
(schemaObject) => schemaObject.type as MixedArraySchemaType,
A.reduce([] as Array<RequestBodyExample>, (aggregatedExample, itemType) => {
// TODO: Figure out how to include non-primitive types as well
if (isSchemaTypePrimitive(itemType)) {
aggregatedExample.push(getPrimitiveTypePlaceholder(itemType))
}
return aggregatedExample
})
)
const generateArrayRequestBodyExample = (
schemaObject: OpenAPIV31.ArraySchemaObject
): RequestBodyExample => [
generateRequestBodyExampleFromSchemaObject(
schemaObject.items as OpenAPIV31.SchemaObject
),
]
const generateRequestBodyExampleFromSchemaObject = (
schemaObject: OpenAPIV31.SchemaObject
): RequestBodyExample => {
// TODO: Handle schema objects with oneof or anyof
if (schemaObject.example) return schemaObject.example as RequestBodyExample
if (schemaObject.examples)
return schemaObject.examples[0] as RequestBodyExample
if (!schemaObject.type) return ""
if (isSchemaTypePrimitive(schemaObject.type))
return generatePrimitiveRequestBodyExample(
schemaObject as OpenAPIV31.NonArraySchemaObject
)
if (schemaObject.type === "object")
return generateObjectRequestBodyExample(schemaObject)
if (schemaObject.type === "array")
return generateArrayRequestBodyExample(schemaObject)
return generateMixedArrayRequestBodyEcample(schemaObject)
}
export const generateRequestBodyExampleFromMediaObject = (
mediaObject: OpenAPIV31.MediaTypeObject
): RequestBodyExample => {
if (mediaObject.example) return mediaObject.example as RequestBodyExample
if (mediaObject.examples) return mediaObject.examples[0] as RequestBodyExample
return mediaObject.schema
? generateRequestBodyExampleFromSchemaObject(mediaObject.schema)
: ""
}

View File

@@ -24,8 +24,12 @@ import * as S from "fp-ts/string"
import * as O from "fp-ts/Option"
import * as TE from "fp-ts/TaskEither"
import * as RA from "fp-ts/ReadonlyArray"
import { step } from "../steps"
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "."
import { step } from "../../steps"
import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "../"
import { generateRequestBodyExampleFromMediaObject as generateExampleV31 } from "./exampleV31"
import { generateRequestBodyExampleFromMediaObject as generateExampleV3 } from "./exampleV3"
import { generateRequestBodyExampleFromOpenAPIV2Body } from "./exampleV2"
import { prettyPrintJSON } from "~/helpers/functional/json"
export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const
@@ -114,8 +118,12 @@ const parseOpenAPIV2Body = (op: OpenAPIV2.OperationObject): HoppRESTReqBody => {
if (
obj !== "multipart/form-data" &&
obj !== "application/x-www-form-urlencoded"
)
return { contentType: obj as any, body: "" }
) {
return {
contentType: obj as any,
body: generateRequestBodyExampleFromOpenAPIV2Body(op),
}
}
const formDataValues = pipe(
(op.parameters ?? []) as OpenAPIV2.Parameter[],
@@ -178,7 +186,8 @@ const parseOpenAPIV3BodyFormData = (
}
const parseOpenAPIV3Body = (
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject
op: OpenAPIV3.OperationObject | OpenAPIV31.OperationObject,
isV31Request: boolean
): HoppRESTReqBody => {
const objs = Object.entries(
(
@@ -197,11 +206,20 @@ const parseOpenAPIV3Body = (
OpenAPIV3.MediaTypeObject | OpenAPIV31.MediaTypeObject
] = objs[0]
const exampleBody = pipe(
prettyPrintJSON(
isV31Request
? generateExampleV31(media as OpenAPIV31.MediaTypeObject)
: generateExampleV3(media as OpenAPIV3.MediaTypeObject)
),
O.getOrElse(() => "")
)
return contentType in knownContentTypes
? contentType === "multipart/form-data" ||
contentType === "application/x-www-form-urlencoded"
? parseOpenAPIV3BodyFormData(contentType, media)
: { contentType: contentType as any, body: "" }
: { contentType: contentType as any, body: exampleBody }
: { contentType: null, body: null }
}
@@ -213,12 +231,20 @@ const isOpenAPIV3Operation = (
typeof doc.openapi === "string" &&
doc.openapi.startsWith("3.")
const isOpenAPIV31Operation = (
doc: OpenAPI.Document,
op: OpenAPIOperationType
): op is OpenAPIV31.OperationObject =>
objectHasProperty(doc, "openapi") &&
typeof doc.openapi === "string" &&
doc.openapi.startsWith("3.1")
const parseOpenAPIBody = (
doc: OpenAPI.Document,
op: OpenAPIOperationType
): HoppRESTReqBody =>
isOpenAPIV3Operation(doc, op)
? parseOpenAPIV3Body(op)
? parseOpenAPIV3Body(op, isOpenAPIV31Operation(doc, op))
: parseOpenAPIV2Body(op)
const resolveOpenAPIV3SecurityObj = (