zio / zio-http

A next-generation Scala framework for building scalable, correct, and efficient HTTP clients and servers
https://zio.dev/zio-http
Apache License 2.0
748 stars 379 forks source link

Endpoint API: OpenAPI documentation generator crashes on parameterized case classes #2767

Open dubinsky opened 2 months ago

dubinsky commented 2 months ago

Describe the bug

With parameterized case class used as endpoint's output, Open API documentation generator crashes.

To Reproduce Steps to reproduce the behaviour:

  1. Run the following code:
//> using scala 3.4.0
//> using dep dev.zio::zio-http:3.0.0-RC6

import zio.http.Method
import zio.http.endpoint.Endpoint
import zio.http.endpoint.openapi.JsonSchema.SchemaStyle
import zio.http.endpoint.openapi.OpenAPIGen
import zio.schema.{DeriveSchema, Schema}

object OpenAPIBug:
  final case class Item(isEdited: Boolean)
  given Schema[Item] = DeriveSchema.gen[Item]
  final case class Data[A](data: List[A])
  given dataSchema[A: Schema]: Schema[Data[A]] = DeriveSchema.gen[Data[A]]
  def main(args: Array[String]): Unit = OpenAPIGen.fromEndpoints(SchemaStyle.Reference, List(
    Endpoint(Method.GET / "bug").out[Data[Item]]
  ))
  1. Observe the following exception:
java.util.NoSuchElementException: None.get
  at zio.http.endpoint.openapi.OpenAPIGen$$anon$7.applyOrElse(OpenAPIGen.scala:707)

(Caused by the fact that zio.http.endpoint.openapi.OpenAPI.Key.fromString("Data[A]") returns None.)

Expected behaviour I expect correct Open API documentation to be generated, or, at a minimum, no crashes ;)

lachezar commented 1 month ago

I’ve also experienced the same issue as reported.

Update:

I've found a workaround - manually generating the Schema with a simple id.

Example:


given [V <: ValidationStatus]: Schema[CreateItemInput[V]] =
  Schema.CaseClass3[String, Money, ProductType, CreateItemInput[V]](
    id0 = TypeId.fromTypeName("CreateItemInput"), // <- "simple" id
    field01 = Schema.Field(name0 = "name", schema0 = Schema[String], get0 = _.name, set0 = (v, x) => v.copy(name = x)),
    field02 =
      Schema.Field(name0 = "price", schema0 = Schema[Money], get0 = _.price, set0 = (v, x) => v.copy(price = x)),
    field03 = Schema.Field(
      name0 = "productType",
      schema0 = Schema[ProductType],
      get0 = _.productType,
      set0 = (v, x) => v.copy(productType = x),
    ),
    construct0 = (name, price, productType) => CreateItemInput[V](name, price, productType),
  )
jdegoes commented 4 weeks ago

/bounty $150

algora-pbc[bot] commented 4 weeks ago

💎 $150 bounty • ZIO

Steps to solve:

  1. Start working: Comment /attempt #2767 with your implementation plan
  2. Submit work: Create a pull request including /claim #2767 in the PR body to claim the bounty
  3. Receive payment: 100% of the bounty is received 2-5 days post-reward. Make sure you are eligible for payouts

Thank you for contributing to zio/zio-http!

Add a bountyShare on socials

Attempt Started (GMT+0) Solution
🔴 @Anshgrover23 Jun 14, 2024, 7:20:13 AM WIP
Anshgrover23 commented 2 weeks ago

hey i have submitted the pr plss check

countfloyd commented 2 weeks ago

I think it would be nice if the name generated for openapi be specified with its type parameters (i.e. Data_String) instead of just Data. Systems that consume the openapi json or yaml file could then generate the correct code for different instances of the parameterized type.

987Nabil commented 2 weeks ago

@jdegoes This problem is not solvable in a clean way in zio-http. We need to fix this in zio-schema.

  1. The Scala 3 macro behaves different then the Scala 2 macro, by adding to the typeName of the id [A] for the example given in the issue. This is imo just wrong.
  2. We can't do what @countfloyd suggested -> make the generically filled types part of the OpenAPI schema name. This is because zio-schema does not store this information at all. I think we should store in a Product/Enum schema which generics are filled with which schemas and the fields that are of this generic. Maybe something like generics: Map[String, GenericInfo] where the key is just the generic name (A here) and case class GenericInfo(implementigSchema: Schema, fieldNames: List[String] ). I guess field names should be enough to do the mapping where needed, for example to get the schema by finding the field with such name.