You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
8.6 KiB

package wow.doge.http4sdemo.services
import io.circe.generic.semiauto._
import monix.bio.IO
import monix.bio.Task
import monix.bio.UIO
import monix.reactive.Observable
import org.http4s.dsl.Http4sDsl
import slick.jdbc.JdbcBackend
import slick.jdbc.JdbcProfile
import wow.doge.http4sdemo.dto.Author
import wow.doge.http4sdemo.dto.Book
import wow.doge.http4sdemo.dto.BookSearchMode
import wow.doge.http4sdemo.dto.BookSearchMode.AuthorName
import wow.doge.http4sdemo.dto.BookSearchMode.BookTitle
import wow.doge.http4sdemo.dto.BookUpdate
import wow.doge.http4sdemo.dto.NewAuthor
import wow.doge.http4sdemo.dto.NewBook
import wow.doge.http4sdemo.implicits._
import wow.doge.http4sdemo.slickcodegen.Tables
object LibraryService {
sealed trait Error extends Exception {
def message: String
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 EntityAlreadyExists(message: String) extends Error
// final case class MessageBodyError(cause: MessageBodyFailure) extends Error
// final case class MyError2(message: String) extends Error
// case object C3 extends Error { val message: String = "C3" }
object Error {
implicit val codec = deriveCodec[Error]
// def convert(e: MessageBodyFailure) = e match {
// case InvalidMessageBodyFailure(details, cause) => ()
// case MalformedMessageBodyFailure(details, cause) => ()
// }
}
}
trait LibraryService {
import LibraryService._
def getBooks: Observable[Book]
def getBookById(id: Int): Task[Option[Book]]
def searchBook(mode: BookSearchMode, value: String): Observable[Book]
def updateBook(id: Int, updateData: BookUpdate): IO[Error, Unit]
def deleteBook(id: Int): Task[Int]
def insertBook(newBook: NewBook): IO[Error, Book]
def insertAuthor(a: NewAuthor): Task[Int]
def booksForAuthor(authorId: Int): Observable[Book]
}
class LibraryServiceImpl(
profile: JdbcProfile,
dbio: LibraryDbio,
db: JdbcBackend.DatabaseDef
) extends LibraryService {
import profile.api._
import LibraryService._
def getBooks = db.streamO(dbio.getBooks.transactionally)
def getBookById(id: Int) = db.runL(dbio.getBookById(id))
def searchBook(mode: BookSearchMode, value: String): Observable[Book] =
mode match {
case BookTitle =>
db.streamO(dbio.getBooksByTitle(value))
case AuthorName =>
Observable
.fromTask((for {
author <- db.runL(dbio.getAuthorByName(value)).flatMap {
case None =>
IO.raiseError(
new EntityDoesNotExist(
s"Author with name=$value does not exist"
)
)
case Some(value) => IO.pure(value)
}
books = db
.streamO(dbio.getBooksForAuthor(author.authorId))
.map(Book.fromBooksRow)
} yield books).toTask)
.flatten
}
def insertAuthor(a: NewAuthor): Task[Int] = db.runL(dbio.insertAuthor(a))
def updateBook(id: Int, updateData: BookUpdate): IO[Error, Unit] =
for {
action <- UIO.deferAction(implicit s =>
UIO(for {
mbRow <- Tables.Books.filter(_.bookId === id).result.headOption
updatedRow <- mbRow match {
case Some(value) =>
println(s"Original value -> $value")
println(s"Value to be updated with -> $updateData")
DBIO.successful(updateData.update(value))
case None =>
DBIO.failed(
EntityDoesNotExist(s"Book with id=$id does not exist")
)
}
updateAction = Tables.Books.filter(_.bookId === id).update(updatedRow)
_ = println(s"SQL = ${updateAction.statements}")
_ <- updateAction
} yield ())
)
_ <- db
.runTryL(action.transactionally.asTry)
.mapErrorPartial { case e: Error =>
e
}
} yield ()
def deleteBook(id: Int) = db.runL(dbio.deleteBook(id))
def insertBook(newBook: NewBook): IO[Error, Book] =
IO.deferAction { implicit s =>
for {
action <- UIO(for {
_ <- Tables.Books
.filter(_.isbn === newBook.isbn)
.map(Book.fromBooksTableFn)
.result
.headOption
.flatMap {
case None => DBIO.successful(())
case Some(_) =>
DBIO.failed(
EntityAlreadyExists(
s"Book with isbn=${newBook.isbn} already exists"
)
)
}
_ <- dbio.getAuthor(newBook.authorId).flatMap {
case None =>
DBIO.failed(
EntityDoesNotExist(
s"Author with id=${newBook.authorId} does not exist"
)
)
case Some(_) => DBIO.successful(())
}
book <- dbio.insertBookAndGetBook(newBook)
} yield book)
book <- db
.runTryL(action.transactionally.asTry)
.mapErrorPartial { case e: Error =>
e
}
} yield book
}
def booksForAuthor(authorId: Int) =
db.streamO(dbio.getBooksForAuthor(authorId).transactionally)
.map(Book.fromBooksRow)
}
class LibraryDbio(val profile: JdbcProfile) {
import profile.api._
/* */
def getBooks: StreamingDBIO[Seq[Book], Book] = Query.getBooksInner.result
def insertBookAndGetId(newBook: NewBook): DBIO[Int] =
Query.insertBookGetId += newBook
def insertBookAndGetBook(newBook: NewBook): DBIO[Book] =
Query.insertBookGetBook += newBook
def insertAuthor(newAuthor: NewAuthor): DBIO[Int] =
Query.insertAuthorGetId += newAuthor
def getAuthor(id: Int): DBIO[Option[Author]] =
Query.selectAuthor(id).map(Author.fromAuthorsTableFn).result.headOption
def getAuthorByName(name: String): DBIO[Option[Author]] =
Tables.Authors
.filter(_.authorName === name)
.map(Author.fromAuthorsTableFn)
.result
.headOption
def deleteBook(id: Int): DBIO[Int] = Query.selectBookById(id).delete
def getBookById(id: Int): DBIO[Option[Book]] = Query
.selectBookById(id)
.map(Book.fromBooksTableFn)
.result
.headOption
def getBooksByTitle(title: String): StreamingDBIO[Seq[Book], Book] =
Tables.Books.filter(_.bookTitle === title).map(Book.fromBooksTableFn).result
def getBooksForAuthor(
authorId: Int
): StreamingDBIO[Seq[Tables.BooksRow], Tables.BooksRow] =
Query.booksForAuthorInner(authorId).result
private object Query {
val getBooksInner = Book.fromBooksTable
val insertBookGetId =
NewBook.fromBooksTable.returning(Tables.Books.map(_.bookId))
val insertBookGetBook = NewBook.fromBooksTable.returning(getBooksInner)
val insertAuthorGetId =
Tables.Authors
.map(a => (a.authorName).mapTo[NewAuthor])
.returning(Tables.Authors.map(_.authorId))
// val insertAuthor = NewAuthor.fromAuthorsTable
def booksForAuthorInner(authorId: Int) = for {
b <- Tables.Books
a <- Tables.Authors
if b.authorId === a.authorId && b.authorId === authorId
} yield b
def selectAuthor(authorId: Int) =
Tables.Authors.filter(_.authorId === authorId)
def selectBookById(id: Int) = Tables.Books.filter(_.bookId === id)
def selectBookByIsbn(isbn: String) = Tables.Books.filter(_.isbn === isbn)
}
}
trait NoopLibraryService extends LibraryService {
def getBooks: Observable[Book] =
Observable.raiseError(new NotImplementedError)
def getBookById(id: Int): Task[Option[Book]] =
IO.terminate(new NotImplementedError)
def searchBook(
mode: BookSearchMode,
value: String
): Observable[Book] = Observable.raiseError(new NotImplementedError)
def updateBook(
id: Int,
updateData: BookUpdate
): IO[LibraryService.Error, Unit] = IO.terminate(new NotImplementedError)
def deleteBook(id: Int): Task[Int] = IO.terminate(new NotImplementedError)
def insertBook(newBook: NewBook): IO[LibraryService.Error, Book] =
IO.terminate(new NotImplementedError)
def insertAuthor(a: NewAuthor): Task[Int] =
IO.terminate(new NotImplementedError)
def booksForAuthor(authorId: Int): Observable[Book] =
Observable.raiseError(new NotImplementedError)
}