From 1c12418f737c7d74762a8da0bbedf17a88364d42 Mon Sep 17 00:00:00 2001 From: IvanC Date: Thu, 11 Mar 2021 19:00:27 +0100 Subject: [PATCH 1/2] version running with mac m1 --- docker-compose.yml | 6 +++--- project/build.properties | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6f18f07..80b1d9f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,13 +3,13 @@ version: '3' services: mysql: container_name: codelytv-cqrs_ddd_scala_example-mysql - image: mysql:8.0 + image: mysql/mysql-server:8.0.23 restart: unless-stopped ports: - "3316:3306" env_file: - .env - command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci --lower_case_table_names=1 volumes: - ./etc/infrastructure/mysql/init:/docker-entrypoint-initdb.d @@ -21,4 +21,4 @@ services: - "5672:5672" - "8181:15672" env_file: - - .env + - .env \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index c0bab04..862afa5 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.2.8 +sbt.version=1.4.7 \ No newline at end of file From 114c8fbeabd2b8a0b7132f6a4de55058671478bc Mon Sep 17 00:00:00 2001 From: IvanC Date: Sun, 21 Mar 2021 00:01:18 +0100 Subject: [PATCH 2/2] added podcast service --- README.md | 3 +- .../api/EntryPointDependencyContainer.scala | 11 +++- app/main/tv/codely/mooc/api/MoocApiApp.scala | 7 ++- app/main/tv/codely/mooc/api/Routes.scala | 56 ++++++++++++++++- .../podcast/PodcastGetController.scala | 13 ++++ .../podcast/PodcastPostController.scala | 18 ++++++ .../podcast/PodcastPostRateController.scala | 18 ++++++ app/test/tv/codely/HttpSpec.scala | 10 ++- database/podcast.sql | 16 +++++ .../application/create/PodcastsCreator.scala | 20 ++++++ .../application/rating/PodcastRater.scala | 20 ++++++ .../application/search/PodcastsSearcher.scala | 9 +++ .../codely/mooc/podcast/domain/Podcast.scala | 21 +++++++ .../mooc/podcast/domain/PodcastCreated.scala | 28 +++++++++ .../podcast/domain/PodcastDescription.scala | 3 + .../mooc/podcast/domain/PodcastDuration.scala | 9 +++ .../mooc/podcast/domain/PodcastId.scala | 9 +++ .../mooc/podcast/domain/PodcastRating.scala | 3 + .../podcast/domain/PodcastRepository.scala | 10 +++ .../mooc/podcast/domain/PodcastTitle.scala | 3 + .../mooc/podcast/domain/PodcastVotes.scala | 3 + .../PodcastModuleDependencyContainer.scala | 22 +++++++ ...odcastAttributesJsonFormatMarshaller.scala | 62 +++++++++++++++++++ .../PodcastCreatedJsonFormatMarshaller.scala | 31 ++++++++++ .../PodcastJsonFormatMarshaller.scala | 18 ++++++ .../DoobieMySqlPodcastRepository.scala | 29 +++++++++ .../marshaller/DomainEventsMarshaller.scala | 6 +- .../DoobieMySqlVideoRepository.scala | 2 +- 28 files changed, 448 insertions(+), 12 deletions(-) create mode 100644 app/main/tv/codely/mooc/api/controller/podcast/PodcastGetController.scala create mode 100644 app/main/tv/codely/mooc/api/controller/podcast/PodcastPostController.scala create mode 100644 app/main/tv/codely/mooc/api/controller/podcast/PodcastPostRateController.scala create mode 100644 database/podcast.sql create mode 100644 src/mooc/main/tv/codely/mooc/podcast/application/create/PodcastsCreator.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/application/rating/PodcastRater.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/application/search/PodcastsSearcher.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/Podcast.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastCreated.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDescription.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDuration.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastId.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRating.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRepository.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastTitle.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/domain/PodcastVotes.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/infrastructure/dependency_injection/PodcastModuleDependencyContainer.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastAttributesJsonFormatMarshaller.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastCreatedJsonFormatMarshaller.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastJsonFormatMarshaller.scala create mode 100644 src/mooc/main/tv/codely/mooc/podcast/infrastructure/repository/DoobieMySqlPodcastRepository.scala diff --git a/README.md b/README.md index 4c66288..4e5889f 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ + [![License](https://img.shields.io/github/license/CodelyTV/cqrs-ddd-scala-example.svg?style=flat-square)](LICENSE) [![Build Status](https://img.shields.io/travis/CodelyTV/cqrs-ddd-scala-example/master.svg?style=flat-square)](https://travis-ci.org/CodelyTV/cqrs-ddd-scala-example) [![Coverage Status](https://img.shields.io/coveralls/github/CodelyTV/cqrs-ddd-scala-example/master.svg?style=flat-square)](https://coveralls.io/github/CodelyTV/cqrs-ddd-scala-example?branch=master) @@ -122,4 +123,4 @@ We'll try to maintain this project as simple as possible, but Pull Requests are ## License -The MIT License (MIT). Please see [License](LICENSE) for more information. +The MIT License (MIT). Please see [License](LICENSE) for more information. \ No newline at end of file diff --git a/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala b/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala index c05109f..6ee0ff9 100644 --- a/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala +++ b/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala @@ -1,14 +1,17 @@ package tv.codely.mooc.api +import tv.codely.mooc.api.controller.podcast.{PodcastGetController, PodcastPostController, PodcastPostRateController} import tv.codely.mooc.api.controller.status.StatusGetController import tv.codely.mooc.api.controller.user.{UserGetController, UserPostController} import tv.codely.mooc.api.controller.video.{VideoGetController, VideoPostController} +import tv.codely.mooc.podcast.infrastructure.dependency_injection.PodcastModuleDependencyContainer import tv.codely.mooc.user.infrastructure.dependency_injection.UserModuleDependencyContainer import tv.codely.mooc.video.infrastructure.dependency_injection.VideoModuleDependencyContainer final class EntryPointDependencyContainer( userDependencies: UserModuleDependencyContainer, - videoDependencies: VideoModuleDependencyContainer + videoDependencies: VideoModuleDependencyContainer, + podcastDependencies: PodcastModuleDependencyContainer ) { val statusGetController = new StatusGetController @@ -17,4 +20,8 @@ final class EntryPointDependencyContainer( val videoGetController = new VideoGetController(videoDependencies.videosSearcher) val videoPostController = new VideoPostController(videoDependencies.videoCreator) -} + + val podcastGetController = new PodcastGetController(podcastDependencies.podcastsSearcher) + val podcastPostController = new PodcastPostController(podcastDependencies.podcastCreator) + val podcastPostRateController = new PodcastPostRateController(podcastDependencies.podcastRater) +} \ No newline at end of file diff --git a/app/main/tv/codely/mooc/api/MoocApiApp.scala b/app/main/tv/codely/mooc/api/MoocApiApp.scala index d5fdb87..e5202b6 100644 --- a/app/main/tv/codely/mooc/api/MoocApiApp.scala +++ b/app/main/tv/codely/mooc/api/MoocApiApp.scala @@ -2,11 +2,11 @@ package tv.codely.mooc.api import scala.concurrent.ExecutionContext import scala.io.StdIn - import akka.actor.ActorSystem import akka.http.scaladsl.Http import akka.stream.ActorMaterializer import com.typesafe.config.ConfigFactory +import tv.codely.mooc.podcast.infrastructure.dependency_injection.PodcastModuleDependencyContainer import tv.codely.mooc.user.infrastructure.dependency_injection.UserModuleDependencyContainer import tv.codely.mooc.video.infrastructure.dependency_injection.VideoModuleDependencyContainer import tv.codely.shared.infrastructure.bus.rabbitmq.RabbitMqConfig @@ -33,7 +33,8 @@ object MoocApiApp { val container = new EntryPointDependencyContainer( new UserModuleDependencyContainer(sharedDependencies.doobieDbConnection, sharedDependencies.messagePublisher), - new VideoModuleDependencyContainer(sharedDependencies.doobieDbConnection, sharedDependencies.messagePublisher) + new VideoModuleDependencyContainer(sharedDependencies.doobieDbConnection, sharedDependencies.messagePublisher), + new PodcastModuleDependencyContainer(sharedDependencies.doobieDbConnection, sharedDependencies.messagePublisher) ) val routes = new Routes(container) @@ -60,4 +61,4 @@ object MoocApiApp { println("Server stopped!") }) } -} +} \ No newline at end of file diff --git a/app/main/tv/codely/mooc/api/Routes.scala b/app/main/tv/codely/mooc/api/Routes.scala index e6035e1..85de263 100644 --- a/app/main/tv/codely/mooc/api/Routes.scala +++ b/app/main/tv/codely/mooc/api/Routes.scala @@ -44,8 +44,60 @@ final class Routes(container: EntryPointDependencyContainer) { } } - val all: Route = status ~ user ~ video +// private val podcast = get { +// path("podcasts")(container.podcastGetController.get()) +// } + + private val podcast = get { + path("podcasts")(container.podcastGetController.get()) + } ~ + post { + concat( + path("podcasts") { + jsonBody { body => + container.podcastPostController.post( + body("id").convertTo[String], + body("title").convertTo[String], + body("duration_in_seconds").convertTo[Int].seconds, + body("description").convertTo[String] + ) + } + }, + path("podcasts" / "rates") { + jsonBody { body => + container.podcastPostRateController.post( + body("id").convertTo[String], + body("rate").convertTo[Int] + ) + } + } + ) + } +// post { +// path("podcasts") { +// jsonBody { body => +// container.podcastPostController.post( +// body("id").convertTo[String], +// body("title").convertTo[String], +// body("duration_in_seconds").convertTo[Int].seconds, +// body("description").convertTo[String] +// ) +// } +// } +// } ~ +// post { +// path("podcasts/rate") { +// jsonBody { body => +// container.podcastPostRateController.post( +// body("id").convertTo[String], +// body("rate").convertTo[Int] +// ) +// } +// } +// } + + val all: Route = status ~ user ~ video ~ podcast private def jsonBody(handler: Map[String, JsValue] => Route): Route = entity(as[JsValue])(json => handler(json.asJsObject.fields)) -} +} \ No newline at end of file diff --git a/app/main/tv/codely/mooc/api/controller/podcast/PodcastGetController.scala b/app/main/tv/codely/mooc/api/controller/podcast/PodcastGetController.scala new file mode 100644 index 0000000..16adaff --- /dev/null +++ b/app/main/tv/codely/mooc/api/controller/podcast/PodcastGetController.scala @@ -0,0 +1,13 @@ +package tv.codely.mooc.api.controller.podcast + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import akka.http.scaladsl.server.Directives.complete +import akka.http.scaladsl.server.StandardRoute +import spray.json.DefaultJsonProtocol +import tv.codely.mooc.podcast.application.search.PodcastsSearcher +import tv.codely.mooc.podcast.infrastructure.marshaller.PodcastJsonFormatMarshaller._ + + +final class PodcastGetController(searcher: PodcastsSearcher) extends SprayJsonSupport with DefaultJsonProtocol { + def get(): StandardRoute = complete(searcher.all()) +} \ No newline at end of file diff --git a/app/main/tv/codely/mooc/api/controller/podcast/PodcastPostController.scala b/app/main/tv/codely/mooc/api/controller/podcast/PodcastPostController.scala new file mode 100644 index 0000000..c314bb5 --- /dev/null +++ b/app/main/tv/codely/mooc/api/controller/podcast/PodcastPostController.scala @@ -0,0 +1,18 @@ +package tv.codely.mooc.api.controller.podcast + +import akka.http.scaladsl.model.HttpResponse +import akka.http.scaladsl.model.StatusCodes.NoContent +import akka.http.scaladsl.server.Directives.complete +import akka.http.scaladsl.server.StandardRoute +import tv.codely.mooc.podcast.application.create.PodcastsCreator +import tv.codely.mooc.podcast.domain.{PodcastDescription, PodcastDuration, PodcastId, PodcastTitle} + +import scala.concurrent.duration.Duration + +final class PodcastPostController(creator: PodcastsCreator) { + def post(id: String, title: String, duration: Duration, description: String): StandardRoute = { + creator.create(PodcastId(id), PodcastTitle(title), PodcastDuration(duration), PodcastDescription(description)) + + complete(HttpResponse(NoContent)) + } +} \ No newline at end of file diff --git a/app/main/tv/codely/mooc/api/controller/podcast/PodcastPostRateController.scala b/app/main/tv/codely/mooc/api/controller/podcast/PodcastPostRateController.scala new file mode 100644 index 0000000..405a4c9 --- /dev/null +++ b/app/main/tv/codely/mooc/api/controller/podcast/PodcastPostRateController.scala @@ -0,0 +1,18 @@ +package tv.codely.mooc.api.controller.podcast + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import akka.http.scaladsl.model.HttpResponse +import akka.http.scaladsl.model.StatusCodes.NoContent +import akka.http.scaladsl.server.Directives.complete +import akka.http.scaladsl.server.StandardRoute +import spray.json.DefaultJsonProtocol +import tv.codely.mooc.podcast.application.rating.PodcastRater +import tv.codely.mooc.podcast.domain.{PodcastId, PodcastRating} + +final class PodcastPostRateController(rater: PodcastRater) extends SprayJsonSupport with DefaultJsonProtocol { + def post(id: String, rate: Int): StandardRoute = { + rater.rate(PodcastId(id), PodcastRating(rate)) + + complete(HttpResponse(NoContent)) + } +} \ No newline at end of file diff --git a/app/test/tv/codely/HttpSpec.scala b/app/test/tv/codely/HttpSpec.scala index 19e2f99..6d8cebc 100644 --- a/app/test/tv/codely/HttpSpec.scala +++ b/app/test/tv/codely/HttpSpec.scala @@ -7,6 +7,7 @@ import com.typesafe.config.ConfigFactory import org.scalatest.{Matchers, WordSpec} import org.scalatest.concurrent.ScalaFutures import tv.codely.mooc.api.{EntryPointDependencyContainer, Routes} +import tv.codely.mooc.podcast.infrastructure.dependency_injection.PodcastModuleDependencyContainer import tv.codely.mooc.user.infrastructure.dependency_injection.UserModuleDependencyContainer import tv.codely.mooc.video.infrastructure.dependency_injection.VideoModuleDependencyContainer import tv.codely.shared.infrastructure.bus.rabbitmq.RabbitMqConfig @@ -31,7 +32,12 @@ abstract class HttpSpec extends WordSpec with Matchers with ScalaFutures with Sc sharedDependencies.messagePublisher )(sharedDependencies.executionContext) - private val routes = new Routes(new EntryPointDependencyContainer(userDependencies, videoDependencies)) + protected val podcastDependencies = new PodcastModuleDependencyContainer( + sharedDependencies.doobieDbConnection, + sharedDependencies.messagePublisher + )(sharedDependencies.executionContext) + + private val routes = new Routes(new EntryPointDependencyContainer(userDependencies, videoDependencies, podcastDependencies)) protected val doobieDbConnection: DoobieDbConnection = sharedDependencies.doobieDbConnection @@ -46,4 +52,4 @@ abstract class HttpSpec extends WordSpec with Matchers with ScalaFutures with Sc ) ~> routes.all ~> check(body) protected def getting[T](path: String)(body: ⇒ T): T = Get(path) ~> routes.all ~> check(body) -} +} \ No newline at end of file diff --git a/database/podcast.sql b/database/podcast.sql new file mode 100644 index 0000000..92a040e --- /dev/null +++ b/database/podcast.sql @@ -0,0 +1,16 @@ +CREATE TABLE podcasts +( + id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT, + podcast_id CHAR(36) NOT NULL, + title VARCHAR(255) NOT NULL, + duration_in_seconds BIGINT(20) UNSIGNED NOT NULL, + description VARCHAR(255) NOT NULL, + rating INT NOT NULL, + votes INT NOT NULL, + updated_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + PRIMARY KEY (id), + UNIQUE KEY u_podcast_id (podcast_id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/application/create/PodcastsCreator.scala b/src/mooc/main/tv/codely/mooc/podcast/application/create/PodcastsCreator.scala new file mode 100644 index 0000000..dd705ef --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/application/create/PodcastsCreator.scala @@ -0,0 +1,20 @@ +package tv.codely.mooc.podcast.application.create + +import tv.codely.mooc.podcast.domain._ +import tv.codely.mooc.shared.infrastructure.marshaller.DomainEventsMarshaller.MessageMarshaller +import tv.codely.shared.domain.bus.MessagePublisher + +final class PodcastsCreator(repository: PodcastRepository, publisher: MessagePublisher) { + def create( + id: PodcastId, + title: PodcastTitle, + duration: PodcastDuration, + description: PodcastDescription, + ): Unit = { + val podcast = Podcast(id, title, duration, description, PodcastRating(0), PodcastVotes(0)) + + repository.save(podcast) + + publisher.publish(PodcastCreated(podcast))(MessageMarshaller) + } +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/application/rating/PodcastRater.scala b/src/mooc/main/tv/codely/mooc/podcast/application/rating/PodcastRater.scala new file mode 100644 index 0000000..e6f5e49 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/application/rating/PodcastRater.scala @@ -0,0 +1,20 @@ +package tv.codely.mooc.podcast.application.rating + +import tv.codely.mooc.podcast.domain._ + +import scala.concurrent.ExecutionContext.Implicits.global + +final class PodcastRater(repository: PodcastRepository) { + def rate( + id: PodcastId, + value: PodcastRating + ): Unit = { + repository.get(id).map( + p => { + val newRating = (p.rating.value + value.value) / (p.votes.votes + 1) + val podcast = p.copy(rating = PodcastRating(newRating), votes = PodcastVotes(p.votes.votes + 1)) + repository.update(podcast) + } + ) + } +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/application/search/PodcastsSearcher.scala b/src/mooc/main/tv/codely/mooc/podcast/application/search/PodcastsSearcher.scala new file mode 100644 index 0000000..880a7cb --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/application/search/PodcastsSearcher.scala @@ -0,0 +1,9 @@ +package tv.codely.mooc.podcast.application.search + +import tv.codely.mooc.podcast.domain.{Podcast, PodcastRepository} + +import scala.concurrent.Future + +final class PodcastsSearcher(repository: PodcastRepository) { + def all(): Future[Seq[Podcast]] = repository.all() +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/Podcast.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/Podcast.scala new file mode 100644 index 0000000..88578c6 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/Podcast.scala @@ -0,0 +1,21 @@ +package tv.codely.mooc.podcast.domain + +import scala.concurrent.duration.Duration + +object Podcast { + def apply(id: String, title: String, duration: Duration, description: String, rating: BigDecimal, votes: Int): Podcast = Podcast( + PodcastId(id), + PodcastTitle(title), + PodcastDuration(duration), + PodcastDescription(description), + PodcastRating(rating), + PodcastVotes(votes) + ) +} + +case class Podcast(id: PodcastId, + title: PodcastTitle, + duration: PodcastDuration, + description: PodcastDescription, + rating: PodcastRating, + votes: PodcastVotes) \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastCreated.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastCreated.scala new file mode 100644 index 0000000..d22b01a --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastCreated.scala @@ -0,0 +1,28 @@ +package tv.codely.mooc.podcast.domain + +import tv.codely.shared.domain.bus.Message + +object PodcastCreated { + def apply(id: String, title: String, duration: BigDecimal, description: String, rating: BigDecimal, votes: Int): PodcastCreated = apply( + PodcastId(id), + PodcastTitle(title), + PodcastDuration(duration), + PodcastDescription(description), + PodcastRating(rating), + PodcastVotes(votes) + ) + + def apply(podcast: Podcast): PodcastCreated = + apply(podcast.id, podcast.title, podcast.duration, podcast.description, podcast.rating, podcast.votes) +} + +final case class PodcastCreated( + id: PodcastId, + title: PodcastTitle, + duration: PodcastDuration, + description: PodcastDescription, + rating: PodcastRating, + votes: PodcastVotes +) extends Message { + override val subType: String = "podcast_created" +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDescription.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDescription.scala new file mode 100644 index 0000000..52de77f --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDescription.scala @@ -0,0 +1,3 @@ +package tv.codely.mooc.podcast.domain + +case class PodcastDescription (description: String) \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDuration.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDuration.scala new file mode 100644 index 0000000..dd17e33 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastDuration.scala @@ -0,0 +1,9 @@ +package tv.codely.mooc.podcast.domain + +import scala.concurrent.duration.{Duration, DurationLong} + +object PodcastDuration { + def apply(seconds: BigDecimal): PodcastDuration = PodcastDuration(seconds.longValue().seconds) +} + +case class PodcastDuration(value: Duration) \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastId.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastId.scala new file mode 100644 index 0000000..cad31bc --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastId.scala @@ -0,0 +1,9 @@ +package tv.codely.mooc.podcast.domain + +import java.util.UUID + +object PodcastId { + def apply(value: String): PodcastId = PodcastId(UUID.fromString(value)) +} + +case class PodcastId(value: UUID) \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRating.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRating.scala new file mode 100644 index 0000000..55c91fc --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRating.scala @@ -0,0 +1,3 @@ +package tv.codely.mooc.podcast.domain + +case class PodcastRating(value: BigDecimal) \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRepository.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRepository.scala new file mode 100644 index 0000000..0d4fbea --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastRepository.scala @@ -0,0 +1,10 @@ +package tv.codely.mooc.podcast.domain + +import scala.concurrent.Future + +trait PodcastRepository { + def all(): Future[Seq[Podcast]] + def get(id: PodcastId): Future[Podcast] + def save(podcast: Podcast): Future[Unit] + def update(podcast: Podcast): Future[Unit] +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastTitle.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastTitle.scala new file mode 100644 index 0000000..3ec808d --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastTitle.scala @@ -0,0 +1,3 @@ +package tv.codely.mooc.podcast.domain + +case class PodcastTitle (title: String) \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastVotes.scala b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastVotes.scala new file mode 100644 index 0000000..0fcee3e --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/domain/PodcastVotes.scala @@ -0,0 +1,3 @@ +package tv.codely.mooc.podcast.domain + +case class PodcastVotes(votes: Int) \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/infrastructure/dependency_injection/PodcastModuleDependencyContainer.scala b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/dependency_injection/PodcastModuleDependencyContainer.scala new file mode 100644 index 0000000..8177d7f --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/dependency_injection/PodcastModuleDependencyContainer.scala @@ -0,0 +1,22 @@ +package tv.codely.mooc.podcast.infrastructure.dependency_injection + +import tv.codely.mooc.podcast.application.create.PodcastsCreator +import tv.codely.mooc.podcast.application.rating.PodcastRater +import tv.codely.mooc.podcast.application.search.PodcastsSearcher +import tv.codely.mooc.podcast.domain.PodcastRepository +import tv.codely.mooc.podcast.infrastructure.repository.DoobieMySqlPodcastRepository +import tv.codely.shared.domain.bus.MessagePublisher +import tv.codely.shared.infrastructure.doobie.DoobieDbConnection + +import scala.concurrent.ExecutionContext + +final class PodcastModuleDependencyContainer( + doobieDbConnection: DoobieDbConnection, + messagePublisher: MessagePublisher +)(implicit executionContext: ExecutionContext) { + val repository: PodcastRepository = new DoobieMySqlPodcastRepository(doobieDbConnection) + + val podcastsSearcher: PodcastsSearcher = new PodcastsSearcher(repository) + val podcastCreator: PodcastsCreator = new PodcastsCreator(repository, messagePublisher) + val podcastRater: PodcastRater = new PodcastRater(repository) +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastAttributesJsonFormatMarshaller.scala b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastAttributesJsonFormatMarshaller.scala new file mode 100644 index 0000000..62ceca2 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastAttributesJsonFormatMarshaller.scala @@ -0,0 +1,62 @@ +package tv.codely.mooc.podcast.infrastructure.marshaller + +import java.util.UUID + +import spray.json.{DeserializationException, JsNumber, JsString, JsValue, JsonFormat, _} +import tv.codely.mooc.podcast.domain._ +import tv.codely.shared.infrastructure.marshaller.UuidJsonFormatMarshaller._ + +import java.util.UUID + +object PodcastAttributesJsonFormatMarshaller { + implicit object PodcastIdMarshaller extends JsonFormat[PodcastId] { + override def write(value: PodcastId): JsValue = value.value.toJson + + override def read(value: JsValue): PodcastId = PodcastId(value.convertTo[UUID]) + } + + implicit object PodcastTitleMarshaller extends JsonFormat[PodcastTitle] { + override def write(value: PodcastTitle): JsValue = JsString(value.title) + + override def read(value: JsValue): PodcastTitle = value match { + case JsString(name) => PodcastTitle(name) + case _ => throw DeserializationException("Expected 1 string for PodcastTitle") + } + } + + implicit object PodcastDurationMarshaller extends JsonFormat[PodcastDuration] { + override def write(value: PodcastDuration): JsValue = JsNumber(value.value.toSeconds) + + override def read(value: JsValue): PodcastDuration = value match { + case JsNumber(seconds) => PodcastDuration(seconds) + case _ => throw DeserializationException("Expected 1 string for PodcastDuration") + } + } + + implicit object PodcastCategoryMarshaller extends JsonFormat[PodcastDescription] { + override def write(value: PodcastDescription): JsValue = JsString(value.description) + + override def read(value: JsValue): PodcastDescription = value match { + case JsString(name) => PodcastDescription(name) + case _ => throw DeserializationException("Expected 1 string for PodcastDescription") + } + } + + implicit object PodcastRatingMarshaller extends JsonFormat[PodcastRating] { + override def write(value: PodcastRating): JsValue = JsNumber(value.value) + + override def read(value: JsValue): PodcastRating = value match { + case JsNumber(value) => PodcastRating(value.intValue()) + case _ => throw DeserializationException("Expected 1 string for PodcastRating") + } + } + + implicit object PodcastVotesMarshaller extends JsonFormat[PodcastVotes] { + override def write(value: PodcastVotes): JsValue = JsNumber(value.votes) + + override def read(value: JsValue): PodcastVotes = value match { + case JsNumber(value) => PodcastVotes(value.intValue()) + case _ => throw DeserializationException("Expected 1 string for PodcastVotes") + } + } +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastCreatedJsonFormatMarshaller.scala b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastCreatedJsonFormatMarshaller.scala new file mode 100644 index 0000000..8a5c4de --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastCreatedJsonFormatMarshaller.scala @@ -0,0 +1,31 @@ +package tv.codely.mooc.podcast.infrastructure.marshaller + +import spray.json.{DefaultJsonProtocol, DeserializationException, JsString, JsValue, RootJsonFormat, _} +import tv.codely.mooc.podcast.domain._ +import tv.codely.mooc.podcast.infrastructure.marshaller.PodcastAttributesJsonFormatMarshaller._ + +object PodcastCreatedJsonFormatMarshaller extends DefaultJsonProtocol { + + implicit object PodcastCreatedJsonFormat extends RootJsonFormat[PodcastCreated] { + override def write(c: PodcastCreated): JsValue = JsObject( + "type" -> JsString(c.`type`), + "id" -> c.id.toJson, + "title" -> c.title.toJson, + "duration_in_seconds" -> c.duration.toJson, + "description" -> c.description.toJson, + "rating" -> c.rating.toJson, + "votes" -> c.votes.toJson + ) + + override def read(value: JsValue): PodcastCreated = + value.asJsObject.getFields("id", "title", "duration_in_seconds", "description", "rating", "votes") match { + case Seq(JsString(id), JsString(title), JsNumber(duration), JsString(description), JsNumber(rating), JsNumber(votes)) => + PodcastCreated(id, title, duration, description, rating, votes.intValue()) + case unknown => + throw DeserializationException( + s"Error reading PodcastCreated JSON <$unknown>" + ) + } + } + +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastJsonFormatMarshaller.scala b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastJsonFormatMarshaller.scala new file mode 100644 index 0000000..17612bc --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/marshaller/PodcastJsonFormatMarshaller.scala @@ -0,0 +1,18 @@ +package tv.codely.mooc.podcast.infrastructure.marshaller + +import spray.json.{DefaultJsonProtocol, RootJsonFormat} +import tv.codely.mooc.podcast.domain._ +import tv.codely.mooc.podcast.infrastructure.marshaller.PodcastAttributesJsonFormatMarshaller._ + + +object PodcastJsonFormatMarshaller extends DefaultJsonProtocol { + implicit val podcastFormat: RootJsonFormat[Podcast] = jsonFormat( + Podcast.apply(_: PodcastId, _: PodcastTitle, _: PodcastDuration, _: PodcastDescription, _: PodcastRating, _:PodcastVotes), + "id", + "title", + "duration_in_seconds", + "description", + "rating", + "votes" + ) +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/podcast/infrastructure/repository/DoobieMySqlPodcastRepository.scala b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/repository/DoobieMySqlPodcastRepository.scala new file mode 100644 index 0000000..5bf69c2 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/podcast/infrastructure/repository/DoobieMySqlPodcastRepository.scala @@ -0,0 +1,29 @@ +package tv.codely.mooc.podcast.infrastructure.repository + +import doobie.implicits._ +import tv.codely.mooc.podcast.domain.{Podcast, PodcastId, PodcastRepository} +import tv.codely.shared.infrastructure.doobie.DoobieDbConnection +import tv.codely.mooc.shared.infrastructure.doobie.TypesConversions._ + +import scala.concurrent.{ExecutionContext, Future} + +final class DoobieMySqlPodcastRepository(db: DoobieDbConnection)(implicit executionContext: ExecutionContext) + extends PodcastRepository { + override def all(): Future[Seq[Podcast]] = + db.read(sql"SELECT podcast_id, title, duration_in_seconds, description, rating, votes FROM podcasts".query[Podcast].to[Seq]) + + override def get(podcastId: PodcastId): Future[Podcast] = + db.read(sql"SELECT podcast_id, title, duration_in_seconds, description, rating, votes FROM podcasts Where podcast_id = ${podcastId} ".query[Podcast].unique) + + override def save(podcast: Podcast): Future[Unit] = + sql"INSERT INTO podcasts(podcast_id, title, duration_in_seconds, description, rating, votes) VALUES (${podcast.id}, ${podcast.title}, ${podcast.duration}, ${podcast.description}, ${podcast.rating}, ${podcast.votes})".update.run + .transact(db.transactor) + .unsafeToFuture() + .map(_ => ()) + + override def update(podcast: Podcast): Future[Unit] = + sql"UPDATE podcasts set rating = ${podcast.rating}, votes = ${podcast.votes} where podcast_id = ${podcast.id}".update.run + .transact(db.transactor) + .unsafeToFuture() + .map(_ => ()) +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/shared/infrastructure/marshaller/DomainEventsMarshaller.scala b/src/mooc/main/tv/codely/mooc/shared/infrastructure/marshaller/DomainEventsMarshaller.scala index c7474cf..d0329fe 100644 --- a/src/mooc/main/tv/codely/mooc/shared/infrastructure/marshaller/DomainEventsMarshaller.scala +++ b/src/mooc/main/tv/codely/mooc/shared/infrastructure/marshaller/DomainEventsMarshaller.scala @@ -1,10 +1,12 @@ package tv.codely.mooc.shared.infrastructure.marshaller import spray.json.{DeserializationException, JsString, JsValue, RootJsonFormat, SerializationException, _} +import tv.codely.mooc.podcast.domain.PodcastCreated import tv.codely.mooc.user.domain.UserRegistered import tv.codely.mooc.user.infrastructure.marshaller.UserRegisteredJsonFormatMarshaller._ import tv.codely.mooc.video.domain.VideoCreated import tv.codely.mooc.video.infrastructure.marshaller.VideoCreatedJsonFormatMarshaller._ +import tv.codely.mooc.podcast.infrastructure.marshaller.PodcastCreatedJsonFormatMarshaller._ import tv.codely.shared.domain.bus.Message object DomainEventsMarshaller { @@ -12,14 +14,16 @@ object DomainEventsMarshaller { override def write(m: Message): JsValue = m match { case vc: VideoCreated => vc.toJson case ur: UserRegistered => ur.toJson + case pd: PodcastCreated => pd.toJson case unknown => throw new SerializationException(s"Unknown message type to write <${unknown.getClass}>") } override def read(jv: JsValue): Message = jv.asJsObject.getFields("type") match { case Seq(JsString("cqrs_ddd_scala_example.video_created")) => jv.convertTo[VideoCreated] case Seq(JsString("cqrs_ddd_scala_example.user_registered")) => jv.convertTo[UserRegistered] + case Seq(JsString("cqrs_ddd_scala_example.podcast_created")) => jv.convertTo[PodcastCreated] case Seq(JsString(unknown)) => throw DeserializationException(s"Unknown message type to read <$unknown>") } } -} +} \ No newline at end of file diff --git a/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala b/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala index 0f2dfdd..ffcc387 100644 --- a/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala +++ b/src/mooc/main/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepository.scala @@ -17,4 +17,4 @@ final class DoobieMySqlVideoRepository(db: DoobieDbConnection)(implicit executio .transact(db.transactor) .unsafeToFuture() .map(_ => ()) -} +} \ No newline at end of file