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.

261 lines
7.5 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 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
}
final case class EntityDoesNotExist(message: String) extends Error
final case class EntityAlreadyExists(message: String) 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]
}
}
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.getBook(id))
def searchBook(mode: BookSearchMode, value: String): Observable[Book] =
mode match {
case BookTitle =>
db.streamO(dbio.getBooksByTitle(value))
case AuthorName =>
Observable
.fromTask((for {
_ <- IO.unit
id <- IO(value.toInt)
author <- db.runL(dbio.getAuthor(id)).flatMap {
case None =>
IO.raiseError(
new EntityDoesNotExist(s"Author with id=$id does not exist")
)
case Some(value) => IO.pure(value)
}
books = db
.streamO(dbio.getBooksForAuthor(id))
.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 <- dbio.selectBook(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 = dbio.selectBook(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 {
_ <- dbio
.selectBookByIsbn(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 selectBook(id: Int) = Tables.Books.filter(_.bookId === id)
def getAuthor(id: Int) =
Query.selectAuthor(id).map(Author.fromAuthorsTableFn).result.headOption
def deleteBook(id: Int) = selectBook(id).delete
def getBook(id: Int) = selectBook(id)
.map(Book.fromBooksTableFn)
.result
.headOption
def selectBookByIsbn(isbn: String) = Tables.Books.filter(_.isbn === isbn)
def getBooksByTitle(title: String) =
Tables.Books.filter(_.bookTitle === title).map(Book.fromBooksTableFn).result
def getBooksForAuthor(authorId: Int) =
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)
}
}
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)
}