Last active
April 5, 2025 07:36
-
-
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/acf308276fe17d1dfdac65c222fe8c4f9d66ff7b
This file contains hidden or 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
{ | |
"openapi" : "3.1.0", | |
"info" : { | |
"title" : "Chronos API", | |
"version" : "1.0", | |
"description" : "Timekeeping backend" | |
}, | |
"paths" : { | |
"/chronos" : { | |
"get" : { | |
"tags" : [ | |
"chronos" | |
], | |
"summary" : "List all defined chronos", | |
"operationId" : "chronos list", | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"type" : "array", | |
"items" : { | |
"$ref" : "#/components/schemas/Chronos" | |
} | |
} | |
} | |
} | |
} | |
} | |
}, | |
"post" : { | |
"tags" : [ | |
"chronos" | |
], | |
"summary" : "Create a new chronos", | |
"operationId" : "chronos create", | |
"requestBody" : { | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosSpec" | |
} | |
} | |
}, | |
"required" : true | |
}, | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/Chronos" | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "Invalid value for: body", | |
"content" : { | |
"text/plain" : { | |
"schema" : { | |
"type" : "string" | |
} | |
} | |
} | |
} | |
} | |
} | |
}, | |
"/chronos/{chronosId}" : { | |
"get" : { | |
"tags" : [ | |
"chronos" | |
], | |
"summary" : "get detailed information for given chronos", | |
"operationId" : "chronos get", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
} | |
], | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/Chronos" | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
}, | |
"put" : { | |
"tags" : [ | |
"chronos" | |
], | |
"summary" : "update the given chronos", | |
"operationId" : "chronos update", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
} | |
], | |
"requestBody" : { | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosSpec" | |
} | |
} | |
}, | |
"required" : true | |
}, | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/Chronos" | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
}, | |
"delete" : { | |
"tags" : [ | |
"chronos" | |
], | |
"summary" : "delete the given chronos", | |
"operationId" : "chronos delete", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
} | |
], | |
"responses" : { | |
"200" : { | |
"description" : "" | |
}, | |
"400" : { | |
"description" : "Invalid value for: path parameter chronosId", | |
"content" : { | |
"text/plain" : { | |
"schema" : { | |
"type" : "string" | |
} | |
} | |
} | |
} | |
} | |
} | |
}, | |
"/chronos/{chronosId}/competitor" : { | |
"get" : { | |
"tags" : [ | |
"competitors" | |
], | |
"summary" : "List defined competitors for given chronos", | |
"operationId" : "competitors list", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
} | |
], | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"type" : "array", | |
"items" : { | |
"$ref" : "#/components/schemas/Competitor" | |
} | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
}, | |
"post" : { | |
"tags" : [ | |
"competitors" | |
], | |
"summary" : "create a new competitor for the given chronos", | |
"operationId" : "competitor create", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
} | |
], | |
"requestBody" : { | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/CompetitorSpec" | |
} | |
} | |
}, | |
"required" : true | |
}, | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/Competitor" | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
} | |
}, | |
"/chronos/{chronosId}/competitor/{competitorId}" : { | |
"get" : { | |
"tags" : [ | |
"competitors" | |
], | |
"summary" : "List defined competitors for given chronos", | |
"operationId" : "competitor get", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c40dfc42-3b2d-4294-b059-8dcd48d8f380" | |
}, | |
{ | |
"name" : "competitorId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "ad016ce3-e245-442b-b970-f3fb9b87d2f4" | |
} | |
], | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/Competitor" | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
}, | |
"put" : { | |
"tags" : [ | |
"competitors" | |
], | |
"summary" : "update the given competitor for the given chronos", | |
"operationId" : "competitor update", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
}, | |
{ | |
"name" : "competitorId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "ad016ce3-e245-442b-b970-f3fb9b87d2f4" | |
} | |
], | |
"requestBody" : { | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/CompetitorSpec" | |
} | |
} | |
}, | |
"required" : true | |
}, | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/Competitor" | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
}, | |
"delete" : { | |
"tags" : [ | |
"competitors" | |
], | |
"summary" : "delete the given competitor for the given chronos", | |
"operationId" : "competitor delete", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
}, | |
{ | |
"name" : "competitorId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "ad016ce3-e245-442b-b970-f3fb9b87d2f4" | |
} | |
], | |
"responses" : { | |
"200" : { | |
"description" : "" | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
} | |
}, | |
"/chronos/{chronosId}/competitor/{competitorId}/timings" : { | |
"get" : { | |
"tags" : [ | |
"measure" | |
], | |
"summary" : "get timings for the given chronos and competitor", | |
"operationId" : "timings get", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
}, | |
{ | |
"name" : "competitorId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
} | |
], | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"type" : "array", | |
"items" : { | |
"$ref" : "#/components/schemas/Timing" | |
} | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
}, | |
"put" : { | |
"tags" : [ | |
"measure" | |
], | |
"summary" : "add a new timing for the given competitor and chronos, the internal current time is used as timestamp for this timing", | |
"operationId" : "timing put", | |
"parameters" : [ | |
{ | |
"name" : "chronosId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
}, | |
{ | |
"name" : "competitorId", | |
"in" : "path", | |
"required" : true, | |
"schema" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"example" : "c2968f8e-bcbb-46f1-9dba-cb1969b8d391" | |
}, | |
{ | |
"name" : "Category", | |
"in" : "query", | |
"required" : true, | |
"schema" : { | |
"$ref" : "#/components/schemas/TimingCategory" | |
} | |
} | |
], | |
"responses" : { | |
"200" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/Timing" | |
} | |
} | |
} | |
}, | |
"400" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosBadRequestFailure" | |
} | |
} | |
} | |
}, | |
"404" : { | |
"description" : "", | |
"content" : { | |
"application/json" : { | |
"schema" : { | |
"$ref" : "#/components/schemas/ChronosNotFoundFailure" | |
} | |
} | |
} | |
} | |
} | |
} | |
} | |
}, | |
"components" : { | |
"schemas" : { | |
"Chronos" : { | |
"title" : "Chronos", | |
"type" : "object", | |
"required" : [ | |
"id", | |
"name", | |
"description", | |
"created" | |
], | |
"properties" : { | |
"id" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"name" : { | |
"type" : "string" | |
}, | |
"description" : { | |
"type" : "string" | |
}, | |
"created" : { | |
"type" : "string", | |
"format" : "date-time" | |
} | |
} | |
}, | |
"ChronosBadRequestFailure" : { | |
"title" : "ChronosBadRequestFailure", | |
"type" : "object", | |
"required" : [ | |
"message" | |
], | |
"properties" : { | |
"message" : { | |
"type" : "string" | |
} | |
} | |
}, | |
"ChronosNotFoundFailure" : { | |
"title" : "ChronosNotFoundFailure", | |
"type" : "object", | |
"required" : [ | |
"message", | |
"entityId" | |
], | |
"properties" : { | |
"message" : { | |
"type" : "string" | |
}, | |
"entityId" : { | |
"type" : "string", | |
"format" : "uuid" | |
} | |
} | |
}, | |
"ChronosSpec" : { | |
"title" : "ChronosSpec", | |
"type" : "object", | |
"required" : [ | |
"name", | |
"description" | |
], | |
"properties" : { | |
"name" : { | |
"type" : "string" | |
}, | |
"description" : { | |
"type" : "string" | |
} | |
} | |
}, | |
"Competitor" : { | |
"title" : "Competitor", | |
"type" : "object", | |
"required" : [ | |
"id", | |
"firstName", | |
"lastName", | |
"birthDate" | |
], | |
"properties" : { | |
"id" : { | |
"type" : "string", | |
"format" : "uuid" | |
}, | |
"firstName" : { | |
"type" : "string" | |
}, | |
"lastName" : { | |
"type" : "string" | |
}, | |
"birthDate" : { | |
"type" : "string", | |
"format" : "date-time" | |
} | |
} | |
}, | |
"CompetitorSpec" : { | |
"title" : "CompetitorSpec", | |
"type" : "object", | |
"required" : [ | |
"firstName", | |
"lastName", | |
"birthDate" | |
], | |
"properties" : { | |
"firstName" : { | |
"type" : "string" | |
}, | |
"lastName" : { | |
"type" : "string" | |
}, | |
"birthDate" : { | |
"type" : "string", | |
"format" : "date-time" | |
} | |
} | |
}, | |
"Timing" : { | |
"title" : "Timing", | |
"type" : "object", | |
"required" : [ | |
"category", | |
"timestamp" | |
], | |
"properties" : { | |
"category" : { | |
"$ref" : "#/components/schemas/TimingCategory" | |
}, | |
"timestamp" : { | |
"type" : "string", | |
"format" : "date-time" | |
} | |
} | |
}, | |
"TimingCategory" : { | |
"title" : "TimingCategory", | |
"type" : "string", | |
"enum" : [ | |
"End", | |
"Start", | |
"Step" | |
] | |
} | |
} | |
} | |
} |
This file contains hidden or 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
OPENAPI_DEP="org.openapitools:openapi-generator-cli:7.11.0" | |
OPENAPI_GEN="scala-cli --dep $OPENAPI_DEP -e org.openapitools.codegen.OpenAPIGenerator.main(args) --" | |
API_SPECS_FILE=chronos-api.json | |
GENERATED_CODE_DESTINATION=chronos-target/ | |
gen_python_flask() { | |
${OPENAPI_GEN} generate \ | |
-g python-flask \ | |
-i ${API_SPECS_FILE} \ | |
--additional-properties=generateSourceCodeOnly=true \ | |
-o ${GENERATED_CODE_DESTINATION}/python-flask | |
} | |
gen_rust_server() { | |
${OPENAPI_GEN} generate \ | |
-g rust-server \ | |
-i ${API_SPECS_FILE} \ | |
-o ${GENERATED_CODE_DESTINATION}/rust-server | |
} | |
gen_scala_http4s() { | |
${OPENAPI_GEN} generate \ | |
-g scala-http4s-server \ | |
-i ${API_SPECS_FILE} \ | |
-o ${GENERATED_CODE_DESTINATION}/scala-http4s-server | |
} | |
gen_java_undertow() { | |
${OPENAPI_GEN} generate \ | |
-g java-undertow-server \ | |
-i ${API_SPECS_FILE} \ | |
-o ${GENERATED_CODE_DESTINATION}/java-undertow-server | |
} | |
gen_python_flask | |
gen_rust_server | |
gen_scala_http4s | |
gen_java_undertow |
This file contains hidden or 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
#!/usr/bin/env -S scala-cli test -S 3 | |
// summary : Chronos API tests | |
// keywords : scala, cats, sttp, chronos, test | |
// 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 : 91025af9-38bd-4b7a-8b6c-7997162400fc | |
// created-on : 2025-01-11T00:22:40+01:00 | |
// managed-by : https://github.com/dacr/code-examples-manager | |
// run-with : scala-cli test $file | |
// test-with : curl http://127.0.0.1:8080/chronos | |
// --------------------- | |
//> using scala 3.6.4 | |
//> using objectWrapper | |
////> using platform native | |
////> using nativeVersion 0.5.7 | |
//> using dep com.softwaremill.sttp.client4::circe:4.0.0-RC3 | |
//> using dep com.softwaremill.sttp.client4::cats:4.0.0-RC3 | |
//> using dep org.typelevel::cats-effect:3.6.0 | |
//> using dep io.circe::circe-generic:0.14.12 | |
//> using dep org.typelevel::munit-cats-effect-3:1.0.7 | |
// --------------------- | |
import io.circe.generic.auto.* | |
import io.circe.* | |
import io.circe.parser.* | |
import io.circe.syntax.* | |
import sttp.client4.* | |
import sttp.client4.circe.* | |
import sttp.client4.httpclient.cats.HttpClientCatsBackend | |
import HttpClientCatsBackend.resource | |
import cats.effect.{IO, SyncIO} | |
import munit.CatsEffectSuite | |
import java.time.{Instant, OffsetDateTime} | |
import java.util.UUID | |
object Model { | |
type CompetitorId = UUID | |
type ChronosId = UUID | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class ChronosSpec( | |
name: String, | |
description: String | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class CompetitorSpec( | |
firstName: String, | |
lastName: String, | |
birthDate: OffsetDateTime | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class TimerSpec( | |
name: String | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Chronos( | |
id: ChronosId, | |
name: String, | |
description: String, | |
created: Instant | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Competitor( | |
id: CompetitorId, | |
firstName: String, | |
lastName: String, | |
birthDate: OffsetDateTime | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
enum TimingCategory { | |
case Start | |
case Step | |
case End | |
} | |
case class Timing( | |
category: TimingCategory, | |
timestamp: Instant | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
} | |
class ClassicTests extends CatsEffectSuite { | |
import Model.* | |
given Encoder[TimingCategory] = Encoder[String].contramap(_.toString) | |
given Decoder[TimingCategory] = Decoder[String].emap(s => Right(TimingCategory.valueOf(s))) // TODO manage left case | |
val baseAPI = "http://127.0.0.1:8080" | |
// ----------------------------------------------------------------------------------------------- | |
test("List chronos") { | |
resource[IO]().use { backend => | |
for { | |
chronos <- | |
quickRequest | |
.get(uri"$baseAPI/chronos") | |
.response(asJson[List[Chronos]]) | |
.send(backend) | |
} yield assertEquals(chronos.code.code, 200) | |
} | |
} | |
// ----------------------------------------------------------------------------------------------- | |
test("Create, read, update, delete a chronos") { | |
resource[IO]().use { backend => | |
for { | |
randName <- IO.randomUUID.map(uuid => s"Chronos $uuid") | |
chronos <- | |
quickRequest | |
.post(uri"$baseAPI/chronos") | |
.body(ChronosSpec(randName, s"$randName description").asJson.noSpaces) | |
.response(asJson[Chronos]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
gottenChronos <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}") | |
.response(asJson[Chronos]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
_ <- | |
quickRequest | |
.put(uri"$baseAPI/chronos/${chronos.id}") | |
.body(ChronosSpec(randName, s"$randName updated description").asJson.noSpaces) | |
.send(backend) | |
updatedChronos <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}") | |
.response(asJson[Chronos]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
responseDelete <- | |
quickRequest | |
.delete(uri"$baseAPI/chronos/${chronos.id}") | |
.send(backend) | |
resultAfterDelete <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}") | |
.response(asJson[Chronos]) | |
.send(backend) | |
} yield { | |
assertEquals(chronos, gottenChronos) | |
assertEquals(updatedChronos.description, s"$randName updated description") | |
assert(resultAfterDelete.code.code == 404) | |
} | |
} | |
} | |
// ----------------------------------------------------------------------------------------------- | |
test("List Competitors") { | |
resource[IO]().use { backend => | |
for { | |
randName <- IO.randomUUID.map(uuid => s"Chronos $uuid") | |
chronos <- | |
quickRequest | |
.post(uri"$baseAPI/chronos") | |
.body(ChronosSpec(randName, s"$randName description").asJson.noSpaces) | |
.response(asJson[Chronos]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
competitors <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}/competitor") | |
.response(asJson[List[Competitor]]) | |
.send(backend) | |
} yield assertEquals(competitors.code.code, 200) | |
} | |
} | |
// ----------------------------------------------------------------------------------------------- | |
test("Create, read, update, delete a competitor") { | |
resource[IO]().use { backend => | |
for { | |
randName <- IO.randomUUID.map(uuid => s"Chronos $uuid") | |
randFirstName <- IO.randomUUID.map(uuid => s"Joe-$uuid") | |
chronos <- | |
quickRequest | |
.post(uri"$baseAPI/chronos") | |
.body(ChronosSpec(randName, s"$randName description").asJson.noSpaces) | |
.response(asJson[Chronos]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
competitor <- | |
quickRequest | |
.post(uri"$baseAPI/chronos/${chronos.id}/competitor") | |
.body(CompetitorSpec(randFirstName, "doe", OffsetDateTime.parse("1942-01-01T00:00:00Z")).asJson.noSpaces) | |
.response(asJson[Competitor]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
gottenCompetitor <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}") | |
.response(asJson[Competitor]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
_ <- | |
quickRequest | |
.put(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}") | |
.body(CompetitorSpec(s"$randFirstName-updated", competitor.lastName, competitor.birthDate).asJson.noSpaces) | |
.send(backend) | |
updatedCompetitor <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}") | |
.response(asJson[Competitor]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
responseDelete <- | |
quickRequest | |
.delete(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}") | |
.send(backend) | |
resultAfterDelete <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}") | |
.response(asJson[Competitor]) | |
.send(backend) | |
} yield { | |
assertEquals(competitor, gottenCompetitor) | |
assert(updatedCompetitor.firstName.contains("updated")) | |
assert(resultAfterDelete.code.code == 404) | |
} | |
} | |
} | |
// ----------------------------------------------------------------------------------------------- | |
test("Take timings of a competitor ") { | |
resource[IO]().use { backend => | |
for { | |
randName <- IO.randomUUID.map(uuid => s"Chronos $uuid") | |
chronos <- | |
quickRequest | |
.post(uri"$baseAPI/chronos") | |
.body(ChronosSpec(randName, s"$randName description").asJson.noSpaces) | |
.response(asJson[Chronos]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
competitor <- | |
quickRequest | |
.post(uri"$baseAPI/chronos/${chronos.id}/competitor") | |
.body(CompetitorSpec("john", "doe", OffsetDateTime.parse("1942-01-01T00:00:00Z")).asJson.noSpaces) | |
.response(asJson[Competitor]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
timingStart <- | |
quickRequest | |
.put(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}/timings?Category=${TimingCategory.Start}") | |
.response(asJson[Timing]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
timingEnd <- | |
quickRequest | |
.put(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}/timings?Category=${TimingCategory.End}") | |
.response(asJson[Timing]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
timings <- | |
quickRequest | |
.get(uri"$baseAPI/chronos/${chronos.id}/competitor/${competitor.id}/timings") | |
.response(asJson[List[Timing]]) | |
.send(backend) | |
.flatMap(response => IO.fromEither(response.body)) | |
} yield { | |
assert(timings.size == 2) | |
} | |
} | |
} | |
// ----------------------------------------------------------------------------------------------- | |
} |
This file contains hidden or 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
#!/usr/bin/env -S scala-cli shebang -S 3 | |
// summary : Chronos API using zio, tapir and zhttp | |
// keywords : scala, chronos, 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 | |
// attachments : chronos-api.json, chronos-gen.sh, chronos-tests.sc | |
// --------------------- | |
//> using scala 3.6.4 | |
////> using platform native | |
////> using nativeVersion 0.5.7 | |
//> using dep com.softwaremill.sttp.tapir::tapir-zio:1.11.20 | |
//> using dep com.softwaremill.sttp.tapir::tapir-zio-http-server:1.11.20 | |
//> using dep com.softwaremill.sttp.tapir::tapir-json-zio:1.11.20 | |
//> using dep com.softwaremill.sttp.tapir::tapir-swagger-ui-bundle:1.11.20 | |
//> using dep dev.zio::zio-nio:2.0.2 | |
//> using dep dev.zio::zio-json:0.7.39 | |
// --------------------- | |
import java.time.{Instant, OffsetDateTime} | |
import java.util.UUID | |
import zio.* | |
import zio.json.* | |
import sttp.tapir.ztapir.* | |
import sttp.tapir.server.ziohttp.ZioHttpInterpreter | |
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.{BadRequest, NotFound} | |
import sttp.tapir.Codec.PlainCodec | |
import sttp.tapir.docs.openapi.OpenAPIDocsInterpreter | |
import sttp.tapir.{Codec, Schema} | |
import zio.http.Server | |
import zio.nio.file.* | |
object Model { | |
type CompetitorId = UUID | |
type ChronosId = UUID | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class ChronosSpec( | |
name: String, | |
description: String | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class CompetitorSpec( | |
firstName: String, | |
lastName: String, | |
birthDate: OffsetDateTime | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Chronos( | |
id: ChronosId, | |
name: String, | |
description: String, | |
created: Instant | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Competitor( | |
id: CompetitorId, | |
firstName: String, | |
lastName: String, | |
birthDate: OffsetDateTime | |
) derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
enum TimingCategory { | |
case Start | |
case Step | |
case End | |
} | |
object TimingCategory { | |
given JsonEncoder[TimingCategory] = JsonEncoder[String].contramap(p => p.toString) | |
given JsonDecoder[TimingCategory] = JsonDecoder[String].map(p => TimingCategory.valueOf(p)) | |
given Schema[TimingCategory] = Schema.derivedEnumeration.defaultStringBased | |
given PlainCodec[TimingCategory] = Codec.derivedEnumeration[String, TimingCategory].defaultStringBased | |
} | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class Timing( | |
category: TimingCategory, | |
timestamp: Instant | |
) derives JsonCodec | |
} | |
// =================================================================================================== | |
object WebApp extends ZIOAppDefault { | |
import Model.* | |
sealed trait ChronosFailure derives JsonCodec { | |
val message: String | |
} | |
case class ChronosInvalidOperationFailure(message: String, entityId: UUID) extends ChronosFailure derives JsonCodec | |
case class ChronosNotFoundFailure(message: String, entityId: UUID) extends ChronosFailure derives JsonCodec | |
case class ChronosBadRequestFailure(message: String) extends ChronosFailure derives JsonCodec | |
// ------------------------------------------------------------------------------------------------------------------- | |
case class AppState( | |
activeChronos: Map[ChronosId, Chronos] = Map.empty, | |
activeCompetitors: Map[ChronosId, List[Competitor]] = Map.empty, | |
activeTimings: Map[CompetitorId, List[Timing]] = Map.empty | |
) { | |
def upsertChronos(chronos: Chronos): AppState = { | |
copy(activeChronos = activeChronos + (chronos.id -> chronos)) | |
} | |
def deleteChronos(chronosId: ChronosId): AppState = { | |
copy( | |
activeChronos = activeChronos - chronosId, | |
activeCompetitors = activeCompetitors - chronosId, | |
activeTimings = activeTimings -- activeCompetitors.getOrElse(chronosId, Nil).map(_.id) | |
) | |
} | |
def upsertCompetitor(chronosId: ChronosId, competitor: Competitor): AppState = { | |
copy(activeCompetitors = activeCompetitors + (chronosId -> (competitor :: activeCompetitors.getOrElse(chronosId, Nil)))) | |
} | |
def deleteCompetitor(chronosId: ChronosId, competitorId: CompetitorId): AppState = { | |
copy( | |
activeCompetitors = activeCompetitors + (chronosId -> activeCompetitors.getOrElse(chronosId, Nil).filterNot(_.id == competitorId)), | |
activeTimings = activeTimings -- activeCompetitors.getOrElse(chronosId, Nil).map(_.id) | |
) | |
} | |
def addTiming(competitorId: CompetitorId, timing: Timing): AppState = { | |
copy(activeTimings = activeTimings + (competitorId -> (timing :: activeTimings.getOrElse(competitorId, Nil)))) | |
} | |
def deleteTiming(competitorId: CompetitorId, timingId: UUID): AppState = { | |
//TODO généré par Gemini, à tester tout de même | |
copy( | |
activeTimings = activeTimings.get(competitorId) match { | |
case Some(timings) => | |
val updatedTimings = timings.filterNot(_.timestamp == timingId) // Filter out the matching timing | |
if (updatedTimings.isEmpty) activeTimings - competitorId // Remove competitor if no timings remain | |
else activeTimings + (competitorId -> updatedTimings) // Update with new timing list | |
case None => activeTimings // No changes if competitor not found | |
} | |
) | |
} | |
} | |
// =================================================================================================================== | |
type ChronosEnv = Ref[AppState] | |
// ------------------------------------------------------------------------------------------------------------------- | |
val chronosList: ZIO[ChronosEnv, Nothing, List[Chronos]] = | |
for { | |
state <- ZIO.serviceWithZIO[Ref[AppState]](_.get) | |
} yield state.activeChronos.values.toList | |
val chronosListEndPoint = | |
endpoint | |
.tag("chronos") | |
.name("chronos list") | |
.summary("List all defined chronos") | |
.get | |
.in("chronos") | |
.out(jsonBody[List[Chronos]]) | |
val chronosListRoute = chronosListEndPoint.zServerLogic(_ => chronosList) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def chronosGet(id: ChronosId): ZIO[ChronosEnv, ChronosFailure, Chronos] = | |
for { | |
state <- ZIO.serviceWithZIO[Ref[AppState]](_.get) | |
chronos <- ZIO.from(state.activeChronos.get(id)).mapError(err => ChronosNotFoundFailure(s"Chronos not found", id)) | |
} yield chronos | |
val chronosGetEndPoint = | |
endpoint | |
.tag("chronos") | |
.name("chronos get") | |
.summary("get detailed information for given chronos") | |
.get | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.out(jsonBody[Chronos]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val chronosGetRoute = chronosGetEndPoint.zServerLogic(chronosGet) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def chronosCreate(spec: ChronosSpec): ZIO[ChronosEnv, Nothing, Chronos] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
uuid <- Random.nextUUID | |
now <- Clock.instant | |
chronos = Chronos(id = uuid, name = spec.name, description = spec.description, created = now) | |
_ <- ref.update(_.upsertChronos(chronos)) | |
} yield chronos | |
val chronosCreateEndPoint = | |
endpoint | |
.tag("chronos") | |
.name("chronos create") | |
.summary("Create a new chronos") | |
.post | |
.in("chronos") | |
.in(jsonBody[ChronosSpec]) | |
.out(jsonBody[Chronos]) | |
val chronosCreateRoute = chronosCreateEndPoint.zServerLogic(chronosCreate) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def chronosUpdate(chronosId: ChronosId, spec: ChronosSpec): ZIO[ChronosEnv, ChronosFailure, Chronos] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
previous <- chronosGet(chronosId) | |
chronos = previous.copy(name = spec.name, description = spec.description) | |
_ <- ref.update(_.upsertChronos(chronos)) | |
} yield chronos | |
val chronosUpdateEndPoint = | |
endpoint | |
.tag("chronos") | |
.name("chronos update") | |
.summary("update the given chronos") | |
.put | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in(jsonBody[ChronosSpec]) | |
.out(jsonBody[Chronos]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val chronosUpdateRoute = chronosUpdateEndPoint.zServerLogic(chronosUpdate) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def chronosDelete(id: ChronosId): ZIO[ChronosEnv, Nothing, Unit] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
_ <- ref.update(_.deleteChronos(id)) | |
} yield () | |
val chronosDeleteEndPoint = | |
endpoint | |
.tag("chronos") | |
.name("chronos delete") | |
.summary("delete the given chronos") | |
.delete | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
val chronosDeleteRoute = chronosDeleteEndPoint.zServerLogic(chronosDelete) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def competitorList(chronosId: ChronosId): ZIO[ChronosEnv, ChronosFailure, List[Competitor]] = | |
for { | |
state <- ZIO.serviceWithZIO[Ref[AppState]](_.get) | |
competitors = state.activeCompetitors.getOrElse(chronosId, Nil) | |
} yield competitors | |
val competitorListEndPoint = | |
endpoint | |
.tag("competitors") | |
.name("competitors list") | |
.summary("List defined competitors for given chronos") | |
.get | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("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: ChronosId, competitorId: CompetitorId): ZIO[ChronosEnv, ChronosFailure, Competitor] = | |
for { | |
state <- ZIO.serviceWithZIO[Ref[AppState]](_.get) | |
competitors <- ZIO | |
.from(state.activeCompetitors.get(chronosId)) | |
.mapError(err => ChronosNotFoundFailure(s"Chronos not found", chronosId)) | |
competitor <- ZIO | |
.from(competitors.find(_.id == competitorId)) | |
.mapError(err => ChronosNotFoundFailure(s"Competitor not found", competitorId)) | |
} yield competitor | |
val competitorGetEndPoint = | |
endpoint | |
.tag("competitors") | |
.name("competitor get") | |
.summary("List defined competitors for given chronos") | |
.get | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c40dfc42-3b2d-4294-b059-8dcd48d8f380"))) | |
.in("competitor") | |
.in(path[CompetitorId]("competitorId").example(UUID.fromString("ad016ce3-e245-442b-b970-f3fb9b87d2f4"))) | |
.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: ChronosId, spec: CompetitorSpec): ZIO[ChronosEnv, ChronosFailure, Competitor] = | |
for { | |
chronos <- chronosGet(chronosId) | |
ref <- ZIO.service[Ref[AppState]] | |
uuid <- Random.nextUUID | |
competitor = Competitor( | |
id = uuid, | |
firstName = spec.firstName, | |
lastName = spec.lastName, | |
birthDate = spec.birthDate | |
) | |
_ <- ref.update(_.upsertCompetitor(chronos.id, competitor)) | |
} yield competitor | |
val competitorCreateEndPoint = | |
endpoint | |
.tag("competitors") | |
.name("competitor create") | |
.summary("create a new competitor for the given chronos") | |
.post | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in("competitor") | |
.in(jsonBody[CompetitorSpec]) | |
.out(jsonBody[Competitor]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val competitorCreateRoute = competitorCreateEndPoint.zServerLogic(competitorCreate) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def competitorUpdate(chronosId: ChronosId, competitorId: CompetitorId, spec: CompetitorSpec): ZIO[ChronosEnv, ChronosFailure, Competitor] = | |
for { | |
chronos <- chronosGet(chronosId) | |
ref <- ZIO.service[Ref[AppState]] | |
previous <- competitorGet(chronosId, competitorId) | |
competitor = previous.copy( | |
firstName = spec.firstName, | |
lastName = spec.lastName, | |
birthDate = spec.birthDate | |
) | |
_ <- ref.update(_.upsertCompetitor(chronos.id, competitor)) | |
} yield competitor | |
val competitorUpdateEndPoint = | |
endpoint | |
.tag("competitors") | |
.name("competitor update") | |
.summary("update the given competitor for the given chronos") | |
.put | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in("competitor") | |
.in(path[CompetitorId]("competitorId").example(UUID.fromString("ad016ce3-e245-442b-b970-f3fb9b87d2f4"))) | |
.in(jsonBody[CompetitorSpec]) | |
.out(jsonBody[Competitor]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val competitorUpdateRoute = competitorUpdateEndPoint.zServerLogic(competitorUpdate) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def competitorDelete(chronosId: ChronosId, competitorId: CompetitorId): ZIO[ChronosEnv, ChronosFailure, Unit] = | |
for { | |
chronos <- chronosGet(chronosId) | |
ref <- ZIO.service[Ref[AppState]] | |
competitor <- competitorGet(chronosId, competitorId) | |
_ <- ref.update(_.deleteCompetitor(chronos.id, competitor.id)) | |
} yield () | |
val competitorDeleteEndPoint = | |
endpoint | |
.tag("competitors") | |
.name("competitor delete") | |
.summary("delete the given competitor for the given chronos") | |
.delete | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in("competitor") | |
.in(path[CompetitorId]("competitorId").example(UUID.fromString("ad016ce3-e245-442b-b970-f3fb9b87d2f4"))) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val competitorDeleteRoute = competitorDeleteEndPoint.zServerLogic(competitorDelete) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def timingList(chronosId: UUID, competitorId: UUID): ZIO[ChronosEnv, ChronosFailure, List[Timing]] = | |
for { | |
state <- ZIO.serviceWithZIO[Ref[AppState]](_.get) | |
chronos <- chronosGet(chronosId) | |
competitor <- competitorGet(chronosId, competitorId) | |
timings = state.activeTimings.getOrElse(competitorId, Nil) | |
} yield timings | |
val timingListEndPoint = | |
endpoint | |
.tag("measure") | |
.name("timings get") | |
.summary("get timings for the given chronos and competitor") | |
.get | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in("competitor") | |
.in(path[CompetitorId]("competitorId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in("timings") | |
.out(jsonBody[List[Timing]]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val timingListRoute = timingListEndPoint.zServerLogic(timingList) | |
// ------------------------------------------------------------------------------------------------------------------- | |
def timingPut(chronosId: UUID, competitorId: UUID, category: TimingCategory): ZIO[ChronosEnv, ChronosFailure, Timing] = | |
for { | |
ref <- ZIO.service[Ref[AppState]] | |
chronos <- chronosGet(chronosId) | |
competitor <- competitorGet(chronosId, competitorId) | |
timestamp <- Clock.currentDateTime | |
timings <- timingList(chronosId, competitorId) | |
_ <- ZIO.cond(category != TimingCategory.Start || timings.isEmpty, (), ChronosInvalidOperationFailure(s"Timings already started", competitor.id)) | |
_ <- ZIO.cond(category != TimingCategory.End || timings.nonEmpty, (), ChronosInvalidOperationFailure(s"Can not end a not started timings", competitor.id)) | |
_ <- ZIO.cond( | |
category != TimingCategory.End || !timings.lastOption.contains(TimingCategory.End), | |
(), | |
ChronosInvalidOperationFailure(s"Already ended timings", competitor.id) | |
) | |
_ <- ZIO.cond(category != TimingCategory.Step || timings.nonEmpty, (), ChronosInvalidOperationFailure(s"Not started timings", competitor.id)) | |
timing = Timing(category = category, timestamp = timestamp.toInstant) | |
_ <- ref.update(_.addTiming(competitor.id, timing)) | |
} yield timing | |
val timingPutEndPoint = | |
endpoint | |
.tag("measure") | |
.name("timing put") | |
.summary("add a new timing for the given competitor and chronos, the internal current time is used as timestamp for this timing") | |
.put | |
.in("chronos") | |
.in(path[ChronosId]("chronosId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in("competitor") | |
.in(path[CompetitorId]("competitorId").example(UUID.fromString("c2968f8e-bcbb-46f1-9dba-cb1969b8d391"))) | |
.in("timings") | |
.in(query[TimingCategory]("Category")) | |
.out(jsonBody[Timing]) | |
.errorOut( | |
oneOf( | |
oneOfVariant(BadRequest, jsonBody[ChronosBadRequestFailure]), | |
oneOfVariant(NotFound, jsonBody[ChronosNotFoundFailure]) | |
) | |
) | |
val timingPutRoute = timingPutEndPoint.zServerLogic(timingPut) | |
// ------------------------------------------------------------------------------------------------------------------- | |
val chronosRoutes = List( | |
chronosCreateRoute, | |
chronosGetRoute, | |
chronosUpdateRoute, | |
chronosDeleteRoute, | |
chronosListRoute, | |
competitorCreateRoute, | |
competitorGetRoute, | |
competitorUpdateRoute, | |
competitorDeleteRoute, | |
competitorListRoute, | |
timingListRoute, | |
timingPutRoute | |
) | |
// ------------------------------------------------------------------------------------------------------------------- | |
val apiInfo = Info(title = "Chronos API", version = "1.0", description = Some("Timekeeping backend")) | |
val apiDocRoutes = | |
SwaggerInterpreter() | |
.fromServerEndpoints( | |
chronosRoutes, | |
apiInfo | |
) | |
// =================================================================================================================== | |
val generateAPIDocFile = { | |
import io.circe.syntax.* | |
import sttp.apispec.openapi.circe.* | |
for { | |
apiDocs <- ZIO.attempt(OpenAPIDocsInterpreter().toOpenAPI(chronosRoutes.map(_.endpoint), apiInfo)) | |
apiDocsJson <- ZIO.attempt(apiDocs.asJson) | |
apiDocsJsonFilepath = Path(".") / "chronos-api.json" | |
_ <- Files.writeBytes(apiDocsJsonFilepath, Chunk.fromArray(apiDocsJson.toString.getBytes("UTF-8"))) | |
infoMessage = s"OPENAPI specification file written to $apiDocsJsonFilepath" | |
_ <- ZIO.logInfo(infoMessage) | |
_ <- Console.printLine(infoMessage) | |
} yield () | |
} | |
// =================================================================================================================== | |
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 | |
_ <- generateAPIDocFile | |
_ <- 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) | |
} 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