├── .scalafmt.conf ├── .gitignore ├── src ├── main │ ├── scala │ │ └── io │ │ │ └── github │ │ │ └── spf3000 │ │ │ └── hutsapi │ │ │ ├── entities │ │ │ └── Hut.scala │ │ │ ├── HutRepository.scala │ │ │ └── HutServer.scala │ └── resources │ │ └── logback.xml └── test │ └── scala │ └── io │ └── github │ └── spf3000 │ └── hutsapi │ └── HutSpec.scala └── README.md /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "2.0.1" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .tags 2 | .idea/ 3 | project/ 4 | target/ 5 | -------------------------------------------------------------------------------- /src/main/scala/io/github/spf3000/hutsapi/entities/Hut.scala: -------------------------------------------------------------------------------- 1 | package io.github.spf3000.hutsapi.entities 2 | 3 | 4 | case class Hut(name: String) 5 | 6 | case class HutWithId(id: String, name: String) 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | To run the app: `sbt run` 5 | 6 | Example commands (GET, POST, PUT, DELETE): 7 | 8 | 9 | curl -i http://localhost:8080/huts/123 10 | 11 | 12 | curl -v -H "Content-Type: application/json" -X POST http://localhost:8080/huts -d '{"name":"River Hut"}' 13 | 14 | 15 | curl -v -H "Content-Type: application/json" -X PUT http://localhost:8080/huts -d '{"id":"123","name":"Mountain Hut"}' 16 | 17 | 18 | curl -v -X DELETE http://localhost:8080/huts/123 -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | true 9 | 10 | [%thread] %highlight(%-5level) %cyan(%logger{15}) - %msg %n 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/main/scala/io/github/spf3000/hutsapi/HutRepository.scala: -------------------------------------------------------------------------------- 1 | package io.github.spf3000.hutsapi 2 | 3 | import java.util.UUID 4 | import cats.effect._ 5 | import scala.collection.mutable.ListBuffer 6 | import cats.FlatMap 7 | import cats.implicits._ 8 | import cats.effect.IO 9 | import io.github.spf3000.hutsapi.entities._ 10 | 11 | 12 | final case class HutRepository[F[_]](private val huts: ListBuffer[HutWithId])(implicit e: Effect[F]) { 13 | val makeId: F[String] = e.delay { UUID.randomUUID().toString } 14 | 15 | def getHut(id: String): F[Option[HutWithId]] = 16 | e.delay { huts.find(_.id == id) } 17 | 18 | def addHut(hut: Hut): F[String] = 19 | for { 20 | uuid <- makeId 21 | _ <- e.delay { huts += hutWithId(hut, uuid) } 22 | } yield uuid 23 | 24 | def updateHut(hutWithId: HutWithId): F[Unit] = { 25 | for { 26 | _ <- e.delay { huts -= hutWithId } 27 | _ <- e.delay { huts += hutWithId } 28 | } yield() 29 | } 30 | 31 | def deleteHut(hutId: String): F[Unit] = 32 | e.delay { huts.find(_.id == hutId).foreach(h => huts -= h) } 33 | 34 | 35 | def hutWithId(hut: Hut, id: String): HutWithId = 36 | HutWithId(id, hut.name) 37 | } 38 | object HutRepository { 39 | def empty[F[_]](implicit m: Effect[F]): IO[HutRepository[F]] = IO{new HutRepository[F](ListBuffer())} 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala/io/github/spf3000/hutsapi/HutServer.scala: -------------------------------------------------------------------------------- 1 | package io.github.spf3000.hutsapi 2 | 3 | import cats.effect.IO 4 | import cats.Monad 5 | import cats.FlatMap 6 | import cats.implicits._ 7 | import cats.effect._ 8 | import fs2.StreamApp 9 | import fs2.Stream 10 | import io.circe.generic.auto._ 11 | import io.circe.syntax._ 12 | 13 | import org.http4s._ 14 | import org.http4s.circe._ 15 | import org.http4s.dsl.Http4sDsl 16 | import org.http4s.server.blaze.BlazeBuilder 17 | import org.http4s.dsl.io._ 18 | 19 | import scala.concurrent.ExecutionContext.Implicits.global 20 | 21 | import entities.Hut 22 | import entities._ 23 | 24 | object HutServer extends StreamApp[IO] with Http4sDsl[IO] { 25 | 26 | val HUTS = "huts" 27 | 28 | def service[F[_]](hutRepo: HutRepository[F])(implicit F: Effect[F]) = HttpService[F] { 29 | 30 | case GET -> Root / HUTS / hutId => 31 | hutRepo.getHut(hutId) 32 | .flatMap{ 33 | case Some(hut) => Response(status = Status.Ok).withBody(hut.asJson) 34 | case None => F.pure(Response(status = Status.NotFound)) 35 | } 36 | 37 | case req @ POST -> Root / HUTS => 38 | req.decodeJson[Hut] 39 | .flatMap(hutRepo.addHut) 40 | .flatMap(hut => Response(status = Status.Created).withBody(hut.asJson)) 41 | 42 | case req @ PUT -> Root / HUTS => 43 | req.decodeJson[HutWithId] 44 | .flatMap(hutRepo.updateHut) 45 | .flatMap(_ => F.pure(Response(status = Status.Ok))) 46 | 47 | case DELETE -> Root / HUTS / hutId => 48 | hutRepo.deleteHut(hutId) 49 | .flatMap(_ => F.pure(Response(status = Status.NoContent))) 50 | } 51 | 52 | def stream(args: List[String], requestShutdown: IO[Unit]) = 53 | Stream.eval(HutRepository.empty[IO]).flatMap { hutRepo => 54 | BlazeBuilder[IO] 55 | .bindHttp(8080, "0.0.0.0") 56 | .mountService(service(hutRepo), "/") 57 | .serve 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/test/scala/io/github/spf3000/hutsapi/HutSpec.scala: -------------------------------------------------------------------------------- 1 | package io.github.spf3000.hutsapi 2 | 3 | 4 | import cats._ 5 | import cats.effect.IO 6 | import io.circe.generic.auto._ 7 | import io.circe.syntax._ 8 | import io.circe.Json 9 | import io.github.spf3000.hutsapi.entities._ 10 | import org.http4s.circe._ 11 | import org.http4s._ 12 | import org.http4s.implicits._ 13 | import org.specs2.matcher.MatchResult 14 | import org.specs2.mutable.Specification 15 | import scala.io.Source 16 | import scala.collection.mutable.ListBuffer 17 | 18 | 19 | class HutSpec extends Specification { 20 | 21 | 22 | "Get Huts" >> { 23 | "return 200" >> { 24 | getHutsReturns200() 25 | } 26 | "return huts" >> { 27 | getHutsReturnsHut() 28 | } 29 | } 30 | 31 | "Post Huts" >> { 32 | "return 201" >> { 33 | postHutReturns201() 34 | } 35 | } 36 | 37 | "Put Huts" >> { 38 | "return 200" >> { 39 | putHutReturns200() 40 | } 41 | } 42 | 43 | "Delete Huts" >> { 44 | "return 204" >> { 45 | deleteHutReturns204() 46 | } 47 | } 48 | 49 | 50 | val hutWithId = HutWithId("123", "Mountain Hut") 51 | val hut = Hut("Mountain Hut") 52 | 53 | val hutRepo = HutRepository[IO](ListBuffer(hutWithId)) 54 | 55 | def testService(): HttpService[IO] = HutServer.service[IO](hutRepo) 56 | 57 | private[this] val retGetHut: Response[IO] = { 58 | val getLstngs = Request[IO](Method.GET, Uri.uri("/huts/123")) 59 | testService.orNotFound(getLstngs).unsafeRunSync() 60 | } 61 | 62 | private[this] def getHutsReturns200(): MatchResult[Status] = 63 | retGetHut.status must beEqualTo(Status.Ok) 64 | 65 | private[this] def getHutsReturnsHut(): MatchResult[String] = { 66 | val hut = Json.fromString("""{"id":"123","name":"Mountain Hut"}""") 67 | retGetHut.as[String].unsafeRunSync() must beEqualTo(hut.asString.get) 68 | } 69 | 70 | private[this] val retPostHut: Response[IO] = { 71 | val postLstngs = Request[IO](Method.POST, Uri.uri("/huts")).withBody(hut.asJson).unsafeRunSync() 72 | testService().orNotFound(postLstngs).unsafeRunSync() 73 | } 74 | 75 | private[this] def postHutReturns201(): MatchResult[Status] = 76 | retPostHut.status must beEqualTo(Status.Created) 77 | 78 | private[this] def retPutHut: Response[IO] = { 79 | val putLsting = Request[IO](Method.PUT, Uri.uri("/huts")).withBody(hutWithId.asJson).unsafeRunSync() 80 | testService().orNotFound(putLsting).unsafeRunSync() 81 | } 82 | 83 | private[this] def putHutReturns200(): MatchResult[Status] = 84 | retPutHut.status must beEqualTo(Status.Ok) 85 | 86 | 87 | private[this] def retDeleteHut: Response[IO] = { 88 | val delLstng = Request[IO](Method.DELETE, Uri.uri("/huts/1234")) 89 | testService.orNotFound(delLstng).unsafeRunSync() 90 | } 91 | 92 | private[this] def deleteHutReturns204(): MatchResult[Status] = 93 | retDeleteHut.status must beEqualTo(Status.NoContent) 94 | 95 | 96 | } 97 | --------------------------------------------------------------------------------