diff --git a/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala b/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala index c05109f..7cc58a6 100644 --- a/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala +++ b/app/main/tv/codely/mooc/api/EntryPointDependencyContainer.scala @@ -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 @@ -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) } diff --git a/app/main/tv/codely/mooc/api/Routes.scala b/app/main/tv/codely/mooc/api/Routes.scala index e6035e1..95f4be3 100644 --- a/app/main/tv/codely/mooc/api/Routes.scala +++ b/app/main/tv/codely/mooc/api/Routes.scala @@ -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") { @@ -43,6 +49,7 @@ final class Routes(container: EntryPointDependencyContainer) { } } } + val all: Route = status ~ user ~ video diff --git a/app/main/tv/codely/mooc/api/controller/video/VideoGetShortestController.scala b/app/main/tv/codely/mooc/api/controller/video/VideoGetShortestController.scala new file mode 100644 index 0000000..6845040 --- /dev/null +++ b/app/main/tv/codely/mooc/api/controller/video/VideoGetShortestController.scala @@ -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()) +} diff --git a/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala b/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala index 9348f6a..4971eb3 100644 --- a/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala +++ b/app/test/tv/codely/mooc/api/VideoEntryPointShould.scala @@ -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 @@ -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) + } + } } diff --git a/src/mooc/main/tv/codely/mooc/video/application/search/ShortestVideoSearcher.scala b/src/mooc/main/tv/codely/mooc/video/application/search/ShortestVideoSearcher.scala new file mode 100644 index 0000000..fe3b2a6 --- /dev/null +++ b/src/mooc/main/tv/codely/mooc/video/application/search/ShortestVideoSearcher.scala @@ -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() +} diff --git a/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala b/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala index d770462..efe05ca 100644 --- a/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala +++ b/src/mooc/main/tv/codely/mooc/video/domain/VideoRepository.scala @@ -6,4 +6,6 @@ trait VideoRepository { def all(): Future[Seq[Video]] def save(video: Video): Future[Unit] + + def shortest(): Future[Option[Video]] } diff --git a/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala b/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala index 26ba3d5..b11567a 100644 --- a/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala +++ b/src/mooc/main/tv/codely/mooc/video/infrastructure/dependency_injection/VideoModuleDependencyContainer.scala @@ -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 @@ -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) } 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..0dff8c2 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,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) } diff --git a/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala b/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala new file mode 100644 index 0000000..73b7b8d --- /dev/null +++ b/src/mooc/test/tv/codely/mooc/video/application/search/ShortestVideoSearcherShould.scala @@ -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 + } +} diff --git a/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala b/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala index c8ca6de..977bc76 100644 --- a/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala +++ b/src/mooc/test/tv/codely/mooc/video/infrastructure/marshaller/VideoJsValueMarshaller.scala @@ -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 ) } diff --git a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala index f11e509..3c7e3be 100644 --- a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala +++ b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/DoobieMySqlVideoRepositoryShould.scala @@ -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 @@ -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 + } } diff --git a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala index 6433762..932c732 100644 --- a/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala +++ b/src/mooc/test/tv/codely/mooc/video/infrastructure/repository/VideoRepositoryMock.scala @@ -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)) }