Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active August 18, 2023 07:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dacr/75e31e80fc6371c37ebb43ab9ece7855 to your computer and use it in GitHub Desktop.
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
// 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