Last active
August 18, 2023 07:29
-
-
Save dacr/75e31e80fc6371c37ebb43ab9ece7855 to your computer and use it in GitHub Desktop.
Chronos API using zio, tapir and zhttp / published by https://github.com/dacr/code-examples-manager #84978260-6962-410a-8fca-dfe6e187262c/d4375f2899c6b79cff01d4730f821f9b00b346f8
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// summary : Chronos API using zio, tapir and zhttp | |
// keywords : scala, zio, tapir, http, http-server, zhttp, stateful, state, dto, json, chronos, @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 : 84978260-6962-410a-8fca-dfe6e187262c | |
// created-on : 2022-03-21T07:42:42+01:00 | |
// managed-by : https://github.com/dacr/code-examples-manager | |
// run-with : scala-cli $file | |
// test-with : curl http://127.0.0.1:8080/chronos | |
/* | |
START IT AND VISIT http://127.0.0.1:8080/docs FOR API DOCUMENTATION | |
*/ | |
// --------------------- | |
//> using scala "3.3.0" | |
//> using dep "com.softwaremill.sttp.tapir::tapir-zio:1.6.0" | |
//> using dep "com.softwaremill.sttp.tapir::tapir-zio-http-server:1.6.0" | |
//> using dep "com.softwaremill.sttp.tapir::tapir-json-zio:1.6.0" | |
////> using dep "com.softwaremill.sttp.tapir::tapir-redoc-bundle:1.6.0" | |
//> using lib "com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.6.0" | |
//> using dep "dev.zio::zio-json:0.5.0" | |
// --------------------- | |
import zio.* | |
import zio.json.* | |
import java.time.Instant | |
import sttp.tapir.ztapir.* | |
import sttp.tapir.server.ziohttp.ZioHttpInterpreter | |
//import sttp.tapir.redoc.bundle.RedocInterpreter | |
import sttp.tapir.swagger.bundle.SwaggerInterpreter | |
import sttp.tapir.json.zio.* | |
import sttp.tapir.generic.auto.* | |
import sttp.apispec.openapi.Info | |
import sttp.model.StatusCode.{NotFound, BadRequest} | |
import zio.http.Server | |
object WebApp extends ZIOAppDefault { | |
type UUID = String | |
// ------------------------------------------------------------------------------------------------------------------- | |
sealed trait ChronosFailure derives JsonCodec { | |
val message: String | |
} | |
case class ChronosNotFoundFailure(message: String, entityId: UUID) extends ChronosFailure derives JsonCodec | |
case class ChronosBadRequestFailure(message: String) extends ChronosFailure derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class TimerCreate( | |
name: String | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class CompetitorCreate( | |
firstName: String, | |
lastName: String, | |
birthYear: Int | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class ChronosCreate( | |
name: String, | |
description: String | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Competitor( | |
uuid: UUID, | |
firstName: String, | |
lastName: String, | |
birthYear: Int | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Timing( | |
name: String, | |
timestamp: Instant | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Timer( | |
uuid: UUID, | |
competitorUUID: UUID, | |
name: String, | |
created: Instant, | |
timings: List[Timing] = Nil | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Chronos( | |
uuid: UUID, | |
name: String, | |
description: String, | |
created: Instant, | |
timers: Map[UUID, Timer] = Map.empty, | |
competitors: Map[UUID, Competitor] = Map.empty | |
) derives JsonCodec { | |
def addCompetitor(competitor: Competitor) = copy(competitors = competitors + (competitor.uuid -> competitor)) | |
def addTimer(timer: Timer) = copy(timers = timers + (timer.uuid -> timer)) | |
def addTiming(timer: Timer, timing: Timing) = | |
copy(timers = timers ++ timers.get(timer.uuid).map(timer => timer.uuid -> timer.copy(timings = timing :: timer.timings))) | |
} | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class AppState(chronosMap: Map[UUID, Chronos] = Map.empty) derives JsonCodec { | |
def addChronos(chronos: Chronos): AppState = | |
copy(chronosMap = chronosMap + (chronos.uuid -> chronos)) | |
def addCompetitor(chronos: Chronos, competitor: Competitor): AppState = | |
copy(chronosMap = chronosMap + (chronos.uuid -> chronosMap(chronos.uuid).addCompetitor(competitor))) | |
def addTimer(chronos: Chronos, timer: Timer): AppState = | |
copy(chronosMap = chronosMap + (chronos.uuid -> chronosMap(chronos.uuid).addTimer(timer))) | |
def addTiming(chronos: Chronos, timer: Timer, timing: Timing): AppState = | |
copy(chronosMap = chronosMap + (timer.uuid -> chronosMap(timer.uuid).addTiming(timer, timing))) | |
} | |
// =================================================================================================================== | |
type ChronosEnv = Ref[AppState] | |
// ------------------------------------------------------------------------------------------------------------------- | |
val chronosList: ZIO[ChronosEnv, Nothing, List[Chronos]] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
state <- ref.get | |
} yield state.chronosMap.values.toList | |
val chronosListEndPoint = | |
endpoint | |
.name("chronos list") | |
.description("List all defined chronos") | |
.get | |
.in("chronos") | |
.out(jsonBody[List[Chronos]]) | |
val chronosListRoute = chronosListEndPoint.zServerLogic(_ => chronosList) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def chronosGet(id: UUID): ZIO[ChronosEnv, ChronosFailure, Chronos] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
state <- ref.get | |
chronos <- ZIO.from(state.chronosMap.get(id)).mapError(err => ChronosNotFoundFailure(s"Chronos not found", id)) | |
} yield chronos | |
val chronosGetEndPoint = | |
endpoint | |
.name("chronos get") | |
.description("get detailed information for given chronos") | |
.get | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.out(jsonBody[Chronos]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val chronosGetRoute = chronosGetEndPoint.zServerLogic(id => chronosGet(id)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def chronosCreate(body: ChronosCreate): ZIO[ChronosEnv, Nothing, Chronos] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
uuid <- Random.nextUUID | |
now <- Clock.instant | |
chronos = Chronos(uuid = uuid.toString, name = body.name, description = body.description, created = now) | |
state <- ref.getAndUpdate(_.addChronos(chronos)) | |
} yield chronos | |
val chronosCreateEndPoint = | |
endpoint | |
.name("chronos create") | |
.description("Create a new chronos") | |
.post | |
.in("chronos") | |
.in(jsonBody[ChronosCreate]) | |
.out(jsonBody[Chronos]) | |
val chronosCreateRoute = chronosCreateEndPoint.zServerLogic(arg => chronosCreate(arg)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def competitorList(chronosId: UUID): ZIO[ChronosEnv, ChronosFailure, List[Competitor]] = | |
for { | |
chronos <- chronosGet(chronosId) | |
competitors = chronos.competitors.values.toList | |
} yield competitors | |
val competitorListEndPoint = | |
endpoint | |
.name("competitors list") | |
.description("List defined competitors for given chronos") | |
.get | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("competitor") | |
.out(jsonBody[List[Competitor]]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val competitorListRoute = competitorListEndPoint.zServerLogic(chronosId => competitorList(chronosId)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def competitorGet(chronosId: UUID, competitorId: UUID): ZIO[ChronosEnv, ChronosFailure, Competitor] = | |
for { | |
chronos <- chronosGet(chronosId) | |
competitors <- ZIO | |
.from(chronos.competitors.get(competitorId)) | |
.mapError(err => ChronosNotFoundFailure(s"Competitor not found for chronos $chronosId", competitorId)) | |
} yield competitors | |
val competitorGetEndPoint = | |
endpoint | |
.name("competitor get") | |
.description("List defined competitors for given chronos") | |
.get | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("competitor") | |
.in(path[UUID]("competitorId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.out(jsonBody[Competitor]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val competitorGetRoute = competitorGetEndPoint.zServerLogic((chronosId, competitorId) => competitorGet(chronosId, competitorId)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def competitorCreate(chronosId: UUID, competitorCreate: CompetitorCreate): ZIO[ChronosEnv, ChronosFailure, Competitor] = | |
for { | |
chronos <- chronosGet(chronosId) | |
ref <- ZIO.service[Ref[AppState]] | |
uuid <- Random.nextUUID | |
competitor = Competitor( | |
uuid = uuid.toString, | |
firstName = competitorCreate.firstName, | |
lastName = competitorCreate.lastName, | |
birthYear = competitorCreate.birthYear | |
) | |
state <- ref.getAndUpdate(_.addCompetitor(chronos, competitor)) | |
} yield competitor | |
val competitorCreateEndPoint = | |
endpoint | |
.name("competitor create") | |
.description("create a new competitor for the given chronos") | |
.post | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("competitor") | |
.in(jsonBody[CompetitorCreate]) | |
.out(jsonBody[Competitor]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val competitorCreateRoute = competitorCreateEndPoint.zServerLogic((chronosId, payload) => competitorCreate(chronosId, payload)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def timerList(timerId: UUID): ZIO[ChronosEnv, ChronosFailure, List[Timer]] = | |
for { | |
chronos <- chronosGet(timerId) | |
timers = chronos.timers.values.toList | |
} yield timers | |
val timerListEndPoint = | |
endpoint | |
.name("timer list") | |
.description("List defined timers for given chronos") | |
.get | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("timer") | |
.out(jsonBody[List[Timer]]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val timerListRoute = timerListEndPoint.zServerLogic(timerId => timerList(timerId)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def timerGet(chronosId: UUID, timerId: UUID): ZIO[ChronosEnv, ChronosFailure, Timer] = | |
for { | |
chronos <- chronosGet(chronosId) | |
timer <- ZIO | |
.from(chronos.timers.get(timerId)) | |
.mapError(err => ChronosNotFoundFailure(s"Timer not found for chronos $chronosId", timerId)) | |
} yield timer | |
val timerGetEndPoint = | |
endpoint | |
.name("timer get") | |
.description("get timer info for given chronos") | |
.get | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("timer") | |
.in(path[UUID]("timerId").example("b1a0c019-8883-451b-b5d6-9a4589e425f8")) | |
.out(jsonBody[Timer]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val timerGetRoute = timerGetEndPoint.zServerLogic((chronosId, timerId) => timerGet(chronosId, timerId)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def timerCreate(chronosId: UUID, competitorId: UUID, timerCreate: TimerCreate): ZIO[ChronosEnv, ChronosFailure, Timer] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
chronos <- chronosGet(chronosId) | |
competitor <- competitorGet(chronosId, competitorId) | |
uuid <- Random.nextUUID | |
timestamp <- Clock.currentDateTime | |
timer = Timer( | |
uuid = uuid.toString, | |
competitorUUID = competitorId, | |
name = timerCreate.name, | |
created = timestamp.toInstant | |
) | |
state <- ref.getAndUpdate(_.addTimer(chronos, timer)) | |
} yield timer | |
val timerCreateEndPoint = | |
endpoint | |
.name("timer create") | |
.description("create a new timer for the given chronos") | |
.post | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("competitor") | |
.in(path[UUID]("competitorId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("timer") | |
.in(jsonBody[TimerCreate]) | |
.out(jsonBody[Timer]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val timerCreateRoute = timerCreateEndPoint.zServerLogic((chronosId, competitorId, payload) => timerCreate(chronosId, competitorId, payload)) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def timingPut(chronosId: UUID, competitorId: UUID, timerId: UUID, name: String): ZIO[ChronosEnv, ChronosFailure, Timing] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
chronos <- chronosGet(chronosId) | |
competitor <- competitorGet(chronosId, competitorId) | |
timer <- timerGet(chronosId, timerId) | |
_ <- ZIO.cond(competitor.uuid == timer.uuid, (), ChronosBadRequestFailure(s"$timerId doesn't belong to $competitor")) | |
timestamp <- Clock.currentDateTime | |
timing = Timing(name = name, timestamp = timestamp.toInstant) | |
state <- ref.getAndUpdate(_.addTiming(chronos, timer, timing)) | |
} yield timing | |
val timingPutEndPoint = | |
endpoint | |
.name("timing put") | |
.description("add a new timing for the given timer, competitor and chronos") | |
.put | |
.in("chronos") | |
.in(path[UUID]("chronosId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("competitor") | |
.in(path[UUID]("competitorId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("timer") | |
.in(path[UUID]("timerId").example("c2968f8e-bcbb-46f1-9dba-cb1969b8d391")) | |
.in("name") | |
.in(path[String]("name").example("start|step0|end|...")) | |
.out(jsonBody[Timing]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val timingPutRoute = timingPutEndPoint.zServerLogic(timingPut) | |
// ------------------------------------------------------------------------------------------------------------------- | |
val chronosRoutes = List( | |
chronosCreateRoute, | |
chronosGetRoute, | |
chronosListRoute, | |
competitorCreateRoute, | |
competitorGetRoute, | |
competitorListRoute, | |
timerCreateRoute, | |
timerGetRoute, | |
timerListRoute, | |
timingPutRoute | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
val apiDocRoutes = | |
// RedocInterpreter() | |
SwaggerInterpreter() | |
.fromServerEndpoints( | |
chronosRoutes, | |
Info(title = "Chronos API", version = "1.0", description = Some("Timekeeping backend")) | |
) | |
// =================================================================================================================== | |
val routes = ZioHttpInterpreter().toHttp(chronosRoutes ++ apiDocRoutes) | |
val appStateLayer = ZLayer.fromZIO(Ref.make(AppState())) | |
val server = for { | |
config <- ZIO.config(Server.Config.config) | |
port = config.address.getPort | |
_ <- Console.printLine(s"Server listening on http://127.0.0.1:$port/") | |
_ <- Console.printLine(s"API documentation ON http://127.0.0.1:$port/docs") | |
_ <- Server.serve(routes.withDefaultErrorResponse) | |
} yield () | |
override def run = | |
server | |
.provide( | |
appStateLayer, | |
Server.default | |
) | |
} | |
WebApp.main(Array.empty) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment