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

Feature: Get shortest Video #33

Open
wants to merge 6 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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package tv.codely.mooc.api

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.api.controller.video.{VideoGetController, VideoPostController, VideoGetShortestController}
import tv.codely.mooc.user.infrastructure.dependency_injection.UserModuleDependencyContainer
import tv.codely.mooc.video.infrastructure.dependency_injection.VideoModuleDependencyContainer

Expand All @@ -17,4 +17,6 @@ final class EntryPointDependencyContainer(

val videoGetController = new VideoGetController(videoDependencies.videosSearcher)
val videoPostController = new VideoPostController(videoDependencies.videoCreator)

val videoGetShortestController = new VideoGetShortestController(videoDependencies.shortestVideoSearcher)
}
9 changes: 8 additions & 1 deletion app/main/tv/codely/mooc/api/Routes.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ final class Routes(container: EntryPointDependencyContainer) {
}

private val video = get {
path("videos")(container.videoGetController.get())
path("videos" / "shortest") {
(container.videoGetShortestController.get())
} ~
path("videos") {
(container.videoGetController.get())
}

} ~
post {
path("videos") {
Expand All @@ -43,6 +49,7 @@ final class Routes(container: EntryPointDependencyContainer) {
}
}
}


val all: Route = status ~ user ~ video

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package tv.codely.mooc.api.controller.video

import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport
import akka.http.scaladsl.server.StandardRoute
import akka.http.scaladsl.server.Directives.complete
import spray.json.DefaultJsonProtocol
import tv.codely.mooc.video.application.search.ShortestVideoSearcher
import tv.codely.mooc.video.infrastructure.marshaller.VideoJsonFormatMarshaller._

final class VideoGetShortestController(searcher: ShortestVideoSearcher) extends SprayJsonSupport with DefaultJsonProtocol {
def get(): StandardRoute = complete(searcher.shortest())
}
15 changes: 15 additions & 0 deletions app/test/tv/codely/mooc/api/VideoEntryPointShould.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import doobie.implicits._
import org.scalatest.BeforeAndAfterEach
import spray.json._
import tv.codely.mooc.video.domain.VideoMother
import tv.codely.mooc.video.domain.VideoDuration
import tv.codely.mooc.video.infrastructure.marshaller.VideoJsValueMarshaller
import tv.codely.HttpSpec

Expand Down Expand Up @@ -47,4 +48,18 @@ final class VideoEntryPointShould extends HttpSpec with BeforeAndAfterEach {
entityAs[String].parseJson shouldBe VideoJsValueMarshaller.marshall(videos)
}
}

