diff --git a/README.md b/README.md index 408c5fa..c1fef07 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,9 @@ # SnorriScalason -ScalaBridge Project - mentored by Noel Welsh +ScalaBridge Project - mentored by Zainab Ali & Noel Welsh +Tasks: + - Update krop to the latest version + - Change the way krop handles JSON + - Read and serve the JSON \ No newline at end of file diff --git a/backend/resources/book.json b/backend/resources/book.json new file mode 100644 index 0000000..62a6e6f --- /dev/null +++ b/backend/resources/book.json @@ -0,0 +1,52 @@ +[ + { + "id" : "978-0641723445", + "cat" : ["book","hardcover"], + "name" : "The Lightning Thief", + "author" : "Rick Riordan", + "series_t" : "Percy Jackson and the Olympians", + "sequence_i" : 1, + "genre_s" : "fantasy", + "inStock" : true, + "price" : 12.50, + "pages_i" : 384 + } + , + { + "id" : "978-1423103349", + "cat" : ["book","paperback"], + "name" : "The Sea of Monsters", + "author" : "Rick Riordan", + "series_t" : "Percy Jackson and the Olympians", + "sequence_i" : 2, + "genre_s" : "fantasy", + "inStock" : true, + "price" : 6.49, + "pages_i" : 304 + } + , + { + "id" : "978-1857995879", + "cat" : ["book","paperback"], + "name" : "Sophie's World : The Greek Philosophers", + "author" : "Jostein Gaarder", + "sequence_i" : 1, + "genre_s" : "fantasy", + "inStock" : true, + "price" : 3.07, + "pages_i" : 64 + } + , + { + "id" : "978-1933988177", + "cat" : ["book","paperback"], + "name" : "Lucene in Action, Second Edition", + "author" : "Michael McCandless", + "sequence_i" : 1, + "genre_s" : "IT", + "inStock" : true, + "price" : 30.50, + "pages_i" : 475 + } +] + \ No newline at end of file diff --git a/backend/src/main/resources/book.json b/backend/src/main/resources/book.json new file mode 100644 index 0000000..7a6ca06 --- /dev/null +++ b/backend/src/main/resources/book.json @@ -0,0 +1,60 @@ +[ + { + "id": "978-0641723445", + "name": "The Lightning Thief", + "author": "Rick Riordan", + "price": 12.50, + "pages": 384, + "genres": "fantasy", + "inStock": true, + "series_t": "Percy Jackson and the Olympians", + "sequence_i": 1, + "cat": [ + "book", + "hardcover" + ] + }, + { + "id": "978-1423103349", + "name": "The Sea of Monsters", + "author": "Rick Riordan", + "price": 6.49, + "pages": 304, + "genres": "fantasy", + "inStock": true, + "series_t": "Percy Jackson and the Olympians", + "sequence_i": 2, + "cat": [ + "book", + "paperback" + ] + }, + { + "id": "978-1857995879", + "name": "Sophie's World : The Greek Philosophers", + "author": "Jostein Gaarder", + "price": 3.07, + "pages": 64, + "genres": "fantasy", + "inStock": true, + "sequence_i": 1, + "cat": [ + "book", + "paperback" + ] + }, + { + "id": "978-1933988177", + "name": "Lucene in Action, Second Edition", + "author": "Michael McCandless", + "price": 30.50, + "pages": 475, + "genres": "IT", + "inStock": true, + "sequence_i": 1, + "cat": [ + "book", + "paperback" + ] + } +] diff --git a/backend/src/main/scala/snorri/Book.scala b/backend/src/main/scala/snorri/Book.scala new file mode 100644 index 0000000..d51bc1b --- /dev/null +++ b/backend/src/main/scala/snorri/Book.scala @@ -0,0 +1,19 @@ +package snorri + +import io.circe.{Encoder, Decoder} +import io.circe.generic.semiauto._ + +// NOTE: Not yet reading series_t,sequence_i and cat +case class Book( + id: String, + name: String, + author: String, + genres: String, + inStock: Boolean, + price: Double, + pages: Int +) + +object Book: + implicit val bookEncoder: Encoder[Book] = deriveEncoder[Book] + implicit val bookDecoder: Decoder[Book] = deriveDecoder[Book] diff --git a/backend/src/main/scala/snorri/Server.scala b/backend/src/main/scala/snorri/Server.scala index 8cd4470..251bd85 100644 --- a/backend/src/main/scala/snorri/Server.scala +++ b/backend/src/main/scala/snorri/Server.scala @@ -1,9 +1,13 @@ package snorri import krop.all.* +import krop.all.given object Server { - val app: Application = Snorri.route.otherwise(Application.notFound) + private val app: Application = + Snorri.getBooksRoute + .orElse(Snorri.echoRoute) + .orElse(Application.notFound) @main def go(): Unit = ServerBuilder.default.withApplication(app).run() diff --git a/backend/src/main/scala/snorri/Snorri.scala b/backend/src/main/scala/snorri/Snorri.scala index 43b6167..55e22bb 100644 --- a/backend/src/main/scala/snorri/Snorri.scala +++ b/backend/src/main/scala/snorri/Snorri.scala @@ -1,11 +1,49 @@ package snorri +import cats.effect.IO +import io.circe.parser.decode +import io.circe.syntax.* +import io.circe.{Encoder, Json} import krop.all.* +import krop.route.InvariantEntity +import org.http4s.circe.{CirceEntityDecoder, CirceEntityEncoder} + +import scala.io.{BufferedSource, Source} object Snorri { - val route = + private val jsonEntity: InvariantEntity[Json] = + Entity( + CirceEntityDecoder.circeEntityDecoder, + CirceEntityEncoder.circeEntityEncoder + ) + + val echoRoute = Route( Request.get(Path.root / "echo" / Param.string), - Response.ok[String] - ).handle(param => s"Your message was ${param}") + Response.ok(Entity.text) + ).handle(param => s"Your message was $param") + + val getBooksRoute = + Route( + Request.get(Path.root / "books"), + Response.ok(jsonEntity) + ).handle(() => loadBooks().asJson) + + private def loadBooks(): List[Book] = + decode[List[Book]](retrieveBooks()) match { + case Right(books) => books + case Left(err) => + err.fillInStackTrace().printStackTrace() + throw err.getCause + } + + private def retrieveBooks(): String = + use(booksResource()) { src => + src.mkString + } + + private def booksResource(): BufferedSource = { + val stream = getClass.getResourceAsStream("/book.json") + Source.fromInputStream(stream) + } } diff --git a/backend/src/main/scala/snorri/package.scala b/backend/src/main/scala/snorri/package.scala new file mode 100644 index 0000000..e0451c2 --- /dev/null +++ b/backend/src/main/scala/snorri/package.scala @@ -0,0 +1,7 @@ +package object snorri: + // A lame try-with-resources equivalent + def use[A <: AutoCloseable, B](resource: A)(code: A => B): B = + try + code(resource) + finally + resource.close() diff --git a/build.sbt b/build.sbt index 602c89f..0aab136 100644 --- a/build.sbt +++ b/build.sbt @@ -5,7 +5,10 @@ ThisBuild / tlBaseVersion := "0.1" ThisBuild / tlCiHeaderCheck := false val commonSettings = Seq( - libraryDependencies += "org.creativescala" %% "krop-core" % "0.4.1" + libraryDependencies ++= + "org.creativescala" %% "krop-core" % "0.6.0" :: + Modules.circe ::: + Nil ) lazy val snorriRoot = diff --git a/project/Modules.scala b/project/Modules.scala new file mode 100644 index 0000000..d87319e --- /dev/null +++ b/project/Modules.scala @@ -0,0 +1,10 @@ +import sbt.* + +object Modules { + val CirceVersion = "0.14.6" + + lazy val circe: List[ModuleID] = + "io.circe" %% "circe-parser" % CirceVersion :: + "io.circe" %% "circe-generic" % CirceVersion :: + Nil +}