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

[BUG] In documentation collection refined as NonEmpty does not appear as required #3828

Closed
dacr opened this issue Jun 6, 2024 · 1 comment · Fixed by #3839
Closed

[BUG] In documentation collection refined as NonEmpty does not appear as required #3828

dacr opened this issue Jun 6, 2024 · 1 comment · Fixed by #3839
Assignees
Labels
bug Something isn't working

Comments

@dacr
Copy link

dacr commented Jun 6, 2024

Tapir version: 1.10.8

Scala version: 3.4.2

What is the problem?

In the following record :

  case class Salute(
    name: String Refined NonEmpty,
    age: Option[Int Refined Positive],
    gender: Option[Gender],
    // nicknames: List[String], // https://github.com/softwaremill/tapir/issues/993
    nicknames: List[String Refined (MinSize[3] And MaxSize[6])] Refined NonEmpty
  ) derives JsonCodec

The field nicknames doesn´t appear as required in the openapi generated documentation although it has been refined as NonEmpty
as you can see in this capture :
image

How to reproduce?

// ---------------------
//> using scala "3.4.2"
//> using dep "com.softwaremill.sttp.tapir::tapir-zio:1.10.8"
//> using dep "com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.8"
//> using dep "com.softwaremill.sttp.tapir::tapir-refined:1.10.8"
//> using dep "com.softwaremill.sttp.tapir::tapir-json-zio:1.10.8"
//> using dep "com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.8"
//> using dep "dev.zio::zio-json-interop-refined:0.7.0"
// ---------------------

import sttp.tapir.ztapir.*
import sttp.tapir.server.ziohttp.ZioHttpInterpreter
import sttp.apispec.openapi.Info
import sttp.tapir.swagger.bundle.SwaggerInterpreter
import sttp.tapir.Schema.annotations.customise
import sttp.tapir.json.zio.*
import sttp.tapir.Schema
import sttp.tapir.generic.auto.*
import sttp.tapir.codec.refined.*
import eu.timepit.refined.*
import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto.*
import eu.timepit.refined.boolean.And
import eu.timepit.refined.collection.{MaxSize, MinSize, NonEmpty}
import eu.timepit.refined.numeric.Positive
import zio.*
import zio.json.*
import zio.json.interop.refined.*
import zio.http.Server

object WebApp extends ZIOAppDefault {

  enum Gender derives JsonCodec {
    case Female, Male
  }

  object Gender {
    given JsonEncoder[Gender] = JsonEncoder[String].contramap(p => p.toString)
    given JsonDecoder[Gender] = JsonDecoder[String].map(p => Gender.valueOf(p))
    given Schema[Gender]      = Schema.derivedEnumeration.defaultStringBased
  }

  case class Salute(
    name: String Refined NonEmpty,
    age: Option[Int Refined Positive],
    gender: Option[Gender],
    // nicknames: List[String], // https://github.com/softwaremill/tapir/issues/993
    nicknames: List[String Refined (MinSize[3] And MaxSize[6])] Refined NonEmpty
  ) derives JsonCodec


  case class Greeting(message: String) derives JsonCodec

  // --------------------------------------------------
  def helloLogic(salute: Salute): UIO[Greeting] = {
    for {
      nickName <- Random.shuffle(salute.nicknames.toList).map(_.headOption.map(_.value)) // .when(!salute.nicknames.isEmpty)
      name      = nickName.getOrElse(salute.name)
      message   = salute match {
                    case Salute(name, Some(foundAge), _, _) if foundAge < 18  => s"Hi $name"
                    case Salute(name, Some(foundAge), Some(Gender.Female), _) => s"Hello madam $name"
                    case Salute(name, Some(foundAge), Some(Gender.Male), _)   => s"Hello mister $name"
                    case Salute(name, _, _, _)                                => s"Good morning $name"
                  }
    } yield Greeting(message)
  }

  val helloEndPoint =
    endpoint
      .tag("Greetings")
      .name("hello")
      .description("Returns stateful greeting")
      .post
      .in("hello")
      //.in(jsonBody[Salute].example(Salute("Joe", None, None, List("joe"))))
      .in(jsonBody[Salute])
      .out(jsonBody[Greeting])

  val helloRoute = helloEndPoint.zServerLogic[Any](helloLogic)

  // --------------------------------------------------

  val apiDocRoute =
    SwaggerInterpreter()
      .fromServerEndpoints(
        List(helloRoute),
        Info(
          title = "Greeting API",
          version = "1.0",
          description = Some("Everything required to be polite")
        )
      )

  // --------------------------------------------------

  val routes = ZioHttpInterpreter().toHttp(List(helloRoute) ++ apiDocRoute)

  // --------------------------------------------------
  val server = for {
    _ <- ZIO.log("API documentation : http://127.0.0.1:8080/docs")
    _ <- Server.serve(routes)
  } yield ()

  override def run = server.provide(Server.default)
}

WebApp.main(Array.empty)
@dacr
Copy link
Author

dacr commented Jun 15, 2024

Fixed 👍
image

The full code example as a scala-cli script : https://gist.github.com/dacr/8ec983f3dc90da617dd1c816eca760af

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
Development

Successfully merging a pull request may close this issue.

2 participants