From 3cd223ddddc85089ffc946333a59b035e591ab6b Mon Sep 17 00:00:00 2001 From: Guntis Smaukstelis Date: Tue, 25 Apr 2023 20:11:49 +0300 Subject: [PATCH] Validate routes, pretty json, meteo keys --- src/main/scala/server/Server.scala | 122 +++++++-------------- src/main/scala/server/ValidateRoutes.scala | 43 ++++++++ 2 files changed, 83 insertions(+), 82 deletions(-) create mode 100644 src/main/scala/server/ValidateRoutes.scala diff --git a/src/main/scala/server/Server.scala b/src/main/scala/server/Server.scala index a785f54..8f21e2b 100644 --- a/src/main/scala/server/Server.scala +++ b/src/main/scala/server/Server.scala @@ -4,109 +4,67 @@ import cats.effect._ import cats.implicits.toTraverseOps import db.DBService import fetch.FetchService +import parse.{MeteoData, Parser} +import server.ValidateRoutes.{Aggregate, CityList, DateTimeRange, ValidDate} import io.circe.{Json, Printer} import org.http4s._ import org.http4s.dsl.io._ -import org.http4s.implicits._ import org.http4s.server.Router import org.http4s.server.blaze.BlazeServerBuilder import io.circe.syntax._ -import io.circe.generic.auto._ -import org.http4s.circe.jsonEncoder -import parse.{Meteo, Parser} -import java.time.{LocalDate, LocalDateTime} -import java.time.format.DateTimeFormatter -import scala.util.Try object Server extends IOApp { - private val formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmm") + + // Define the extension method `pretty` for Json + implicit class JsonPrettyPrinter(json: Json) { + def pretty: String = { + val printer = Printer.spaces2.copy(dropNullValues = true) + printer.print(json) + } + } + private val appRoutes = HttpRoutes.of[IO] { + // http://localhost:3000/query/20230414_2200-20230501_1230/Liepāja,Rēzekne/tempAvg - case GET -> Root / "query" / timestampRange / cities / aggregate => - // TODO proly better to Validated with chained errors - val parsedArguments = for { - (from, to) <- timestampRange.split("-").toList - .map(str => Try(LocalDateTime.parse(str, formatter)).toOption) match { - case List(Some(from), Some(to)) => Some(from, to) - case _ => None - } - cityList <- cities.split(",").toList match { - case list => Some(list) - case Nil => None - } - aggregate <- Meteo.stringToAggregateParam(aggregate) - - } - yield (from, to, cityList, aggregate) - - parsedArguments match { - case Some((from, to, cityList, aggregate)) => { - val res = for { - lines <- db.DBService.getInRange(from, to) - parsedData <- IO.pure(parse.Parser.queryData(lines, cityList, aggregate)) - } yield parsedData - - Ok(res.map(_.toString())) - } - case _ => BadRequest(s"Invalid request format") - } + case GET -> Root / "query" / DateTimeRange(from, to) / CityList(cities) / Aggregate(aggregate) => + DBService.getInRange(from, to) + .map(Parser.queryData(_, cities, aggregate)) + .flatMap(result => Ok(result.asJson.pretty)) // http://localhost:3000/fetch/date/20230423 - case GET -> Root / "fetch" / "date" / dateStr => { - val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd") - val maybeDate = Try(LocalDate.parse(dateStr, dateFormatter)).toEither - maybeDate match { - case Right(date) => { - val result = for { - fetched <- FetchService.fetchFromDate(date) - (fetchErrors, successDownloads) = fetched.partitionMap(identity) - saved <- successDownloads.traverse { case (name, content) => DBService.save(name, content) } - (saveErrors, successSaves) = saved.partitionMap(identity) - successes = successDownloads.map(s => s"fetched: ${s._1}") ++ successSaves.map(s => s"saved: $s") - errors = fetchErrors.map(e => s"FetchError: ${e.getMessage}") ++ saveErrors.map(e => s"SaveError: ${e.getMessage}") - } yield (successes, errors) + case GET -> Root / "fetch" / "date" / ValidDate(date) => + val result = for { + fetched <- FetchService.fetchFromDate(date) + (fetchErrors, successDownloads) = fetched.partitionMap(identity) + saved <- successDownloads.traverse { case (name, content) => DBService.save(name, content) } + (saveErrors, successSaves) = saved.partitionMap(identity) + successes = successDownloads.map(s => s"fetched: ${s._1}") ++ successSaves.map(s => s"saved: $s") + errors = fetchErrors.map(e => s"FetchError: ${e.getMessage}") ++ saveErrors.map(e => s"SaveError: ${e.getMessage}") + } yield (successes, errors) - result.flatMap { case (successes, errors) => - val responseBody = Json.obj( - "errors" -> errors.asJson, - "successes" -> successes.asJson - ) - val printer = Printer.spaces2.copy(dropNullValues = true) - val prettyJson = printer.print(responseBody) - Ok(prettyJson) - } - } - case Left(_) => BadRequest("Invalid request format") + result.flatMap { case (successes, errors) => + Ok(Json.obj( + "errors" -> errors.asJson, + "successes" -> successes.asJson + ).pretty) } - } // http://localhost:3000/show/all_dates case GET -> Root / "show" / "all_dates" => - DBService.getDates().flatMap(dates => { - val printer = Printer.spaces2.copy(dropNullValues = true) - val prettyJson = printer.print(dates.asJson) - Ok(prettyJson) - }) + DBService.getDates().flatMap(dates => + Ok(dates.asJson.pretty) + ) // http://localhost:3000/show/date/20230423 - case GET -> Root / "show" / "date" / dateStr => - val dateFormatter = DateTimeFormatter.ofPattern("yyyyMMdd") - val maybeDate = Try(LocalDate.parse(dateStr, dateFormatter)).toEither - maybeDate match { - case Right(date) => { - DBService.getDateFileNames(date).flatMap(fileNames => { - val printer = Printer.spaces2.copy(dropNullValues = true) - val prettyJson = printer.print(fileNames.asJson) - Ok(prettyJson) - }) - } - case Left(_) => BadRequest("Invalid request format") - } - - case GET -> Root / "show" / dateRange => - ??? + case GET -> Root / "show" / "date" / ValidDate(date) => + DBService.getDateFileNames(date).flatMap(fileNames => + Ok(fileNames.asJson.pretty) + ) + // http://localhost:3000/help + case GET -> Root / "help" => + Ok(MeteoData.getKeys.asJson.pretty) } private val httpApp = Router("/" -> appRoutes).orNotFound diff --git a/src/main/scala/server/ValidateRoutes.scala b/src/main/scala/server/ValidateRoutes.scala new file mode 100644 index 0000000..9e6787e --- /dev/null +++ b/src/main/scala/server/ValidateRoutes.scala @@ -0,0 +1,43 @@ +package server + +import parse.{AggregateMeteo, Meteo} + +import java.time.{LocalDate, LocalDateTime} +import java.time.format.DateTimeFormatter +import scala.util.Try + +object ValidateRoutes { + object DateTimeRange { + def unapply(str: String): Option[(LocalDateTime, LocalDateTime)] = { + val formatter = DateTimeFormatter.ofPattern("yyyyMMdd_HHmm") + str.split("-") + .toList + .map(str => Try(LocalDateTime.parse(str, formatter)).toOption) match { + case List(Some(from), Some(to)) => Some(from, to) + case _ => None + } + } + } + + object ValidDate { + def unapply(str: String): Option[LocalDate] = { + val formatter = DateTimeFormatter.ofPattern("yyyyMMdd") + Try(LocalDate.parse(str, formatter)).toOption + } + } + + object CityList { + def unapply(str: String): Option[List[String]] = { + str.split(",").toList match { + case list => Some(list) + case Nil => None + } + } + } + + object Aggregate { + def unapply(str: String): Option[AggregateMeteo] = { + Meteo.stringToAggregateParam(str) + } + } +}