Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OpenAPI Generator missing endpoints #2541

Closed
daharon opened this issue Dec 4, 2023 · 2 comments · Fixed by #2542
Closed

OpenAPI Generator missing endpoints #2541

daharon opened this issue Dec 4, 2023 · 2 comments · Fixed by #2542
Labels
bug Something isn't working

Comments

@daharon
Copy link
Contributor

daharon commented Dec 4, 2023

Describe the bug
I'm testing the new OpenAPIGen functionality, and I'm getting output that doesn't match my expectations.
I have two endpoints, one GET and one POST to the path /books.
The POST request is the only one that shows up in the generated OpenAPI document.

If I remove the POST endpoint, then I do see the GET endpoint in the generated OpenAPI document.

To Reproduce
Here is an example:

package dev.zioopenapi.test

import zio.{ExitCode, URIO, ZIO, ZIOAppDefault}
import zio.http.{MediaType, Method, Status}
import zio.http.endpoint.Endpoint
import zio.http.endpoint.openapi.OpenAPIGen
import zio.schema.{DeriveSchema, Schema}

import java.io.{File, FileWriter}
import java.time.Year

final case class Book(
    title: String,
    author: String,
    yearPublished: Year,
)
object Book:
  given Schema[Book] = DeriveSchema.gen
  given Schema[Year] = Schema.primitive[Int].transform(Year.of, _.getValue)

object RestApi:
  final case class GetBooksResponse(books: List[Book])
  private object GetBooksResponse:
    given Schema[GetBooksResponse] = DeriveSchema.gen

  val getBooksEndpoint =
    Endpoint(Method.GET / "books")
      .out[GetBooksResponse](MediaType.application.json)

  val addBookEndpoint =
    Endpoint(Method.POST / "books")
      .in[Book]
      .out[String](Status.Created)

  val endpoints = Vector(getBooksEndpoint, addBookEndpoint)

  val openApiDoc =
    OpenAPIGen
      .fromEndpoints(
        title = "OpenAPI Generator Example",
        version = "0.0.1",
        endpoints
      )
      .toJsonPretty

object OpenApiExample extends ZIOAppDefault:
  override def run: URIO[Any, ExitCode] =
    def openFile(fileName: String)    = ZIO.attemptBlockingIO(new FileWriter(new File(fileName)))
    def closeFile(writer: FileWriter) = ZIO.attemptBlockingIO(writer.close()).orDie
    ZIO
      .acquireReleaseWith(openFile("openapi.json"))(closeFile) { writer =>
        ZIO.attemptBlockingIO(writer.write(RestApi.openApiDoc))
      }
      .exitCode

And here is the resulting OpenAPI document:

{
  "openapi" : "3.1.0",
  "info" : {
    "title" : "OpenAPI Generator Example",
    "version" : "0.0.1"
  },
  "paths" : {
    "/books" : {
      "post" : {
        "requestBody" : 
          {
          "content" : {
            "application/json" : {
              "schema" : 
                {
                "$ref" : "#/components/schemas/Book"
              }
            }
          },
          "required" : true
        },
        "responses" : {
          "201" : 
            {
            "description" : "",
            "content" : {
              "application/json" : {
                "schema" : 
                  {
                  "type" : 
                    "string"
                }
              }
            }
          }
        },
        "deprecated" : false
      }
    }
  },
  "components" : {
    "schemas" : {
      "Book" : 
        {
        "type" : 
          "object",
        "properties" : {
          "title" : {
            "type" : 
              "string"
          },
          "author" : {
            "type" : 
              "string"
          },
          "yearPublished" : {
            "type" : 
              "integer",
            "format" : "int32"
          }
        },
        "additionalProperties" : 
          true,
        "required" : [
          "title",
          "author",
          "yearPublished"
        ]
      },
      "GetBooksResponse" : 
        {
        "type" : 
          "object",
        "properties" : {
          "books" : {
            "type" : 
              "array",
            "items" : {
              "type" : 
                "object",
              "properties" : {
                "title" : {
                  "type" : 
                    "string"
                },
                "author" : {
                  "type" : 
                    "string"
                },
                "yearPublished" : {
                  "type" : 
                    "integer",
                  "format" : "int32"
                }
              },
              "additionalProperties" : 
                true,
              "required" : [
                "title",
                "author",
                "yearPublished"
              ]
            }
          }
        },
        "additionalProperties" : 
          true,
        "required" : [
          "books"
        ]
      }
    }
  }
}

