Last active
May 10, 2024 06:59
-
-
Save erdeszt/cccac947ef6141a7444a00c56b14ce87 to your computer and use it in GitHub Desktop.
Direct style scala effect tracking
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
package direct | |
import java.util.UUID | |
import scala.language.experimental.saferExceptions | |
import Contains.given | |
import izumi.reflect.macrortti.LightTypeTag | |
import izumi.reflect.{*, given} | |
import sttp.tapir.* | |
import sttp.tapir.server.netty.sync.{Id, NettySyncServer} | |
case class InvalidAge(age: Int) extends Exception(s"Invalid age: ${age}") | |
case class InvalidName(name: String) extends Exception(s"Invalid name: ${name}") | |
case class RequestId(id: UUID) | |
case class SessionId(id: UUID) | |
case class UserId(id: UUID) | |
def validateAge(age: Int): Unit throws InvalidAge = | |
if age < 20 || age > 80 then throw InvalidAge(age) | |
def validateName(name: String): Unit throws InvalidName = | |
if name.contains("hacker") then throw InvalidName(name) | |
def getUserId(using env: Env[SessionId]): UserId = | |
val sessionId = env.get[SessionId] | |
// or: val sessionId = Env.get[SessionId].! | |
UserId(UUID.fromString(sessionId.id.toString.replaceAll("4", "9"))) | |
def logRequest(userId: UserId)(using env: Env[RequestId]): Unit = | |
val requestId = env.get[RequestId] | |
println(s"[${requestId.id}] User: `${userId}` made a request") | |
def businessLogic(name: String, age: Int)(using | |
env: Env[RequestId & SessionId] | |
): String throws InvalidAge | InvalidName = | |
validateAge(age) | |
validateName(name) | |
val user = getUserId | |
logRequest(user) | |
val reqId = env.get[RequestId] | |
s"Hello ${name}(${age}) with id: ${reqId}" | |
val helloWorld: PublicEndpoint[(String, Int), String, String, Any] = | |
endpoint.get | |
.in("hello" / path[String]("name") / path[Int]("age")) | |
.out(stringBody) | |
.errorOut(stringBody) | |
val hwe = helloWorld.serverLogic[Id] { (name, age) => | |
try | |
given env: Env[RequestId & SessionId] = EnvBuilder.empty | |
.add(RequestId(UUID.randomUUID())) | |
.add(SessionId(UUID.randomUUID())) | |
.build | |
val result = businessLogic(name, age) | |
Right(result) | |
catch | |
case error: InvalidName => | |
println(s"Error: ${error}") | |
Left(s"Invalid name: ${error.name}") | |
case error: InvalidAge => | |
println(s"Error: ${error}") | |
Left(s"Invalid age: ${error.age}") | |
} | |
@main | |
def main(): Unit = | |
NettySyncServer().addEndpoint(hwe).port(8080).startAndWait() | |
trait Env[+R]: | |
def get[A: Tag](using Contains[R, A]): A | |
trait Contains[-R, A] | |
trait LowPrioContains: | |
given containsBase[A: Tag]: Contains[A, A] = | |
new Contains[A, A] {} | |
object Contains extends LowPrioContains: | |
given containsRec[A, R, R0](using | |
contains: Contains[R, A] | |
): Contains[R0 & R, A] = | |
new Contains[R0 & R, A] {} | |
case class EnvBuilder[+R](map: Map[LightTypeTag, Any]): | |
def add[A](instance: A)(using tag: Tag[A]): EnvBuilder[A & R] = | |
EnvBuilder( | |
map + (tag.tag -> instance) | |
) | |
def build: Env[R] = | |
new Env[R] { | |
override def get[A](using tag: Tag[A], _ev: Contains[R, A]): A = | |
map(tag.tag).asInstanceOf[A] | |
} | |
object EnvBuilder: | |
def empty: EnvBuilder[Any] = EnvBuilder(Map.empty) | |
object Env: | |
def get[A: Tag]: EnvGetter[A] = | |
new EnvGetter[A] {} | |
class EnvGetter[A: Tag]: | |
def ![R](using contains: Contains[R, A], env: Env[R]): A = | |
env.get[A] |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment