From deb31fd2ea05eacf6f500f1f81cbb965d1b51cf2 Mon Sep 17 00:00:00 2001 From: Luca Tassinari Date: Fri, 12 Jan 2024 13:28:50 +0100 Subject: [PATCH] refactor: move into package posts the examples --- .../tassiLuca/gears/posts/PostsService.scala | 89 --------------- .../tassiLuca/gears/posts/UsePostsApp.scala | 22 ---- .../gears/posts/quo/PostsService.scala | 102 ------------------ .../gears/posts/quo/UsePostsApp.scala | 37 ------- .../{gears => }/posts/PostsModel.scala | 2 +- .../io/github/tassiLuca/posts/Utils.scala | 18 ++++ .../tassiLuca/posts/direct/BlogPostsApp.scala | 35 ++++++ .../direct/PostsRepositoryComponent.scala | 44 ++++++++ .../posts/direct/PostsServiceComponent.scala | 61 +++++++++++ .../tassiLuca/posts/quo/BlogPostsApp.scala | 25 +++++ .../posts/quo/PostsRepositoryComponent.scala | 45 ++++++++ .../posts/quo/PostsServiceComponent.scala | 58 ++++++++++ 12 files changed, 287 insertions(+), 251 deletions(-) delete mode 100644 src/main/scala/io/github/tassiLuca/gears/posts/PostsService.scala delete mode 100644 src/main/scala/io/github/tassiLuca/gears/posts/UsePostsApp.scala delete mode 100644 src/main/scala/io/github/tassiLuca/gears/posts/quo/PostsService.scala delete mode 100644 src/main/scala/io/github/tassiLuca/gears/posts/quo/UsePostsApp.scala rename src/main/scala/io/github/tassiLuca/{gears => }/posts/PostsModel.scala (90%) create mode 100644 src/main/scala/io/github/tassiLuca/posts/Utils.scala create mode 100644 src/main/scala/io/github/tassiLuca/posts/direct/BlogPostsApp.scala create mode 100644 src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala create mode 100644 src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala create mode 100644 src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala create mode 100644 src/main/scala/io/github/tassiLuca/posts/quo/PostsRepositoryComponent.scala create mode 100644 src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala diff --git a/src/main/scala/io/github/tassiLuca/gears/posts/PostsService.scala b/src/main/scala/io/github/tassiLuca/gears/posts/PostsService.scala deleted file mode 100644 index 18751fcc..00000000 --- a/src/main/scala/io/github/tassiLuca/gears/posts/PostsService.scala +++ /dev/null @@ -1,89 +0,0 @@ -package io.github.tassiLuca.gears.posts - -import gears.async.{Async, Future} -import gears.async.AsyncOperations.sleep -import gears.async.default.given - -import java.util.Date - -trait PostsRepositoryComponent: - context: PostsModel => - - val postsRepository: PostsRepository - - trait PostsRepository: - def save(post: Post)(using Async): Boolean - def loadAll()(using Async): LazyList[Post] - def load(postTitle: Title)(using Async): Option[Post] - - object PostsRepository: - def apply(): PostsRepository = PostsLocalRepository() - - private class PostsLocalRepository extends PostsRepository: - private var posts: Set[Post] = Set() - - override def save(post: Post)(using Async): Boolean = - sleep(1_000) - require(posts.count(_.title == post.title) == 0, "A post with same title has already been saved") - println(s"[PostsRepository - ${Thread.currentThread()}] Saving post $post...") - sleep(3_000) - synchronized { posts = posts + post } - true - - override def load(postTitle: Title)(using Async): Option[Post] = - println(s"[PostsRepository - ${Thread.currentThread()}] Loading post $postTitle...") - sleep(2_000) - posts.find(_.title == postTitle) - - override def loadAll()(using Async): LazyList[Post] = - println(s"[PostsRepository - ${Thread.currentThread()}] Loading all blog posts...") - sleep(7_000) - LazyList.from(posts) - -trait PostsServiceComponent: - context: PostsRepositoryComponent with PostsModel => - - val postsService: PostsService - - trait PostsService: - def create(authorId: AuthorId, title: Title, body: Body): Unit - def get(title: Title): Post - def all(): LazyList[Post] - - object PostsService: - def apply(): PostsService = PostsServiceImpl() - - private class PostsServiceImpl extends PostsService: - override def create(authorId: AuthorId, title: Title, body: Body): Unit = Async.blocking: - println(s"[PostsService - ${Thread.currentThread()}]") - val authorVerification = Future(verifyAuthor(authorId)) // new thread - val contentVerification = Future(contentFiltering(title, body)) // new thread - if (authorVerification.await && contentVerification.await) { // synchronization point - context.postsRepository.save(Post(authorId, title, body, Date())) - } - - private def verifyAuthor(author: AuthorId)(using Async): Boolean = - println(s"[PostsService - ${Thread.currentThread()}] verifying author $author...") - sleep(6_000) // simulating a call to another service... - println(s"[PostsService - ${Thread.currentThread()}] $author verification ended!") - true - - private def contentFiltering(title: Title, body: Body)(using Async): Boolean = - println(s"[PostsService - ${Thread.currentThread()}] verifying post entitled $title content...") - sleep(10_000) // simulating a content filtering algorithm... - println(s"[PostsService - ${Thread.currentThread()}] post entitled $title verification ended!") - true - - override def get(title: Title): Post = Async.blocking: - context.postsRepository.load(title).get - - override def all(): LazyList[Post] = Async.blocking: - context.postsRepository.loadAll() - -object BlogPostsApp extends PostsServiceComponent with PostsModel with PostsRepositoryComponent: - override type AuthorId = String - override type Body = String - override type Title = String - - override val postsRepository: BlogPostsApp.PostsRepository = PostsRepository() - override val postsService: BlogPostsApp.PostsService = PostsService() diff --git a/src/main/scala/io/github/tassiLuca/gears/posts/UsePostsApp.scala b/src/main/scala/io/github/tassiLuca/gears/posts/UsePostsApp.scala deleted file mode 100644 index e9ef6b04..00000000 --- a/src/main/scala/io/github/tassiLuca/gears/posts/UsePostsApp.scala +++ /dev/null @@ -1,22 +0,0 @@ -package io.github.tassiLuca.gears.posts - -import gears.async.{Async, Future} -import gears.async.default.given - -/* TODO WARNING: USING AN OBJECT WHICH EXTENDS `App` CAUSES STARVATION! - * THE WORKER THREAD GETS STUCK WAITING FOR THE RELEASE OF THE MONITOR LOCK - * waiting on the Class initialization monitor for io.github.tassiLuca.gears.posts.UsePostsApp$ - * TO BE INVESTIGATED! */ -@main def usePostsApp(): Unit = - val app = BlogPostsApp - Async.blocking: - val f1 = Future: - println(s"POST 1 carried by ${Thread.currentThread()}") - app.postsService.create("ltassi@gmail.com", "A hello world post", "Hello World!") - println(app.postsService.get("A hello world post")) - val f2 = Future: - println(s"POST 2 carried by ${Thread.currentThread()}") - app.postsService.create("ltassi@gmail.com", "An another post", "Hello World 2!") - println(app.postsService.get("An another post")) - f1.await - f2.await diff --git a/src/main/scala/io/github/tassiLuca/gears/posts/quo/PostsService.scala b/src/main/scala/io/github/tassiLuca/gears/posts/quo/PostsService.scala deleted file mode 100644 index c13a3109..00000000 --- a/src/main/scala/io/github/tassiLuca/gears/posts/quo/PostsService.scala +++ /dev/null @@ -1,102 +0,0 @@ -package io.github.tassiLuca.gears.posts.quo - -import io.github.tassiLuca.gears.posts.PostsModel - -import java.lang.Thread.sleep -import java.util.Date -import scala.concurrent -import scala.concurrent.{ExecutionContext, Future} - -trait PostsRepositoryComponent: - context: PostsModel => - - val postsRepository: PostsRepository - - trait PostsRepository: - def save(post: Post)(using ExecutionContext): Future[Unit] - def loadAll()(using ExecutionContext): Future[LazyList[Post]] - def load(postTitle: Title)(using ExecutionContext): Future[Post] - - object PostsRepository: - def apply(): PostsRepository = PostsLocalRepository() - - private class PostsLocalRepository extends PostsRepository: - private var posts: Set[Post] = Set() - - override def save(post: Post)(using ExecutionContext): Future[Unit] = Future { - sleep(1_000) - require(posts.count(_.title == post.title) == 0, "A post with same title has already been saved") - println(s"[PostsRepository - ${Thread.currentThread()}] Saving post $post...") - sleep(3_000) - synchronized { posts = posts + post } - } - - override def load(postTitle: Title)(using ExecutionContext): Future[Post] = Future { - println(s"[PostsRepository - ${Thread.currentThread()}] Loading post $postTitle...") - sleep(2_000) - posts.find(_.title == postTitle).get - } - - override def loadAll()(using ExecutionContext): Future[LazyList[Post]] = Future { - println(s"[PostsRepository - ${Thread.currentThread()}] Loading all blog posts...") - sleep(7_000) - LazyList.from(posts) - } - -trait PostsServiceComponent: - context: PostsRepositoryComponent with PostsModel => - - val postsService: PostsService - - trait PostsService: - def create(authorId: AuthorId, title: Title, body: Body): Future[Unit] - def get(title: Title): Future[Post] - def all(): Future[LazyList[Post]] - def test(): Future[Unit] - - object PostsService: - def apply(): PostsService = PostsServiceImpl() - - private class PostsServiceImpl extends PostsService: - given ExecutionContext = ExecutionContext.global - - override def test(): Future[Unit] = Future { - Thread.sleep(10_000) - println("ok") - } - - override def create(authorId: AuthorId, title: Title, body: Body): Future[Unit] = - println(s"[PostsService - ${Thread.currentThread()}]") - val authorVerification = Future { verifyAuthor(authorId) } // new thread - val contentVerification = Future { contentFiltering(title, body) } // new thread - for - resultAuthor <- authorVerification - if resultAuthor - resultVerification <- contentVerification - if resultVerification - _ <- context.postsRepository.save(Post(authorId, title, body, Date())) - yield () - - private def verifyAuthor(author: AuthorId): Boolean = - println(s"[PostsService - ${Thread.currentThread()}] verifying author $author...") - sleep(6_000) // simulating a call to another service... - println(s"[PostsService - ${Thread.currentThread()}] $author verification ended!") - true - - private def contentFiltering(title: Title, body: Body): Boolean = - println(s"[PostsService - ${Thread.currentThread()}] verifying post entitled $title content...") - sleep(10_000) // simulating a content filtering algorithm... - println(s"[PostsService - ${Thread.currentThread()}] post entitled $title verification ended!") - true - - override def get(title: Title): Future[Post] = context.postsRepository.load(title) - - override def all(): Future[LazyList[Post]] = context.postsRepository.loadAll() - -object BlogPostsAppQuo extends PostsServiceComponent with PostsModel with PostsRepositoryComponent: - override type AuthorId = String - override type Body = String - override type Title = String - - override val postsRepository: PostsRepository = PostsRepository() - override val postsService: PostsService = PostsService() diff --git a/src/main/scala/io/github/tassiLuca/gears/posts/quo/UsePostsApp.scala b/src/main/scala/io/github/tassiLuca/gears/posts/quo/UsePostsApp.scala deleted file mode 100644 index 30ce49a9..00000000 --- a/src/main/scala/io/github/tassiLuca/gears/posts/quo/UsePostsApp.scala +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.tassiLuca.gears.posts.quo - -import scala.concurrent.duration.Duration -import scala.concurrent.{Await, ExecutionContext, Future} -import scala.util.{Failure, Random, Success} - -def f1()(using ExecutionContext): Future[Boolean] = Future { - println("Started f1") - Thread.sleep(10_000) - true -} - -def f2()(using ExecutionContext): Future[Boolean] = Future { - println("Started f2") - Thread.sleep(5_000) - true -} - -@main def simple(): Unit = - given ExecutionContext = ExecutionContext.global - val results = - for - res1 <- f1() - res2 <- f2() - yield (res1, res2) - Await.ready(results, Duration.Inf) - -@main def usePostsApp(): Unit = - given ExecutionContext = ExecutionContext.global - val app = BlogPostsAppQuo - val post = - for - _ <- app.postsService.create("ltassi@gmail.com", "A hello world post", "Hello World!") - p <- app.postsService.get("A hello world post") - yield p - Await.ready(post, Duration.Inf) - println(post) \ No newline at end of file diff --git a/src/main/scala/io/github/tassiLuca/gears/posts/PostsModel.scala b/src/main/scala/io/github/tassiLuca/posts/PostsModel.scala similarity index 90% rename from src/main/scala/io/github/tassiLuca/gears/posts/PostsModel.scala rename to src/main/scala/io/github/tassiLuca/posts/PostsModel.scala index 4c9fc0c1..ce204fe0 100644 --- a/src/main/scala/io/github/tassiLuca/gears/posts/PostsModel.scala +++ b/src/main/scala/io/github/tassiLuca/posts/PostsModel.scala @@ -1,4 +1,4 @@ -package io.github.tassiLuca.gears.posts +package io.github.tassiLuca.posts import java.util.Date diff --git a/src/main/scala/io/github/tassiLuca/posts/Utils.scala b/src/main/scala/io/github/tassiLuca/posts/Utils.scala new file mode 100644 index 00000000..eaf49be6 --- /dev/null +++ b/src/main/scala/io/github/tassiLuca/posts/Utils.scala @@ -0,0 +1,18 @@ +package io.github.tassiLuca.posts + +import gears.async.default.given +import gears.async.Async +import gears.async.AsyncOperations + +import scala.util.Random + +extension (component: String) + def simulates(action: String)(using Async): Unit = + println(s"[$component - ${Thread.currentThread()}] $action") + AsyncOperations.sleep(Random.nextInt(10_000)) + println(s"[$component - ${Thread.currentThread()}] ended $action") + + def simulatesBlocking(action: String): Unit = + println(s"[$component - ${Thread.currentThread()}] $action") + Thread.sleep(Random.nextInt(10_000)) + println(s"[$component - ${Thread.currentThread()}] ended $action") diff --git a/src/main/scala/io/github/tassiLuca/posts/direct/BlogPostsApp.scala b/src/main/scala/io/github/tassiLuca/posts/direct/BlogPostsApp.scala new file mode 100644 index 00000000..9f94ab70 --- /dev/null +++ b/src/main/scala/io/github/tassiLuca/posts/direct/BlogPostsApp.scala @@ -0,0 +1,35 @@ +package io.github.tassiLuca.posts.direct + +import io.github.tassiLuca.posts.PostsModel + +object BlogPostsApp extends PostsServiceComponent with PostsModel with PostsRepositoryComponent: + override type AuthorId = String + override type Body = String + override type Title = String + + override val repository: PostsRepository = PostsRepository() + override val service: PostsService = PostsService() + +@main def useSimple(): Unit = + val app = BlogPostsApp + val result = app.service.create("ltassi@gmail.com", "A hello world post", "Hello World!") + println(s"Result: $result") + println(app.service.get("A hello world post")) + +///* TODO WARNING: USING AN OBJECT WHICH EXTENDS `App` CAUSES STARVATION! +// * THE WORKER THREAD GETS STUCK WAITING FOR THE RELEASE OF THE MONITOR LOCK +// * waiting on the Class initialization monitor for io.github.tassiLuca.gears.posts.UsePostsApp$ +// * TO BE INVESTIGATED! */ +//@main def usePostsApp(): Unit = +// val app = BlogPostsApp +// Async.blocking: +// val f1 = Future: +// println(s"POST 1 carried by ${Thread.currentThread()}") +// app.postsService.create("ltassi@gmail.com", "A hello world post", "Hello World!") +// println(app.postsService.get("A hello world post")) +// val f2 = Future: +// println(s"POST 2 carried by ${Thread.currentThread()}") +// app.postsService.create("ltassi@gmail.com", "An another post", "Hello World 2!") +// println(app.postsService.get("An another post")) +// f1.await +// f2.await diff --git a/src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala b/src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala new file mode 100644 index 00000000..d63da679 --- /dev/null +++ b/src/main/scala/io/github/tassiLuca/posts/direct/PostsRepositoryComponent.scala @@ -0,0 +1,44 @@ +package io.github.tassiLuca.posts.direct + +import gears.async.Async +import io.github.tassiLuca.posts.{PostsModel, simulates} + +import scala.util.Try + +/** The component exposing blog posts repositories. */ +trait PostsRepositoryComponent: + context: PostsModel => + + /** The repository instance. */ + val repository: PostsRepository + + /** The repository in charge of storing and retrieving blog posts. */ + trait PostsRepository: + /** Save the given [[post]]. */ + def save(post: Post)(using Async): Try[Unit] + + /** Load the post with the given [[postTitle]]. */ + def load(postTitle: Title)(using Async): Try[Post] + + /** Load all the saved post. */ + def loadAll()(using Async): Try[LazyList[Post]] + + object PostsRepository: + /** Constructs a new [[PostsRepository]]. */ + def apply(): PostsRepository = PostsLocalRepository() + + private class PostsLocalRepository extends PostsRepository: + private var posts: Set[Post] = Set() + + override def save(post: Post)(using Async): Try[Unit] = 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 } + + override def load(postTitle: Title)(using Async): Try[Post] = Try: + "PostsRepository" simulates s"loading post $postTitle" + posts.find(_.title == postTitle).get + + override def loadAll()(using Async): Try[LazyList[Post]] = Try: + "PostsRepository" simulates s"loading all blog posts" + LazyList.from(posts) diff --git a/src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala b/src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala new file mode 100644 index 00000000..f15f33d2 --- /dev/null +++ b/src/main/scala/io/github/tassiLuca/posts/direct/PostsServiceComponent.scala @@ -0,0 +1,61 @@ +package io.github.tassiLuca.posts.direct + +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 java.util.Date + +/** The component blog posts service. */ +trait PostsServiceComponent: + context: PostsRepositoryComponent with PostsModel => + + /** The blog post service instance. */ + val service: PostsService + + /** The service exposing a set of functionalities to interact with blog posts. */ + trait PostsService: + /** Creates a new blog post with the given [[title]] and [[body]], authored by [[authorId]], or a string explaining + * the reason of the failure. + */ + def create(authorId: AuthorId, title: Title, body: Body): Either[String, Post] + + /** Get a post from its [[title]] or a string explaining the reason of the failure. */ + def get(title: Title): Either[String, Post] + + /** Gets all the stored blog posts in a lazy manner or a string explaining the reason of the failure. */ + def all(): Either[String, LazyList[Post]] + + object PostsService: + def apply(): PostsService = PostsServiceImpl() + + private class PostsServiceImpl extends PostsService: + + 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 + + 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) diff --git a/src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala b/src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala new file mode 100644 index 00000000..98f3fcf3 --- /dev/null +++ b/src/main/scala/io/github/tassiLuca/posts/quo/BlogPostsApp.scala @@ -0,0 +1,25 @@ +package io.github.tassiLuca.posts.quo + +import io.github.tassiLuca.posts.PostsModel + +import scala.concurrent.duration.Duration +import scala.concurrent.{Await, ExecutionContext} + +object BlogPostsApp$ extends PostsServiceComponent with PostsModel with PostsRepositoryComponent: + override type AuthorId = String + override type Body = String + override type Title = String + + override val repository: PostsRepository = PostsRepository() + override val service: PostsService = PostsService() + +@main def usePostsApp(): Unit = + given ExecutionContext = ExecutionContext.global + val app = BlogPostsApp$ + val post = + for + _ <- app.service.create("ltassi@gmail.com", "A hello world post", "Hello World!") + p <- app.service.get("A hello world post") + yield p + Await.ready(post, Duration.Inf) + println(post) \ No newline at end of file diff --git a/src/main/scala/io/github/tassiLuca/posts/quo/PostsRepositoryComponent.scala b/src/main/scala/io/github/tassiLuca/posts/quo/PostsRepositoryComponent.scala new file mode 100644 index 00000000..784b37d9 --- /dev/null +++ b/src/main/scala/io/github/tassiLuca/posts/quo/PostsRepositoryComponent.scala @@ -0,0 +1,45 @@ +package io.github.tassiLuca.posts.quo + +import io.github.tassiLuca.posts.PostsModel +import io.github.tassiLuca.posts.simulatesBlocking + +import java.lang.Thread.sleep +import scala.concurrent.{ExecutionContext, Future} + +/** The component exposing blog posts repositories. */ +trait PostsRepositoryComponent: + context: PostsModel => + + /** The repository instance. */ + val repository: PostsRepository + + /** The repository in charge of storing and retrieving blog posts. */ + trait PostsRepository: + /** Save the given [[post]]. */ + def save(post: Post)(using ExecutionContext): Future[Unit] + + /** Load the post with the given [[postTitle]]. */ + def loadAll()(using ExecutionContext): Future[LazyList[Post]] + + /** Load all the saved post. */ + def load(postTitle: Title)(using ExecutionContext): Future[Post] + + object PostsRepository: + /** Constructs a new [[PostsRepository]]. */ + def apply(): PostsRepository = PostsLocalRepository() + + private class PostsLocalRepository extends PostsRepository: + private var posts: Set[Post] = Set() + + override def save(post: Post)(using ExecutionContext): Future[Unit] = Future: + require(posts.count(_.title == post.title) == 0, "A post with same title has already been saved") + "PostsRepository" simulatesBlocking s"saving post ${post.title}" + synchronized { posts = posts + post } + + override def load(postTitle: Title)(using ExecutionContext): Future[Post] = Future: + "PostsRepository" simulatesBlocking s"loading post $postTitle" + posts.find(_.title == postTitle).get + + override def loadAll()(using ExecutionContext): Future[LazyList[Post]] = Future: + "PostsRepository" simulatesBlocking s"loading all blog posts" + LazyList.from(posts) diff --git a/src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala b/src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala new file mode 100644 index 00000000..3dcd7eba --- /dev/null +++ b/src/main/scala/io/github/tassiLuca/posts/quo/PostsServiceComponent.scala @@ -0,0 +1,58 @@ +package io.github.tassiLuca.posts.quo + +import io.github.tassiLuca.posts.{PostsModel, simulatesBlocking} + +import java.util.Date +import scala.concurrent +import scala.concurrent.{ExecutionContext, Future} + +/** The component blog posts service. */ +trait PostsServiceComponent: + context: PostsRepositoryComponent with PostsModel => + + /** The blog post service instance. */ + val service: PostsService + + /** The service exposing a set of functionalities to interact with blog posts. */ + trait PostsService: + /** Creates a new blog post with the given [[title]] and [[body]], authored by [[authorId]], or a string explaining + * the reason of the failure. + */ + def create(authorId: AuthorId, title: Title, body: Body): Future[Unit] + + /** Get a post from its [[title]] or a string explaining the reason of the failure. */ + def get(title: Title): Future[Post] + + /** Gets all the stored blog posts in a lazy manner or a string explaining the reason of the failure. */ + def all(): Future[LazyList[Post]] + + object PostsService: + def apply(): PostsService = PostsServiceImpl() + + private class PostsServiceImpl extends PostsService: + given ExecutionContext = ExecutionContext.global + + override def create(authorId: AuthorId, title: Title, body: Body): Future[Unit] = + val post = Post(authorId, title, body, Date()) + val authorVerification = Future { post.verifyAuthor } + val contentVerification = Future { post.verifyContent } + for + resultAuthor <- authorVerification + if resultAuthor + resultVerification <- contentVerification + if resultVerification + _ <- context.repository.save(post) + yield () + + extension (p: Post) + private def verifyAuthor: Boolean = + "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}" + if Math.random() > 0.3 then true else false + + override def get(title: Title): Future[Post] = context.repository.load(title) + + override def all(): Future[LazyList[Post]] = context.repository.loadAll()