As can be seen, the GET /books endpoint is missing.

@daharon daharon added the bug Something isn't working label Dec 4, 2023
@daharon
Copy link
Contributor Author

daharon commented Dec 4, 2023

I narrowed it down to the following line:


Where the two Endpoints share the same path, and since the paths is of type Map[OpenAPI.Path, OpenAPI.PathItem] only one PathItem survives the concatenation of the two.

So no matter how many methods one has for a given path, only one will make it out to the OpenAPI AST.

@daharon
Copy link
Contributor Author

daharon commented Dec 4, 2023

Rather than the example application I posted above, it might be easier to start with the following failing test:

diff --git a/zio-http/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala b/zio-http/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala
index 7c0b25db..919f9089 100644
--- a/zio-http/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala
+++ b/zio-http/src/test/scala/zio/http/endpoint/openapi/OpenAPIGenSpec.scala
@@ -9,7 +9,7 @@ import zio.schema.annotation.{caseName, discriminatorName, noDiscriminator, opti
 import zio.schema.codec.JsonCodec
 import zio.schema.{DeriveSchema, Schema}
 
-import zio.http.Method.GET
+import zio.http.Method.{GET, POST}
 import zio.http._
 import zio.http.codec.{Doc, HttpCodec, QueryCodec}
 import zio.http.endpoint._
@@ -2228,6 +2228,80 @@ object OpenAPIGenSpec extends ZIOSpecDefault {
             |}""".stripMargin
         assertTrue(json == toJsonAst(expectedJson))
       },
+      test("multiple endpoints to same path") {
+        val getEndpoint  = Endpoint(GET / "test")
+          .out[String](MediaType.text.`plain`)
+        val postEndpoint = Endpoint(POST / "test")
+          .in[String]
+          .out[String](Status.Created, MediaType.text.`plain`)
+        val generated    = OpenAPIGen.fromEndpoints("Multiple Endpoints - Same Path", "1.0", getEndpoint, postEndpoint)
+        val json         = toJsonAst(generated)
+        val expectedJson =
+          """{
+            |  "openapi": "3.1.0",
+            |  "info": {
+            |    "title": "Multiple Endpoints - Same Path",
+            |    "version": "1.0"
+            |  },
+            |  "paths": {
+            |    "/test": {
+            |      "get": {
+            |        "requestBody": {
+            |          "content": {
+            |            "application/json": {
+            |              "schema": {
+            |                "type": "null"
+            |              }
+            |            }
+            |          },
+            |          "required": false
+            |        },
+            |        "responses": {
+            |          "200": {
+            |            "description": "",
+            |            "content": {
+            |              "text/plain": {
+            |                "schema": {
+            |                  "type": "string"
+            |                }
+            |              }
+            |            }
+            |          }
+            |        },
+            |        "deprecated": false
+            |      },
+            |      "post": {
+            |        "requestBody": {
+            |          "content": {
+            |            "application/json": {
+            |              "schema": {
+            |                "type": "string"
+            |              }
+            |            }
+            |          },
+            |          "required": true
+            |        },
+            |        "responses": {
+            |          "201": {
+            |            "description": "",
+            |            "content": {
+            |              "text/plain": {
+            |                "schema": {
+            |                  "type": "string"
+            |                }
+            |              }
+            |            }
+            |          }
+            |        },
+            |        "deprecated": false
+            |      }
+            |    }
+            |  },
+            |  "components": {}
+            |}
+            |""".stripMargin
+        assertTrue(json == toJsonAst(expectedJson))
+      },
     )
 
 }

daharon added a commit to daharon/zio-http that referenced this issue Dec 4, 2023
daharon added a commit to daharon/zio-http that referenced this issue Dec 5, 2023
daharon added a commit to daharon/zio-http that referenced this issue Dec 5, 2023
daharon added a commit to daharon/zio-http that referenced this issue Dec 5, 2023
daharon added a commit to daharon/zio-http that referenced this issue Dec 5, 2023
987Nabil pushed a commit that referenced this issue Dec 8, 2023
…methods for the same path. (#2542)

* #2541 Fix bug where the OpenAPI generator fails to generate multiple methods for the same path.

* Update version in README.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
1 participant