Skip to content

Instantly share code, notes, and snippets.

@dacr
Created June 15, 2024 07:58
Show Gist options
  • Save dacr/8ec983f3dc90da617dd1c816eca760af to your computer and use it in GitHub Desktop.
Save dacr/8ec983f3dc90da617dd1c816eca760af to your computer and use it in GitHub Desktop.
tapir with refined for data validity and better documentation - simple way to build refined instances ? / published by https://github.com/dacr/code-examples-manager #f2e2cb9b-7a0b-4bf9-93c9-8c5596ab50b7/791d7ef2ff2d31b7eb30520d54b22e765f7518df
// summary : tapir with refined for data validity and better documentation - simple way to build refined instances ?
// keywords : scala, zio, tapir, refined, @testable, @exclusive
// publish : gist
// authors : David Crosson
// license : Apache NON-AI License Version 2.0 (https://raw.githubusercontent.com/non-ai-licenses/non-ai-licenses/main/NON-AI-APACHE2)
// id : f2e2cb9b-7a0b-4bf9-93c9-8c5596ab50b7
// created-on : 2024-06-06T11:11:45+02:00
// managed-by : https://github.com/dacr/code-examples-manager
// run-with : scala-cli $file
// test-with : curl -L http://127.0.0.1:8080/hello/david
// ---------------------
//> using scala "3.4.2"
//> using dep "com.softwaremill.sttp.tapir::tapir-zio:1.10.9"
//> using dep "com.softwaremill.sttp.tapir::tapir-zio-http-server:1.10.9"
//> using dep "com.softwaremill.sttp.tapir::tapir-refined:1.10.9"
//> using dep "com.softwaremill.sttp.tapir::tapir-json-zio:1.10.9"
//> using dep "com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.10.9"
//> 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.auto.*
import eu.timepit.refined.numeric.*
import eu.timepit.refined.api.*
import eu.timepit.refined.boolean.*
import eu.timepit.refined.collection.*
import eu.timepit.refined.string.*
import eu.timepit.refined.generic.*
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
}
type SaluteName = String Refined NonEmpty
object SaluteName extends RefinedTypeOps[SaluteName, String]
type Age = Int Refined Positive
object Age extends RefinedTypeOps.Numeric[Age, Int]
type NickName = String Refined (MinSize[3] And MaxSize[6])
object NickName extends RefinedTypeOps[NickName, String]
type NickNames = List[NickName] Refined NonEmpty
object NickNames extends RefinedTypeOps[NickNames, List[NickName]]
case class Salute(
name: SaluteName,
age: Option[Age],
gender: Option[Gender],
nicknames: NickNames
) derives JsonCodec
case class Greeting(message: String) derives JsonCodec
// --------------------------------------------------
// val age: Age = 42 // only possible using scala 2 :( Waiting for an update
// val name: SaluteName = "jonathan" // only possible using scala 2 :( Waiting for an update
// val nicknames: NickNames = List("joe") // only possible using scala 2 :( Waiting for an update
val age: Age = Age.unsafeFrom(42)
val name: SaluteName = SaluteName.unsafeFrom("jonathan")
val nickName: NickName = NickName.unsafeFrom("joe")
val nicknames: NickNames = NickNames.unsafeFrom(List(nickName))
val saluteExample =
Salute(
name,
None,
None,
nicknames
)
val helloEndPoint =
endpoint
.tag("Greetings")
.name("hello")
.description("Returns stateful greeting")
.post
.in("hello")
.in(jsonBody[Salute].example(saluteExample))
.out(jsonBody[Greeting])
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 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment