Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added podcast service #36

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
<img src="http://codely.tv/wp-content/uploads/2016/05/cropped-logo-codelyTV.png" align="left" width="192px" height="192px"/>
<img align="left" width="0" height="192px" hspace="10"/>


[![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)
Expand Down Expand Up @@ -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.
11 changes: 9 additions & 2 deletions app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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)
}
7 changes: 4 additions & 3 deletions app/main/tv/codely/mooc/api/MoocApiApp.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -60,4 +61,4 @@ object MoocApiApp {
println("Server stopped!")
})
}
}
}
56 changes: 54 additions & 2 deletions app/main/tv/codely/mooc/api/Routes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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())
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
Original file line number Diff line number Diff line change
@@ -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))
}
}
10 changes: 8 additions & 2 deletions app/test/tv/codely/HttpSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
}
}
16 changes: 16 additions & 0 deletions database/podcast.sql
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -21,4 +21,4 @@ services:
- "5672:5672"
- "8181:15672"
env_file:
- .env
- .env
2 changes: 1 addition & 1 deletion project/build.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
sbt.version=1.2.8
sbt.version=1.4.7
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
)
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
21 changes: 21 additions & 0 deletions src/mooc/main/tv/codely/mooc/podcast/domain/Podcast.scala
Original file line number Diff line number Diff line change
@@ -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)
28 changes: 28 additions & 0 deletions src/mooc/main/tv/codely/mooc/podcast/domain/PodcastCreated.scala
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package tv.codely.mooc.podcast.domain

case class PodcastDescription (description: String)
Original file line number Diff line number Diff line change
@@ -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)
Loading