elysiajs / elysia

Ergonomic Framework for Humans
https://elysiajs.com
MIT License
10.56k stars 225 forks source link

Properly respect enums in OpenAPI/Swagger documentation #512

Open ethndotsh opened 8 months ago

ethndotsh commented 8 months ago

What is the problem this feature would solve?

Right now if a Typebox enum is passed into the response field on a route it will just be shown as "string" in Swagger UI or Scalar, and in the actual Swagger JSON it is currently represented as:

"type": {
  "anyOf": [
    {
      "const": "PUBLIC",
      "type": "string"
    },
    {
      "const": "PRIVATE",
      "type": "string"
    },
    {
      "const": "RESERVED",
      "type": "string"
    },
    {
      "const": "STUDIO",
      "type": "string"
    }
  ]
}

For it to be properly shown as an enum in Swagger UI and Scalar, this should be changed to generate as:

"type": {
  "type": "string",
  "enum": [
    "PUBLIC",
    "PRIVATE",
    "RESERVED",
    "STUDIO"
  ]
}

What is the feature you are proposing to solve the problem?

Adjusting how the values are generated for OpenAPI

What alternatives have you considered?

No response

ethndotsh commented 8 months ago

Could've sworn there was a solution here from the author of Typebox. Am I losing it or was it deleted?

sinclairzx81 commented 8 months ago

@ethndotsh Hi, sorry, I had removed the previous snippet but meant to re-post after I had a deeper look at how Elysia was handling type registration. Here is the snippet again (slightly modified to support deriving the type keyword)

TypeScript Link Here

import { Kind, Static, TSchema, SchemaOptions, TypeRegistry } from '@sinclair/typebox'

// -------------------------------------------------------------------------------------
// TypeRegistry
// -------------------------------------------------------------------------------------
TypeRegistry.Set<TUnionEnum>('UnionEnum', (schema, value) => {
  return (typeof value === 'string' || typeof value === 'number') && schema.enum.includes(value as never)
})
// -------------------------------------------------------------------------------------
// TUnionEnum
// -------------------------------------------------------------------------------------
export type TUnionValue = string | number

export interface TUnionEnum<T extends TUnionValue[] = []> extends TSchema {
  [Kind]: 'UnionEnum'
  static: T[number]
  enum: T
}
// -------------------------------------------------------------------------------------
// UnionEnum
// -------------------------------------------------------------------------------------
/** `[Elysia]` Creates a Union type with a enum array schema representation  */
export function UnionEnum<T extends TUnionValue[]>(values: [...T], options: SchemaOptions = {}) {
  const type = (
    values.every(value => typeof value === 'string') ? { type: 'string' } :
    values.every(value => typeof value === 'number') ? { type: 'number' } :
    {} // mixed string | number
  )
  return { ...options, [Kind]: 'UnionEnum', ...type, enum: values } as TUnionEnum<T>
}

// -------------------------------------------------------------------------------------
// Usage
// -------------------------------------------------------------------------------------

const T = UnionEnum(['A', 'B', 'C']) // const T = { type: 'string', enum: ['A', 'B', 'C'] }

type T = Static<typeof T>            // type T = 'A' | 'B' | 'C'

It should be possible to include the above as a module in your project; with Elysia able to use it so long as the type is registered. I had noted that there is potential here to include the type on Elysia t for the benefit of documentation (as quite a few OpenAPI / Swagger users ask for this enum representation specifically for documentation generation)

The above should provide a good reference point :) Hope this helps! S

kravetsone commented 2 months ago

@ethndotsh Hi, sorry, I had removed the previous snippet but meant to re-post after I had a deeper look at how Elysia was handling type registration. Here is the snippet again (slightly modified to support deriving the type keyword)

TypeScript Link Here

import { Kind, Static, TSchema, SchemaOptions, TypeRegistry } from '@sinclair/typebox'

// -------------------------------------------------------------------------------------
// TypeRegistry
// -------------------------------------------------------------------------------------
TypeRegistry.Set<TUnionEnum>('UnionEnum', (schema, value) => {
  return (typeof value === 'string' || typeof value === 'number') && schema.enum.includes(value as never)
})
// -------------------------------------------------------------------------------------
// TUnionEnum
// -------------------------------------------------------------------------------------
export type TUnionValue = string | number

export interface TUnionEnum<T extends TUnionValue[] = []> extends TSchema {
  [Kind]: 'UnionEnum'
  static: T[number]
  enum: T
}
// -------------------------------------------------------------------------------------
// UnionEnum
// -------------------------------------------------------------------------------------
/** `[Elysia]` Creates a Union type with a enum array schema representation  */
export function UnionEnum<T extends TUnionValue[]>(values: [...T], options: SchemaOptions = {}) {
  const type = (
    values.every(value => typeof value === 'string') ? { type: 'string' } :
    values.every(value => typeof value === 'number') ? { type: 'number' } :
    {} // mixed string | number
  )
  return { ...options, [Kind]: 'UnionEnum', ...type, enum: values } as TUnionEnum<T>
}

// -------------------------------------------------------------------------------------
// Usage
// -------------------------------------------------------------------------------------

const T = UnionEnum(['A', 'B', 'C']) // const T = { type: 'string', enum: ['A', 'B', 'C'] }

type T = Static<typeof T>            // type T = 'A' | 'B' | 'C'

It should be possible to include the above as a module in your project; with Elysia able to use it so long as the type is registered. I had noted that there is potential here to include the type on Elysia t for the benefit of documentation (as quite a few OpenAPI / Swagger users ask for this enum representation specifically for documentation generation)

The above should provide a good reference point :) Hope this helps! S

btw

Why you wont do it out of the box?

is it have some restriction?

kravetsone commented 2 months ago

I add it to Elysia type-system in https://github.com/elysiajs/elysia/pull/808