"return the shortest video" in {
val fiveSecondsVideo = VideoMother.random.copy(duration = VideoDuration(5))
val threeSecondsVideo = VideoMother.random.copy(duration = VideoDuration(3))
val existingVideos = Seq(fiveSecondsVideo, threeSecondsVideo)

existingVideos.foreach(v => videoDependencies.repository.save(v).futureValue)

getting("/videos/shortest") {
status shouldBe StatusCodes.OK
contentType shouldBe ContentTypes.`application/json`
entityAs[String].parseJson shouldBe VideoJsValueMarshaller.marshall(threeSecondsVideo)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package tv.codely.mooc.video.application.search

import tv.codely.mooc.video.domain.{Video, VideoRepository}

import scala.concurrent.Future

final class ShortestVideoSearcher(repository: VideoRepository) {
def shortest(): Future[Option[Video]] = repository.shortest()
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ trait VideoRepository {
def all(): Future[Seq[Video]]

def save(video: Video): Future[Unit]

def shortest(): Future[Option[Video]]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tv.codely.mooc.video.infrastructure.dependency_injection
import tv.codely.shared.infrastructure.doobie.DoobieDbConnection
import tv.codely.mooc.video.application.create.VideoCreator
import tv.codely.mooc.video.application.search.VideosSearcher
import tv.codely.mooc.video.application.search.ShortestVideoSearcher
import tv.codely.mooc.video.domain.VideoRepository
import tv.codely.mooc.video.infrastructure.repository.DoobieMySqlVideoRepository
import scala.concurrent.ExecutionContext
Expand All @@ -17,4 +18,5 @@ final class VideoModuleDependencyContainer(

val videosSearcher: VideosSearcher = new VideosSearcher(repository)
val videoCreator: VideoCreator = new VideoCreator(repository, messagePublisher)
val shortestVideoSearcher: ShortestVideoSearcher = new ShortestVideoSearcher(repository)
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,7 @@ final class DoobieMySqlVideoRepository(db: DoobieDbConnection)(implicit executio
.transact(db.transactor)
.unsafeToFuture()
.map(_ => ())

override def shortest(): Future[Option[Video]] =
db.read(sql"SELECT video_id, title, duration_in_seconds, category, creator_id FROM videos ORDER BY duration_in_seconds ASC LIMIT 1".query[Video].option)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package tv.codely.mooc.video.application.search

import tv.codely.mooc.video.domain.VideoMother
import tv.codely.mooc.video.domain.VideoDuration
import tv.codely.mooc.video.infrastructure.repository.VideoRepositoryMock
import tv.codely.shared.infrastructure.unit.UnitTestCase

final class ShortestVideoSearcherShould extends UnitTestCase with VideoRepositoryMock {
private val searcher = new ShortestVideoSearcher(repository)

"search the shortest existing video" in {
val fiveSecondsVideo = VideoMother.random.copy(duration = VideoDuration(5))
val threeSecondsVideo = VideoMother.random.copy(duration = VideoDuration(3))
val existingVideos = Seq(fiveSecondsVideo, threeSecondsVideo)

repositoryShouldFindShortest(existingVideos)

searcher.shortest().futureValue shouldBe Option(threeSecondsVideo)
}

"return None when search the shortest existing video and there is no video" in {
repositoryShouldFindShortestNone()

searcher.shortest().futureValue shouldBe None
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,14 @@ import spray.json.{JsArray, JsNumber, JsObject, JsString}
import tv.codely.mooc.video.domain.Video

object VideoJsValueMarshaller {
def marshall(v: Video): JsObject = JsObject(
"id" -> JsString(v.id.value.toString),
"title" -> JsString(v.title.value),
"duration_in_seconds" -> JsNumber(v.duration.value.toSeconds),
"category" -> JsString(v.category.toString),
"creator_id" -> JsString(v.creatorId.value.toString)
)
def marshall(videos: Seq[Video]): JsArray = JsArray(
videos
.map(
v =>
JsObject(
"id" -> JsString(v.id.value.toString),
"title" -> JsString(v.title.value),
"duration_in_seconds" -> JsNumber(v.duration.value.toSeconds),
"category" -> JsString(v.category.toString),
"creator_id" -> JsString(v.creatorId.value.toString)
)
)
.toVector
videos.map(v => marshall(v)).toVector
)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package tv.codely.mooc.video.infrastructure.repository

import tv.codely.mooc.video.VideoIntegrationTestCase
import tv.codely.mooc.video.domain.VideoMother
import tv.codely.mooc.video.domain.{ VideoMother, VideoDuration}
import doobie.implicits._
import org.scalatest.BeforeAndAfterEach

Expand Down Expand Up @@ -29,4 +29,18 @@ final class DoobieMySqlVideoRepositoryShould extends VideoIntegrationTestCase wi

repository.all().futureValue shouldBe videos
}

"search shortest existing video" in {
val fiveSecondsVideo = VideoMother.random.copy(duration = VideoDuration(5))
val threeSecondsVideo = VideoMother.random.copy(duration = VideoDuration(3))
val existingVideos = Seq(fiveSecondsVideo, threeSecondsVideo)

existingVideos.foreach(v => repository.save(v).futureValue)

repository.shortest().futureValue shouldBe Option(threeSecondsVideo)
}

"return an empty object when search shortest existing video and there are no videos" in {
repository.shortest().futureValue shouldBe None
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,14 @@ protected[video] trait VideoRepositoryMock extends MockFactory {
(repository.all _)
.expects()
.returning(Future.successful(videos))

protected def repositoryShouldFindShortest(videos: Seq[Video]): Unit =
(repository.shortest _)
.expects()
.returning(Future.successful(Option(videos(1))))

protected def repositoryShouldFindShortestNone(): Unit =
(repository.shortest _)
.expects()
.returning(Future.successful(None))
}