softwaremill / tapir

Rapid development of self-documenting APIs
https://tapir.softwaremill.com
Apache License 2.0
1.33k stars 404 forks source link

Simple default bundle compatible with Scala toolkit #3914

Open bishabosha opened 1 week ago

bishabosha commented 1 week ago

Do you think it would be possible to bundle a bunch of defaults into an object for building simple apps in direct style e.g. JdkHttpServer, Upickle json

Then there can be fewer imports needed

with this simple bundle it would be useful to recommend for bundling with scala toolkit

bishabosha commented 1 week ago

for example, if I bundle the jdkhttp, and upickle json handlers in one object like this

package scala.toolkit

import sttp.tapir.Tapir
import sttp.tapir.server.jdkhttp
import sttp.tapir.json.upickle.TapirJsonuPickle

import scala.util.Try

object httpserver extends Tapir
  with TapirJsonuPickle:

  private val ujsonSchema = Schema.schemaForString.map(
    s => Try(ujson.read(s)).toOption
  )(ujson.write(_))

  given Schema[ujson.Value] = ujsonSchema

  def ujsonBody = jsonBody[ujson.Value]

  export sttp.tapir.Schema
  export jdkhttp.{HttpServer, HttpsConfigurator}
  export jdkhttp.{JdkHttpResponseBody, JdkHttpServer, JdkHttpServerInterpreter, JdkHttpServerOptions}

then I can make a very simple main app like so

package app

import scala.toolkit.httpserver.*
import java.time.LocalDate

val date = endpoint.get.in("date").out(ujsonBody)
  .handleSuccess(_ => ujson.Obj("date" -> LocalDate.now().toString))

@main def launch() =
  val server = JdkHttpServer()
    .addEndpoint(date)
    .port(8080)
    .host("localhost")
    .executor(scala.concurrent.ExecutionContext.global)
    .start()

  println("Server started at http://localhost:8080, ctrl-c to stop")

  sys.addShutdownHook:
    server.stop(0)

An open question is then "what about JDK 21?" - then we could bundle the NettySyncServer instead maybe? we'd have to make a policy in scala/toolkit about JDK support though

adamw commented 1 week ago

Definitely NettySyncServer is the way to go, it's more feature-full (websockets work, multiparts are coming). Before exports we took this approach: https://tapir.softwaremill.com/en/latest/mytapir.html, though I think it would be most useful for customising schema derivation etc.

bishabosha commented 1 week ago

ok so would you accept a PR to add a new artefact that can bundle a few of these things in one import? (based on NettySyncServer, and upickle, probably add the ujsonBody directly to the tapir-json-upickle artifact)

bishabosha commented 1 week ago

I see also that NettySyncServer requires Ox and is only Scala 3, so this complicates it possibly

adamw commented 1 week ago

Sure why not - we already have a "swagger bundle", the same way we could have a "direct bundle".

I don't understand the ujsonBody, though? We could also consider using pickler in that https://tapir.softwaremill.com/en/latest/endpoint/pickler.html

bishabosha commented 1 week ago

Oh I wasn't aware of this module - it seems good - the reason I defined ujsonBody in this example was so you can do very basic serialisation of ujson.Value without needing derivation or explicit type parameter, e.g. ujson.Obj("date" -> LocalDate.now().toString)

adamw commented 1 week ago

So there are no default ReadWriters for ujson.Values? If so, we could just provide Schema instances, the way it's done e.g. with circe - no separate methods needed: https://github.com/softwaremill/tapir/blob/7dddc5795669b776c25f2ad7d09be84dc552ec8f/json/circe/src/main/scala/sttp/tapir/json/circe/TapirJsonCirce.scala