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
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)
|
|
|
|
}
|