Refactor save return type without Either
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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]] = {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user