From 3e1635d494b275dbbcc32c9b2352cff1ad9e6575 Mon Sep 17 00:00:00 2001 From: Guntis Smaukstelis Date: Thu, 22 Jun 2023 13:59:44 +0300 Subject: [PATCH] Refactor save return type without Either --- README.md | 38 +++++++++++++++++++ src/main/scala/app/Main.scala | 4 ++ src/main/scala/db/DBService.scala | 12 +++--- src/main/scala/db/DataService.scala | 17 +++++---- src/main/scala/fetch/FileFetchScheduler.scala | 5 +-- src/main/scala/server/Server.scala | 2 +- src/test/scala/db/DBServiceSpec.scala | 23 +++++++++++ .../scala/fetch/FileFetchSchedulerSpec.scala | 4 +- 8 files changed, 84 insertions(+), 21 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..7dd9403 --- /dev/null +++ b/README.md @@ -0,0 +1,38 @@ +# Weather Tool + +Tool for downloading historical and real-time data from 33 weather stations in Latvia. It aggregates and displays data for different time periods, cities, and weather parameters. + +## Run +``` +// scala +sbt run + +// web +cd web/ +npm run dev +``` + +**Author:** Guntis Smaukstelis +**Mentor:** Jānis Komuls + +**Status:** Live +**URL:** [https://weather-tool.fly.dev/](https://weather-tool.fly.dev/) +**Used By:** State television weather news +**Repository:** [https://github.com/guntisdev/WeatherTool](https://github.com/guntisdev/WeatherTool) + +## Backend Technologies: +- cats-effects +- http4s +- pureconfig +- fs2 +- circe +- scalatest + +## Frontend Technologies: +- SolidJS +- Vite + +## Deployment: +- Docker +- Shell script +- Fly.io diff --git a/src/main/scala/app/Main.scala b/src/main/scala/app/Main.scala index aa8253f..85a6aca 100644 --- a/src/main/scala/app/Main.scala +++ b/src/main/scala/app/Main.scala @@ -9,13 +9,17 @@ import server.Server object Main extends IOApp { def run(args: List[String]): IO[ExitCode] = { for { + // TODO run in separate thread dbService <- DBService.of dataService <- DataService.of(dbService) + fetch <- FetchService.of fileFetchScheduler <- FileFetchScheduler.of(dataService, fetch) schedulerTask = fileFetchScheduler.run.compile.drain + server <- Server.of(dataService, fetch) serverTask = server.run + exitCode <- (serverTask, schedulerTask).parMapN((_, _) => ExitCode.Success) } yield exitCode } diff --git a/src/main/scala/db/DBService.scala b/src/main/scala/db/DBService.scala index 7565fa2..a94fcb8 100644 --- a/src/main/scala/db/DBService.scala +++ b/src/main/scala/db/DBService.scala @@ -40,14 +40,14 @@ class DBService(log: Logger[IO]) extends DataServiceTrait { } } - def save(fileName: String, content: String): IO[Either[Throwable, String]] = { + def save(fileName: String, content: String): IO[String] = { val path = Paths.get(s"$dataPath/$fileName") IO(Files.writeString(path, content)) - .redeemWith( - error => IO(Left(error)) // <* log.error(s"Write file '$fileName' failed with error: ${error.getMessage}") - , - _ => IO(Right(fileName)) // <* log.info(s"write: $fileName") - ) + .attempt + .flatMap { + case Left(error) => log.error(s"Write file '$fileName' failed with error: ${error.getMessage}") *> IO.raiseError(error) + case Right(_) => log.info(s"write: $fileName").as(fileName) + } } def readFile(fileName: String): IO[List[String]] = { diff --git a/src/main/scala/db/DataService.scala b/src/main/scala/db/DataService.scala index f97c467..1d17dda 100644 --- a/src/main/scala/db/DataService.scala +++ b/src/main/scala/db/DataService.scala @@ -9,7 +9,7 @@ import org.typelevel.log4cats.slf4j.Slf4jLogger import java.time.{Instant, LocalDate, LocalDateTime, ZoneId} trait DataServiceTrait { - def save(fileName: String, content: String): IO[Either[Throwable, String]] + def save(fileName: String, content: String): IO[String] def readFile(fileName: String): IO[List[String]] def getInRange(from: LocalDateTime, to: LocalDateTime): IO[List[String]] def getDates: IO[List[LocalDate]] @@ -46,14 +46,15 @@ class DataService private( } } - def save(fileName: String, content: String): IO[Either[Throwable, String]] = { - dbService.save(fileName, content).flatMap { - case Right(savedFileName) => + def save(fileName: String, content: String): IO[String] = { + dbService.save(fileName, content).redeemWith( + error => IO.raiseError(error), + savedFileName => { state.update(st => st.updated(savedFileName, content.split("\n").toList)) *> - filterState *> - logState.as(Right(savedFileName)) - case e@Left(_) => IO.pure(e) - } + filterState *> + logState.as(savedFileName) + } + ).onError(error => log.info(s"errr... $error")) } def readFile(fileName: String): IO[List[String]] = dbService.readFile(fileName) diff --git a/src/main/scala/fetch/FileFetchScheduler.scala b/src/main/scala/fetch/FileFetchScheduler.scala index e4af8f8..be7e638 100644 --- a/src/main/scala/fetch/FileFetchScheduler.scala +++ b/src/main/scala/fetch/FileFetchScheduler.scala @@ -27,10 +27,7 @@ class FileFetchScheduler(dataService: DataServiceTrait, fetch: FetchService, fil case Right((name, content)) => dataService.save(name, content).attempt.flatMap { case Left(err) => log.error(s"error: $err") - case Right(saveResult) => saveResult match { - case Left(err) => log.error(s"error: $err") - case Right(savedName) => log.info(s"saved: $savedName") - } + case Right(savedName) => log.info(s"saved: $savedName") } } } diff --git a/src/main/scala/server/Server.scala b/src/main/scala/server/Server.scala index ab6b9db..2174176 100644 --- a/src/main/scala/server/Server.scala +++ b/src/main/scala/server/Server.scala @@ -59,7 +59,7 @@ class Server(dataService: DataService, fetch: FetchService, log: Logger[IO]) { fetchServiceError = fetchResultEither.left.toOption.map(e => s"FetchServiceError: ${e.getMessage}").toList fetchResult = fetchResultEither.getOrElse(List.empty) (fetchErrors, successDownloads) = fetchResult.partitionMap(identity) - saveResults <- successDownloads.traverse { case (name, content) => dataService.save(name, content) } + saveResults <- successDownloads.traverse { case (name, content) => dataService.save(name, content).attempt } (saveErrors, successSaves) = saveResults.partitionMap(identity) // successes = successDownloads.map(s => s"fetched: ${s._1}") ++ successSaves.map(s => s"saved: $s") successes = successSaves diff --git a/src/test/scala/db/DBServiceSpec.scala b/src/test/scala/db/DBServiceSpec.scala index b798cb6..16f8eaa 100644 --- a/src/test/scala/db/DBServiceSpec.scala +++ b/src/test/scala/db/DBServiceSpec.scala @@ -4,6 +4,7 @@ import cats.effect.unsafe.implicits.global import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers +import java.io.IOException import java.time.format.DateTimeFormatter import java.time.{LocalDate, LocalDateTime} @@ -32,4 +33,26 @@ class DBServiceSpec extends AnyFunSuite with Matchers { lines.length shouldBe 2142 } + + test("DBService.save should return correct result") { + val dbService = DBService.of.unsafeRunSync() + val fileName = "testFile.txt" + val fileContent = "test content..." + + dbService.save(fileName, fileContent).unsafeRunSync() shouldEqual fileName + } + + test("DBService.save returns error on invalid file name") { + val dbService = DBService.of.unsafeRunSync() + val fileName = "/invalid/file/name" + val fileContent = "test content..." + + val result = dbService.save(fileName, fileContent).attempt.unsafeRunSync() + + result match { + case Left(e) => + assert(e.isInstanceOf[IOException]) + case Right(_) => fail("Expected failure did not occur") + } + } } \ No newline at end of file diff --git a/src/test/scala/fetch/FileFetchSchedulerSpec.scala b/src/test/scala/fetch/FileFetchSchedulerSpec.scala index 7c11224..27d0dab 100644 --- a/src/test/scala/fetch/FileFetchSchedulerSpec.scala +++ b/src/test/scala/fetch/FileFetchSchedulerSpec.scala @@ -20,8 +20,8 @@ class FileFetchSchedulerSpec extends AsyncWordSpec with Matchers with IOSuite { refScheduler <- Ref.of[IO, Option[Either[Throwable, (String, String)]]](None) log <- Slf4jLogger.create[IO] dbService = new DBService(log) { - override def save(fileName: String, content: String): IO[Either[Throwable, String]] = { - refDb.set(Some(fileName)).as(Right(fileName)) + override def save(fileName: String, content: String): IO[String] = { + refDb.set(Some(fileName)).as(fileName) } } fileNameService = new FileNameService {