Skip to content

Commit

Permalink
refactor(posts): make explicit the cancelling is only handled with th…
Browse files Browse the repository at this point in the history
…rowing an exception
  • Loading branch information
tassiluca committed Mar 20, 2024
1 parent 14d54a4 commit f296de5
Show file tree
Hide file tree
Showing 6 changed files with 31 additions and 26 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.github.tassiLuca.dse.blog.core

import java.util.Date
import scala.util.Try

/** The model of a simple blog posts service. */
trait PostsModel:
Expand All @@ -23,12 +24,12 @@ trait PostsModel:
/** A blog post, comprising an author, title, body and the last modification. */
case class Post(author: Author, title: Title, body: Body, lastModification: Date)

/** A function that verifies the content of the post, returning [[Right]] with the content of
* the post if the verification succeeds or [[Left]] with the reason why failed.
/** A function that verifies the content of the post, returning a [[scala.util.Success]] with
* the content of the post if the verification succeeds or a [[scala.util.Failure]] otherwise.
*/
type ContentVerifier = (Title, Body) => Either[String, PostContent]
type ContentVerifier = (Title, Body) => Try[PostContent]

/** A function that verifies the author has appropriate permissions, returning [[Right]]
* with their information or [[Left]] with the reason why failed.
/** A function that verifies the author has appropriate permissions, returning a
* [[scala.util.Success]] with their information or a [[scala.util.Failure]] otherwise.
*/
type AuthorsVerifier = AuthorId => Either[String, Author]
type AuthorsVerifier = AuthorId => Try[Author]
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ trait PostsServiceComponent:
context.repository.save(Post(author, post._1, post._2, Date()))

/* Pretending to make a call to the Authorship Service that keeps track of authorized authors. */
private def authorBy(id: AuthorId)(using Async, CanFail): Author =
private def authorBy(id: AuthorId)(using Async): Author =
"PostsService".simulates(s"getting author $id info...", maxDuration = 1_000)
authorsVerifier(id).?
authorsVerifier(id).get

/* Some local computation that verifies the content of the post is appropriate. */
private def verifyContent(title: Title, body: Body)(using Async, CanFail): PostContent =
private def verifyContent(title: Title, body: Body)(using Async): PostContent =
"PostsService".simulates(s"verifying content of post '$title'", minDuration = 1_000)
contentVerifier(title, body).?
contentVerifier(title, body).get

override def get(title: Title)(using Async, CanFail): Option[Post] =
context.repository.load(title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import org.scalatest.BeforeAndAfterEach
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers.shouldBe

import scala.util.Success

class BlogPostsServiceTest extends AnyFlatSpec with BeforeAndAfterEach:

val authorId = "mrossi"
Expand Down Expand Up @@ -63,9 +65,9 @@ class BlogPostsServiceTest extends AnyFlatSpec with BeforeAndAfterEach:
override def completedChecks: Set[Check] = _completedChecks
override val contentVerifier: ContentVerifier = (t, b) =>
_completedChecks += Check.ContentVerified
Right((t, b))
Success((t, b))
override val authorsVerifier: AuthorsVerifier = a =>
require(a == authorId, "No author with the given id matches")
_completedChecks += Check.AuthorVerified
Right(Author(a, "Mario", "Rossi"))
Success(Author(a, "Mario", "Rossi"))
override val service: PostsService = PostsService(contentVerifier, authorsVerifier)
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,12 @@ trait PostsServiceComponent:
/* Pretending to make a call to the Authorship Service that keeps track of authorized authors. */
private def authorBy(id: AuthorId)(using ExecutionContext): Future[Author] = Future:
"PostsService".simulatesBlocking(s"getting author $id info...", maxDuration = 1_000)
authorsVerifier(id) match { case Left(e) => throw RuntimeException(e); case Right(v) => v }
authorsVerifier(id).get

/* Some local computation that verifies the content of the post is appropriate. */
private def verifyContent(title: Title, body: Body)(using ExecutionContext): Future[PostContent] = Future:
"PostsService".simulatesBlocking(s"verifying content of the post '$title'", minDuration = 1_000)
contentVerifier(title, body) match { case Left(e) => throw RuntimeException(e); case Right(v) => v }
contentVerifier(title, body).get

override def get(title: Title)(using ExecutionContext): Future[Option[Post]] =
context.repository.load(title)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import org.scalatest.matchers.should.Matchers.shouldBe

import scala.concurrent.duration.{DurationInt, FiniteDuration}
import scala.concurrent.{Await, ExecutionContext}
import scala.util.Success

class BlogPostsServiceTest extends AnyFlatSpec with BeforeAndAfterEach:

Expand All @@ -15,18 +16,6 @@ class BlogPostsServiceTest extends AnyFlatSpec with BeforeAndAfterEach:
val postTitle = "A hello world post"
val postBody = "Hello World Scala Gears!"

def newBlogPostsAppInstance(): BlogPostsApp & CheckFlag = new BlogPostsApp with CheckFlag:
private var _completedChecks: Set[Check] = Set.empty
override def completedChecks: Set[Check] = _completedChecks
override val contentVerifier: ContentVerifier = (t, b) =>
_completedChecks += Check.ContentVerified
Right((t, b))
override val authorsVerifier: AuthorsVerifier = a =>
require(a == authorId, "No author with the given id matches")
_completedChecks += Check.AuthorVerified
Right(Author(a, "Mario", "Rossi"))
override val service: PostsService = PostsService(contentVerifier, authorsVerifier)

given ExecutionContext = ExecutionContext.global

"BlogPostsService" should "create posts correctly if author and content is legit" in {
Expand Down Expand Up @@ -66,3 +55,15 @@ class BlogPostsServiceTest extends AnyFlatSpec with BeforeAndAfterEach:
Thread.sleep(3_000) // waiting for the max duration of the content verification check
app.completedChecks shouldBe Set(Check.ContentVerified)
}

def newBlogPostsAppInstance(): BlogPostsApp & CheckFlag = new BlogPostsApp with CheckFlag:
private var _completedChecks: Set[Check] = Set.empty
override def completedChecks: Set[Check] = _completedChecks
override val contentVerifier: ContentVerifier = (t, b) =>
_completedChecks += Check.ContentVerified
Success((t, b))
override val authorsVerifier: AuthorsVerifier = a =>
require(a == authorId, "No author with the given id matches")
_completedChecks += Check.AuthorVerified
Success(Author(a, "Mario", "Rossi"))
override val service: PostsService = PostsService(contentVerifier, authorsVerifier)
1 change: 1 addition & 0 deletions docs/presentation/direct-style-presentation.tex
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ \section{Scala \texttt{gears}}
\small
\lstinputlisting[language=scala]{listings/example-1/PostsServiceImpl.scala}
\texttt{zip} operator allows combining the results of two \texttt{Future}s in a pair if both succeed, or fail with the first error encountered. Combined with \texttt{Async.group} the failure of one determines the cancellation of the other.
Note: the only way \texttt{authorBy} and \texttt{verifyContent} can fail, making the \texttt{zip} return immediately the failure (thus cancelling the other check) is by throwing an exception!
\end{frame}
%
\begin{frame}{Sources operators}
Expand Down

0 comments on commit f296de5

Please sign in to comment.