diff --git a/README.md b/README.md
index 4c66288..4e5889f 100644
--- a/README.md
+++ b/README.md
@@ -3,6 +3,7 @@
+
[](LICENSE)
[](https://travis-ci.org/CodelyTV/cqrs-ddd-scala-example)
[](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/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
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