diff --git a/src/main/scala/io/github/tassiLuca/boundaries/either.scala b/src/main/scala/io/github/tassiLuca/boundaries/either.scala index 4fdf7941..bf1f8a40 100644 --- a/src/main/scala/io/github/tassiLuca/boundaries/either.scala +++ b/src/main/scala/io/github/tassiLuca/boundaries/either.scala @@ -1,6 +1,6 @@ package io.github.tassiLuca.boundaries -import scala.util.boundary +import scala.util.{Failure, Success, Try, boundary} import scala.util.boundary.{Label, break} object either: @@ -12,3 +12,10 @@ object either: inline def ?(using Label[Left[L, Nothing]]): R = e match case Right(value) => value case Left(value) => break(Left(value)) + + type ThrowableConverter[L] = Throwable => L + + extension [R](t: Try[R]) + inline def ?[L](using Label[Left[L, Nothing]])(using converter: ThrowableConverter[L]): R = t match + case Success(value) => value + case Failure(exception) => break(Left(converter(exception))) diff --git a/src/main/scala/io/github/tassiLuca/posts/Utils.scala b/src/main/scala/io/github/tassiLuca/posts/Utils.scala index eaf49be6..52d70724 100644 --- a/src/main/scala/io/github/tassiLuca/posts/Utils.scala +++ b/src/main/scala/io/github/tassiLuca/posts/Utils.scala @@ -4,7 +4,7 @@ import gears.async.default.given import gears.async.Async import gears.async.AsyncOperations -import scala.util.Random +import scala.util.{Failure, Random, Success} extension (component: String) def simulates(action: String)(using Async): Unit = @@ -16,3 +16,11 @@ extension (component: String) println(s"[$component - ${Thread.currentThread()}] $action") Thread.sleep(Random.nextInt(10_000)) println(s"[$component - ${Thread.currentThread()}] ended $action") + +extension [T](p: (T, T)) + def both(f: T => Boolean): Boolean = f(p._1) && f(p._2) + +@main def testBoth(): Unit = + println( + (Success(true), Failure(IllegalStateException())).both(r => r.isSuccess && r.get) + ) \ No newline at end of file diff --git a/src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala b/src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala index d63da679..f479284f 100644 --- a/src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala +++ b/src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala @@ -15,7 +15,7 @@ trait PostsRepositoryComponent: /** The repository in charge of storing and retrieving blog posts. */ trait PostsRepository: /** Save the given [[post]]. */ - def save(post: Post)(using Async): Try[Unit] + def save(post: Post)(using Async): Try[Post] /** Load the post with the given [[postTitle]]. */ def load(postTitle: Title)(using Async): Try[Post] @@ -30,10 +30,11 @@ trait PostsRepositoryComponent: private class PostsLocalRepository extends PostsRepository: private var posts: Set[Post] = Set() - override def save(post: Post)(using Async): Try[Unit] = Try: + override def save(post: Post)(using Async): Try[Post] = Try: require(posts.count(_.title == post.title) == 0, "A post with same title has already been saved") "PostsRepository" simulates s"saving post ${post.title}" synchronized { posts = posts + post } + post override def load(postTitle: Title)(using Async): Try[Post] = Try: "PostsRepository" simulates s"loading post $postTitle" diff --git a/src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala b/src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala index f15f33d2..de1e1dd3 100644 --- a/src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala +++ b/src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala @@ -1,12 +1,14 @@ package io.github.tassiLuca.posts.direct +import gears.async.AsyncOperations.sleep import gears.async.default.given import gears.async.{Async, Future, Task} import io.github.tassiLuca.boundaries.either -import io.github.tassiLuca.boundaries.either.? -import io.github.tassiLuca.posts.{PostsModel, simulates} +import io.github.tassiLuca.boundaries.either.{?, ThrowableConverter} +import io.github.tassiLuca.posts.{PostsModel, simulates, both} import java.util.Date +import scala.util.{Failure, Success, Try} /** The component blog posts service. */ trait PostsServiceComponent: @@ -33,29 +35,29 @@ trait PostsServiceComponent: private class PostsServiceImpl extends PostsService: + given ThrowableConverter[String] = (t: Throwable) => t.getMessage + override def create(authorId: AuthorId, title: Title, body: Body): Either[String, Post] = - either: - Async.blocking: - val post: Future[Post] = Future: - val p = Post(authorId, title, body, Date()) - val f1 = p.verifyAuthor.run - val f2 = p.verifyContent.run - f1.await.? - f2.await.? - context.repository.save(post.await) - post.await + Async.blocking: + val post = Post(authorId, title, body, Date()) + if post.verifyAuthor.run.zip(post.verifyContent.run).await.both(r => r.isSuccess && r.get) then + either { context.repository.save(post).? } + else Left("Error") extension (p: Post) - private def verifyAuthor(using Async): Task[Either[String, Post]] = Task: - "PostsService" simulates s"verifying author ${p.author}" - if Math.random() > 0.3 then Right(p) else Left(s"${p.author} is not authorized to post content!") - - private def verifyContent(using Async): Task[Either[String, Post]] = Task: - "PostsService" simulates s"verifying author ${p.author}" - if Math.random() > 0.3 then Right(p) else Left("The post contains non-appropriate sections.") - - override def get(title: Title): Either[String, Post] = Async.blocking: - context.repository.load(title).toEither.left.map(_.getMessage) - - override def all(): Either[String, LazyList[Post]] = Async.blocking: - context.repository.loadAll().toEither.left.map(_.getMessage) + private def verifyAuthor(using Async): Task[Try[Boolean]] = Task: + Try: + sleep(10_000) + "PostsService" simulates s"verifying author '${p.author}'" + true + + private def verifyContent(using Async): Task[Try[Boolean]] = Task: + Try: + "PostsService" simulates s"verifying post '${p.title}' content" + false + + override def get(title: Title): Either[String, Post] = either: + Async.blocking { context.repository.load(title).? } + + override def all(): Either[String, LazyList[Post]] = either: + Async.blocking(context.repository.loadAll().?) diff --git a/src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala b/src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala index 98f3fcf3..7ea243bb 100644 --- a/src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala +++ b/src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala @@ -22,4 +22,5 @@ object BlogPostsApp$ extends PostsServiceComponent with PostsModel with PostsRep p <- app.service.get("A hello world post") yield p Await.ready(post, Duration.Inf) - println(post) \ No newline at end of file + println(post.value) + \ No newline at end of file diff --git a/src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala b/src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala index 3dcd7eba..fdd73b31 100644 --- a/src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala +++ b/src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala @@ -46,11 +46,11 @@ trait PostsServiceComponent: extension (p: Post) private def verifyAuthor: Boolean = - "PostsService" simulatesBlocking s"verifying author ${p.author}" + "PostsService" simulatesBlocking s"verifying author '${p.author}''" if Math.random() > 0.3 then true else false private def verifyContent: Boolean = - "PostsService" simulatesBlocking s"verifying author ${p.author}" + "PostsService" simulatesBlocking s"verifying post '${p.title}' content" if Math.random() > 0.3 then true else false override def get(title: Title): Future[Post] = context.repository.load(title) diff --git a/src/test/scala/io/github/tassiLuca/posts/direct/BlogPostsServiceTest.scala b/src/test/scala/io/github/tassiLuca/posts/direct/BlogPostsServiceTest.scala new file mode 100644 index 00000000..0ef6f813 --- /dev/null +++ b/src/test/scala/io/github/tassiLuca/posts/direct/BlogPostsServiceTest.scala @@ -0,0 +1,5 @@ +package io.github.tassiLuca.posts.direct + +class BlogPostsServiceTest { + +}