minor improvements

This commit is contained in:
Rohan Sircar 2021-05-06 23:40:01 +05:30
parent c25f2e0810
commit 01f2676ca3
11 changed files with 115 additions and 146 deletions

View File

@ -65,7 +65,7 @@ jobs:
- name: Migrate - name: Migrate
run: csbt flyway/flywayMigrate run: csbt flyway/flywayMigrate
- name: Lint - name: Lint
run: csbt "scalafmtCheckAll;scalafixAll --check" run: csbt lint-check
- name: Compile - name: Compile
run: | run: |
csbt "compile; test:compile" csbt "compile; test:compile"

View File

@ -217,6 +217,9 @@ inThisBuild(
) )
) )
addCommandAlias("lint-check", "scalafmtCheckAll; scalafixAll --check")
addCommandAlias("lint-run", "scalafmtAll; scalafixAll")
wartremoverErrors in (Compile, compile) ++= wartremoverErrors in (Compile, compile) ++=
Warts.allBut( Warts.allBut(
Wart.Any, Wart.Any,

View File

@ -1,47 +0,0 @@
package wow.doge.http4sdemo
import cats.Applicative
import cats.effect.Sync
import cats.implicits._
import io.circe.Decoder
import io.circe.Encoder
import io.circe.generic.semiauto._
import monix.bio.Task
import org.http4s.Method._
import org.http4s._
import org.http4s.circe._
import org.http4s.client.Client
import org.http4s.client.dsl.Http4sClientDsl
import org.http4s.implicits._
sealed trait Jokes[F[_]] {
def get: F[Jokes.Joke]
}
object Jokes {
def apply[F[_]](implicit ev: Jokes[F]): Jokes[F] = ev
final case class Joke(joke: String)
object Joke {
implicit val jokeDecoder: Decoder[Joke] = deriveDecoder[Joke]
implicit def jokeEntityDecoder[F[_]: Sync]: EntityDecoder[F, Joke] =
jsonOf
implicit val jokeEncoder: Encoder[Joke] = deriveEncoder[Joke]
implicit def jokeEntityEncoder[F[_]: Applicative]: EntityEncoder[F, Joke] =
jsonEncoderOf
}
final case class JokeError(e: Throwable) extends RuntimeException
def impl(C: Client[Task]): Jokes[Task] = new Jokes[Task] {
val dsl = new Http4sClientDsl[Task] {}
import dsl._
def get: Task[Jokes.Joke] = {
C.expect[Joke](GET(uri"https://icanhazdadjoke.com/"))
.adaptError { case t =>
JokeError(t)
} // Prevent Client Json Decoding Failure Leaking
}
}
}

View File

@ -1,15 +1,38 @@
package wow.doge.http4sdemo package wow.doge.http4sdemo
import scala.concurrent.duration.MILLISECONDS
import cats.effect.ExitCode import cats.effect.ExitCode
import cats.effect.Resource import cats.effect.Resource
import io.odin.Level
import io.odin.consoleLogger
import io.odin.formatter.Formatter
import io.odin.syntax._
import monix.bio.BIOApp import monix.bio.BIOApp
import monix.bio.IO
import monix.bio.Task import monix.bio.Task
import monix.bio.UIO import monix.bio.UIO
import slick.jdbc.JdbcProfile import slick.jdbc.JdbcProfile
object Main extends BIOApp { object Main extends BIOApp {
val profile: JdbcProfile = slick.jdbc.PostgresProfile val profile: JdbcProfile = slick.jdbc.PostgresProfile
def app = for { val app = for {
startTime <- Resource.liftF(IO.clock.realTime(MILLISECONDS))
_ <- Resource.liftF(Task(println("""
| .__ __ __ _____ .___
| | |___/ |__/ |_______ / | | ______ __| _/____ _____ ____
| | | \ __\ __\____ \ / | |_/ ___/ ______ / __ |/ __ \ / \ / _ \
| | Y \ | | | | |_> > ^ /\___ \ /_____/ / /_/ \ ___/| Y Y ( <_> )
| |___| /__| |__| | __/\____ |/____ > \____ |\___ >__|_| /\____/
| \/ |__| |__| \/ \/ \/ \/
""".stripMargin)))
logger <- consoleLogger[Task](
formatter = Formatter.colorful,
minLevel = Level.Debug
).withAsync()
_ <- Resource.liftF(
logger.info(s"Starting ${BuildInfo.name}-${BuildInfo.version}")
)
db <- SlickResource("myapp.database") db <- SlickResource("myapp.database")
_ <- Resource.liftF(for { _ <- Resource.liftF(for {
config <- JdbcDatabaseConfig.loadFromGlobal("myapp.database") config <- JdbcDatabaseConfig.loadFromGlobal("myapp.database")
@ -17,9 +40,10 @@ object Main extends BIOApp {
} yield ()) } yield ())
_ <- Resource.liftF( _ <- Resource.liftF(
Task.deferAction(implicit s => Task.deferAction(implicit s =>
Http4sdemoServer.stream(db, profile).compile.drain new Server(db, profile, logger).stream.compile.drain
) )
) )
} yield () } yield ()
def run(args: List[String]) = { def run(args: List[String]) = {
app app

View File

@ -2,6 +2,7 @@ package wow.doge.http4sdemo
import cats.implicits._ import cats.implicits._
import fs2.Stream import fs2.Stream
import io.odin
import monix.bio.Task import monix.bio.Task
import monix.execution.Scheduler import monix.execution.Scheduler
import org.http4s.client.blaze.BlazeClientBuilder import org.http4s.client.blaze.BlazeClientBuilder
@ -10,36 +11,29 @@ import org.http4s.server.blaze.BlazeServerBuilder
import org.http4s.server.middleware.Logger import org.http4s.server.middleware.Logger
import slick.jdbc.JdbcBackend.DatabaseDef import slick.jdbc.JdbcBackend.DatabaseDef
import slick.jdbc.JdbcProfile import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.routes.LibraryRoutes
import wow.doge.http4sdemo.services.LibraryDbio import wow.doge.http4sdemo.services.LibraryDbio
import wow.doge.http4sdemo.services.LibraryServiceImpl import wow.doge.http4sdemo.services.LibraryServiceImpl
object Http4sdemoServer { final class Server(db: DatabaseDef, p: JdbcProfile, logger: odin.Logger[Task]) {
def stream( def stream(implicit s: Scheduler): Stream[Task, Nothing] = {
db: DatabaseDef, val logger = io.odin.consoleLogger[Task](formatter =
p: JdbcProfile io.odin.formatter.Formatter.colorful
)(implicit s: Scheduler): Stream[Task, Nothing] = { )
val log: String => Task[Unit] = str => logger.debug(str)
for { for {
client <- BlazeClientBuilder[Task](s).stream client <- BlazeClientBuilder[Task](s).stream
helloWorldAlg = HelloWorld.impl
jokeAlg = Jokes.impl(client)
// Combine Service Routes into an HttpApp.
// Can also be done via a Router if you
// want to extract a segments not checked
// in the underlying routes.
libraryDbio = new LibraryDbio(p) libraryDbio = new LibraryDbio(p)
libraryService = new LibraryServiceImpl(p, libraryDbio, db) libraryService = new LibraryServiceImpl(p, libraryDbio, db)
httpApp = ( httpApp = (
Http4sdemoRoutes.helloWorldRoutes[Task](helloWorldAlg) <+> new LibraryRoutes(libraryService, logger).routes
Http4sdemoRoutes.jokeRoutes[Task](jokeAlg) <+>
Http4sdemoRoutes.libraryRoutes(libraryService)
).orNotFound ).orNotFound
finalHttpApp = Logger.httpApp(
// With Middlewares in place true,
finalHttpApp = Logger.httpApp(true, true)(httpApp) true,
// _ = {finalHttpApp.run(Request.)} logAction = log.pure[Option]
)(httpApp)
exitCode <- BlazeServerBuilder[Task](s) exitCode <- BlazeServerBuilder[Task](s)
.bindHttp(8081, "0.0.0.0") .bindHttp(8081, "0.0.0.0")
.withHttpApp(finalHttpApp) .withHttpApp(finalHttpApp)

View File

@ -6,7 +6,5 @@ import slick.jdbc.JdbcBackend.Database
object SlickResource { object SlickResource {
def apply(confPath: String) = def apply(confPath: String) =
Resource.make(Task(Database.forConfig(confPath)))(db => Resource.make(Task(Database.forConfig(confPath)))(db => Task(db.close()))
Task(db.source.close()) >> Task(db.close())
)
} }

View File

@ -101,4 +101,5 @@ object BookSearchMode extends Enum[BookSearchMode] {
withNameEither(s).leftMap(e => ParseFailure(e.getMessage, e.getMessage)) withNameEither(s).leftMap(e => ParseFailure(e.getMessage, e.getMessage))
) )
object Matcher extends QueryParamDecoderMatcher[BookSearchMode]("mode") object Matcher extends QueryParamDecoderMatcher[BookSearchMode]("mode")
} }

View File

@ -2,6 +2,8 @@ package wow.doge.http4sdemo
import scala.util.Try import scala.util.Try
import io.odin.meta.Position
import io.odin.meta.Render
import monix.bio.IO import monix.bio.IO
import monix.bio.Task import monix.bio.Task
import monix.reactive.Observable import monix.reactive.Observable
@ -35,4 +37,18 @@ package object implicits {
monix.eval.Task.deferAction(implicit s => monix.eval.Task.from(task)) monix.eval.Task.deferAction(implicit s => monix.eval.Task.from(task))
} }
implicit final class OdinLoggerExt(private val logger: io.odin.Logger[Task])
extends AnyVal {
def debugU[M](msg: => M)(implicit render: Render[M], position: Position) =
logger.debug(msg).hideErrors
def infoU[M](msg: => M)(implicit render: Render[M], position: Position) =
logger.info(msg).hideErrors
def traceU[M](msg: => M)(implicit render: Render[M], position: Position) =
logger.trace(msg).hideErrors
def warnU[M](msg: => M)(implicit render: Render[M], position: Position) =
logger.warn(msg).hideErrors
def errorU[M](msg: => M)(implicit render: Render[M], position: Position) =
logger.error(msg).hideErrors
}
} }

View File

@ -1,53 +1,29 @@
package wow.doge.http4sdemo package wow.doge.http4sdemo.routes
import cats.effect.Sync
import cats.implicits._
import fs2.interop.reactivestreams._ import fs2.interop.reactivestreams._
import io.circe.Codec import io.circe.Codec
import io.circe.generic.semiauto._ import io.circe.generic.semiauto._
import io.odin.Logger
import monix.bio.IO import monix.bio.IO
import monix.bio.Task import monix.bio.Task
import monix.bio.UIO
import org.http4s.HttpRoutes import org.http4s.HttpRoutes
import org.http4s.dsl.Http4sDsl import org.http4s.dsl.Http4sDsl
import wow.doge.http4sdemo.dto.Book import wow.doge.http4sdemo.dto.Book
import wow.doge.http4sdemo.dto.BookSearchMode import wow.doge.http4sdemo.dto.BookSearchMode
import wow.doge.http4sdemo.dto.BookUpdate import wow.doge.http4sdemo.dto.BookUpdate
import wow.doge.http4sdemo.dto.NewBook import wow.doge.http4sdemo.dto.NewBook
import wow.doge.http4sdemo.implicits._
import wow.doge.http4sdemo.services.LibraryService import wow.doge.http4sdemo.services.LibraryService
object Http4sdemoRoutes { class LibraryRoutes(libraryService: LibraryService, logger: Logger[Task]) {
def jokeRoutes[F[_]: Sync](J: Jokes[F]): HttpRoutes[F] = { val routes: HttpRoutes[Task] = {
val dsl = Http4sDsl[F]
import dsl._
HttpRoutes.of[F] { case GET -> Root / "joke" =>
for {
joke <- J.get
resp <- Ok(joke)
} yield resp
}
}
def helloWorldRoutes[F[_]: Sync](H: HelloWorld[F]): HttpRoutes[F] = {
val dsl = new Http4sDsl[F] {}
import dsl._
HttpRoutes.of[F] { case GET -> Root / "hello" / name =>
for {
greeting <- H.hello(HelloWorld.Name(name))
resp <- Ok(greeting)
r2 <- BadRequest("Bad request")
} yield r2
}
}
def libraryRoutes(libraryService: LibraryService): HttpRoutes[Task] = {
val dsl = Http4sDsl[Task] val dsl = Http4sDsl[Task]
import dsl._ import dsl._
object Value extends QueryParamDecoderMatcher[String]("value") object Value extends QueryParamDecoderMatcher[String]("value")
HttpRoutes.of[Task] { HttpRoutes.of[Task] {
case GET -> Root / "api" / "get" / "book" :? case GET -> Root / "api" / "books" :?
BookSearchMode.Matcher(mode) +& Value(value) => BookSearchMode.Matcher(mode) +& Value(value) =>
import org.http4s.circe.streamJsonArrayEncoder import org.http4s.circe.streamJsonArrayEncoder
import io.circe.syntax._ import io.circe.syntax._
@ -63,7 +39,7 @@ object Http4sdemoRoutes {
} yield res } yield res
) )
case GET -> Root / "api" / "get" / "books" => case GET -> Root / "api" / "books" =>
import org.http4s.circe.streamJsonArrayEncoder import org.http4s.circe.streamJsonArrayEncoder
import io.circe.syntax._ import io.circe.syntax._
Task.deferAction(implicit s => Task.deferAction(implicit s =>
@ -73,67 +49,50 @@ object Http4sdemoRoutes {
.toStream[Task] .toStream[Task]
) )
res <- Ok(books.map(_.asJson)) res <- Ok(books.map(_.asJson))
// res <- Ok(streamJsonArrayEncoderOf[Task, Book].(books))
} yield res } yield res
) )
case GET -> Root / "blah" => Ok().hideErrors case GET -> Root / "api" / "books" / IntVar(id) =>
case GET -> Root / "api" / "get" / "book" / IntVar(id) =>
import org.http4s.circe.CirceEntityCodec._ import org.http4s.circe.CirceEntityCodec._
// import org.http4s.circe.jsonEncoder
// import io.circe.syntax._
for { for {
bookJson <- libraryService.getBookById(id) bookJson <- libraryService.getBookById(id)
res <- Ok(bookJson) res <- Ok(bookJson)
} yield res } yield res
case req @ POST -> Root / "api" / "post" / "book" => case req @ PUT -> Root / "api" / "books" =>
import org.http4s.circe.CirceEntityCodec._ import org.http4s.circe.CirceEntityCodec._
for { for {
newBook <- req.as[NewBook] newBook <- req.as[NewBook]
// .onErrorHandleWith {
// case ParseF
// }
res <- libraryService res <- libraryService
.insertBook(newBook) .insertBook(newBook)
.tapError(err => logger.errorU(err.toString))
.flatMap(book => Created(book).hideErrors) .flatMap(book => Created(book).hideErrors)
.mapErrorPartialWith { .onErrorHandleWith(_.toResponse)
case LibraryService.EntityDoesNotExist(message) =>
BadRequest(message).hideErrors
case LibraryService.EntityAlreadyExists(message) =>
BadRequest(message).hideErrors
// case LibraryService.MyError2(_) => Ok().hideErrors
// case C3 => Ok().hideErrors
}
} yield res } yield res
case req @ PATCH -> Root / "api" / "update" / "book" / IntVar(id) => case req @ PATCH -> Root / "api" / "books" / IntVar(id) =>
import org.http4s.circe.CirceEntityCodec._ import org.http4s.circe.CirceEntityCodec._
for { for {
updateData <- req.as[BookUpdate] updateData <- req.as[BookUpdate]
res <- libraryService res <- libraryService
.updateBook(id, updateData) .updateBook(id, updateData)
.flatMap(_ => Ok().hideErrors) .flatMap(_ => NoContent().hideErrors)
.tapError(err => UIO(println(s"Handled -> ${err.toString}"))) .tapError(err => logger.errorU(err.toString))
.mapErrorPartialWith { .onErrorHandleWith(_.toResponse)
case e @ LibraryService.EntityDoesNotExist(message) =>
BadRequest(e: LibraryService.Error).hideErrors
// case LibraryService.MyError2(_) => Ok().hideErrors
// case C3 => Ok().hideErrors
}
} yield res } yield res
case req @ DELETE -> Root / "api" / "delete" / "book" / IntVar(id) => case req @ DELETE -> Root / "api" / "books" / IntVar(id) =>
for { for {
_ <- libraryService.deleteBook(id) _ <- libraryService.deleteBook(id)
res <- Ok() res <- Ok()
} yield res } yield res
case req @ POST -> Root / "api" / "post" / "books" / "read" => //TODO: use convenience method for decoding json stream
case req @ POST -> Root / "api" / "books" =>
import org.http4s.circe.CirceEntityCodec.circeEntityDecoder import org.http4s.circe.CirceEntityCodec.circeEntityDecoder
for { for {
newBook <- req.as[List[Book]] newBooks <- req.as[List[Book]]
// obs = Observable.fromIterable(newBooks)
// book <- libraryService.insertBook(newBook) // book <- libraryService.insertBook(newBook)
res <- Ok("blah") res <- Ok("blah")
} yield res } yield res

View File

@ -5,6 +5,7 @@ import monix.bio.IO
import monix.bio.Task import monix.bio.Task
import monix.bio.UIO import monix.bio.UIO
import monix.reactive.Observable import monix.reactive.Observable
import org.http4s.dsl.Http4sDsl
import slick.jdbc.JdbcBackend import slick.jdbc.JdbcBackend
import slick.jdbc.JdbcProfile import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.dto.Author import wow.doge.http4sdemo.dto.Author
@ -22,14 +23,31 @@ object LibraryService {
sealed trait Error extends Exception { sealed trait Error extends Exception {
def message: String def message: String
override def getMessage(): String = message override def getMessage(): String = message
def toResponse = {
val dsl = Http4sDsl[Task]
import org.http4s.circe.CirceEntityCodec._
import dsl._
implicit val codec = Error.codec
this match {
case e @ LibraryService.EntityDoesNotExist(message) =>
NotFound(e: LibraryService.Error).hideErrors
case e @ LibraryService.EntityAlreadyExists(message) =>
BadRequest(e: LibraryService.Error).hideErrors
}
}
} }
final case class EntityDoesNotExist(message: String) extends Error final case class EntityDoesNotExist(message: String) extends Error
final case class EntityAlreadyExists(message: String) extends Error final case class EntityAlreadyExists(message: String) extends Error
// final case class MessageBodyError(cause: MessageBodyFailure) extends Error
// final case class MyError2(message: String) extends Error // final case class MyError2(message: String) extends Error
// case object C3 extends Error { val message: String = "C3" } // case object C3 extends Error { val message: String = "C3" }
object Error { object Error {
implicit val codec = deriveCodec[Error] implicit val codec = deriveCodec[Error]
// def convert(e: MessageBodyFailure) = e match {
// case InvalidMessageBodyFailure(details, cause) => ()
// case MalformedMessageBodyFailure(details, cause) => ()
// }
} }
} }

View File

@ -7,12 +7,14 @@ import monix.bio.UIO
import monix.reactive.Observable import monix.reactive.Observable
import org.http4s.Method import org.http4s.Method
import org.http4s.Request import org.http4s.Request
import org.http4s.Status
import org.http4s.Uri import org.http4s.Uri
import org.http4s.implicits._ import org.http4s.implicits._
import wow.doge.http4sdemo.MonixBioSuite import wow.doge.http4sdemo.MonixBioSuite
import wow.doge.http4sdemo.dto.Book import wow.doge.http4sdemo.dto.Book
import wow.doge.http4sdemo.dto.BookSearchMode import wow.doge.http4sdemo.dto.BookSearchMode
import wow.doge.http4sdemo.dto.BookUpdate import wow.doge.http4sdemo.dto.BookUpdate
import wow.doge.http4sdemo.routes.LibraryRoutes
import wow.doge.http4sdemo.services.LibraryService import wow.doge.http4sdemo.services.LibraryService
import wow.doge.http4sdemo.services.NoopLibraryService import wow.doge.http4sdemo.services.NoopLibraryService
@ -34,12 +36,12 @@ class LibraryControllerSpec extends MonixBioSuite {
} }
for { for {
_ <- UIO.unit _ <- UIO.unit
routes = Http4sdemoRoutes.libraryRoutes(service) routes = new LibraryRoutes(service, noopLogger).routes
res <- routes res <- routes
.run(Request[Task](Method.GET, uri"/api/get/books")) .run(Request[Task](Method.GET, uri"/api/books"))
.value .value
.hideErrors .hideErrors
body <- res.map(_.as[List[Book]]).sequence body <- res.traverse(_.as[List[Book]])
_ <- UIO(assertEquals(body, Some(List(book)))) _ <- UIO(assertEquals(body, Some(List(book))))
// _ <- logger2.debug(body.toString).hideErrors // _ <- logger2.debug(body.toString).hideErrors
} yield () } yield ()
@ -57,15 +59,16 @@ class LibraryControllerSpec extends MonixBioSuite {
for { for {
_ <- UIO.unit _ <- UIO.unit
reqBody = BookUpdate(Some("blah"), None) reqBody = BookUpdate(Some("blah"), None)
routes = Http4sdemoRoutes.libraryRoutes(service) routes = new LibraryRoutes(service, noopLogger).routes
res <- routes res <- routes
.run( .run(
Request[Task](Method.PATCH, Root / "api" / "update" / "book" / "1") Request[Task](Method.PATCH, Root / "api" / "books" / "1")
.withEntity(reqBody) .withEntity(reqBody)
) )
.value .value
.hideErrors .hideErrors
body <- res.map(_.as[LibraryService.Error]).sequence _ <- UIO(assertEquals(res.map(_.status), Some(Status.NotFound)))
body <- res.traverse(_.as[LibraryService.Error])
_ <- UIO( _ <- UIO(
assertEquals( assertEquals(
body, body,
@ -98,10 +101,10 @@ class LibraryControllerSpec extends MonixBioSuite {
// logger2 = logger.withConstContext( // logger2 = logger.withConstContext(
// Map("Test" -> "get books by author name") // Map("Test" -> "get books by author name")
// ) // )
routes = Http4sdemoRoutes.libraryRoutes(service) routes = new LibraryRoutes(service, noopLogger).routes
request = Request[Task]( request = Request[Task](
Method.GET, Method.GET,
Root / "api" / "get" / "book" Root / "api" / "books"
withQueryParams Map( withQueryParams Map(
"mode" -> BookSearchMode.AuthorName.entryName, "mode" -> BookSearchMode.AuthorName.entryName,
"value" -> "blah" "value" -> "blah"
@ -109,7 +112,7 @@ class LibraryControllerSpec extends MonixBioSuite {
) )
// _ <- logger2.info(s"Request -> $request") // _ <- logger2.info(s"Request -> $request")
res <- routes.run(request).value.hideErrors res <- routes.run(request).value.hideErrors
body <- res.map(_.as[List[Book]]).sequence body <- res.traverse(_.as[List[Book]])
_ <- UIO.pure(body).assertEquals(Some(books)) _ <- UIO.pure(body).assertEquals(Some(books))
// _ <- logger2.debug(s"Response body -> $body").hideErrors // _ <- logger2.debug(s"Response body -> $body").hideErrors
} yield () } yield ()
@ -134,10 +137,10 @@ class LibraryControllerSpec extends MonixBioSuite {
// logger2 = logger.withConstContext( // logger2 = logger.withConstContext(
// Map("Test" -> "get books by book title") // Map("Test" -> "get books by book title")
// ) // )
routes = Http4sdemoRoutes.libraryRoutes(service) routes = new LibraryRoutes(service, noopLogger).routes
request = Request[Task]( request = Request[Task](
Method.GET, Method.GET,
Root / "api" / "get" / "book" Root / "api" / "books"
withQueryParams Map( withQueryParams Map(
"mode" -> BookSearchMode.BookTitle.entryName, "mode" -> BookSearchMode.BookTitle.entryName,
"value" -> "blah" "value" -> "blah"
@ -145,7 +148,7 @@ class LibraryControllerSpec extends MonixBioSuite {
) )
// _ <- logger2.info(s"Request -> $request") // _ <- logger2.info(s"Request -> $request")
res <- routes.run(request).value.hideErrors res <- routes.run(request).value.hideErrors
body <- res.map(_.as[List[Book]]).sequence body <- res.traverse(_.as[List[Book]])
_ <- UIO.pure(body).assertEquals(Some(books)) _ <- UIO.pure(body).assertEquals(Some(books))
// _ <- logger2.debug(s"Response body -> $body").hideErrors // _ <- logger2.debug(s"Response body -> $body").hideErrors
} yield () } yield ()