From e87b0e1f8751a12b360300a9372b97643b67c810 Mon Sep 17 00:00:00 2001 From: Andrey Stolyarov Date: Thu, 20 Nov 2025 20:35:48 +0300 Subject: [PATCH 1/2] Simplify ConfigYamlLoaderImpl --- .../decline/yaml/ConfigYamlLoaderImpl.scala | 247 ++---------------- .../decline/yaml/ConfigYamlLoaderTest.scala | 7 +- 2 files changed, 31 insertions(+), 223 deletions(-) diff --git a/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala b/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala index f13be5b..9bd3013 100644 --- a/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala +++ b/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala @@ -1,10 +1,9 @@ package ru.d10xa.jsonlogviewer.decline.yaml -import cats.data.NonEmptyList -import cats.data.Validated import cats.data.ValidatedNel import cats.syntax.all.* import io.circe.* +import io.circe.generic.semiauto.* import io.circe.yaml.scalayaml.parser import ru.d10xa.jsonlogviewer.decline.Config.FormatIn import ru.d10xa.jsonlogviewer.decline.FormatInValidator @@ -21,239 +20,47 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader { .replace("\\n", " ") .trim - private def parseOptionalQueryAST( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, Option[QueryAST]] = - parseOptionalStringField( - fields, - fieldName, - s"Invalid '$fieldName' field format" - ).andThen { - case Some(str) => - val trimmed = trimCommentedLines(str) - QueryASTValidator.toValidatedQueryAST(trimmed).map(Some(_)) - case None => Validated.valid(None) - } - - private def parseOptionalFormatIn( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, Option[FormatIn]] = - parseOptionalStringField( - fields, - fieldName, - s"Invalid '$fieldName' field format" - ).andThen { - case Some(formatStr) => - FormatInValidator.toValidatedFormatIn(formatStr).map(Some(_)) - case None => Validated.valid(None) - } - - private def parseOptionalListString( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, Option[List[String]]] = - fields.get(fieldName) match { - case Some(jsonValue) => - jsonValue - .as[List[String]] - .leftMap(_ => s"Invalid '$fieldName' field format") - .toValidatedNel - .map(Some(_)) - case None => Validated.valid(None) - } - - private def parseOptionalFeeds( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, Option[List[Feed]]] = - fields.get(fieldName) match { - case Some(jsonValue) => - jsonValue - .as[List[Json]] - .leftMap(_ => s"Invalid '$fieldName' field format, should be a list") - .toValidatedNel - .andThen(_.traverse(parseFeed)) - .map(Some(_)) - case None => Validated.valid(None) - } - - private def parseOptionalStringField( - fields: Map[String, Json], - fieldName: String, - errorMsg: String - ): ValidatedNel[String, Option[String]] = - fields.get(fieldName) match { - case Some(jsonValue) => - jsonValue.as[String].leftMap(_ => errorMsg).toValidatedNel.map(Some(_)) - case None => Validated.valid(None) - } - - private def parseString( - fields: Map[String, Json], - fieldName: String, - errorMsg: String - ): ValidatedNel[String, String] = - fields.get(fieldName) match { - case Some(j) => - j.as[String].leftMap(_ => errorMsg).toValidatedNel - case None => - Validated.invalidNel(s"Missing '$fieldName' field in feed") - } - - private def parseListString( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, List[String]] = - fields.get(fieldName) match { - case Some(c) => - c.as[List[String]] - .leftMap(_ => s"Invalid '$fieldName' field in feed") - .toValidatedNel - case None => - Validated.invalidNel(s"Missing '$fieldName' field in feed") - } - - private def parseOptionalString( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, Option[String]] = - fields.get(fieldName) match { - case Some(c) => - c.as[Option[String]] - .leftMap(_ => s"Invalid '$fieldName' field in feed") - .toValidatedNel - case None => - Validated.valid(None) - } - - private def parseOptionalFieldNames( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, Option[FieldNames]] = - fields.get(fieldName) match { - case Some(jsonValue) => - jsonValue.asObject.map(_.toMap) match { - case None => - Validated.invalidNel( - s"Invalid '$fieldName' field format, should be an object" - ) - case Some(fieldNamesFields) => - val timestampValidated = - parseOptionalString(fieldNamesFields, "timestamp") - val levelValidated = parseOptionalString(fieldNamesFields, "level") - val messageValidated = - parseOptionalString(fieldNamesFields, "message") - val stackTraceValidated = - parseOptionalString(fieldNamesFields, "stackTrace") - val loggerNameValidated = - parseOptionalString(fieldNamesFields, "loggerName") - val threadNameValidated = - parseOptionalString(fieldNamesFields, "threadName") - - ( - timestampValidated, - levelValidated, - messageValidated, - stackTraceValidated, - loggerNameValidated, - threadNameValidated - ).mapN(FieldNames.apply).map(Some(_)) - } - case None => Validated.valid(None) + private given Decoder[QueryAST] = Decoder[String].emap { str => + val trimmed = trimCommentedLines(str) + QueryASTValidator.toValidatedQueryAST(trimmed).toEither.leftMap { errors => + errors.toList.mkString(", ") } + } - private def parseOptionalBoolean( - fields: Map[String, Json], - fieldName: String - ): ValidatedNel[String, Option[Boolean]] = - fields.get(fieldName) match { - case Some(jsonValue) => - jsonValue - .as[Boolean] - .leftMap(_ => - s"Invalid '$fieldName' field format, should be a boolean" - ) - .toValidatedNel - .map(Some(_)) - case None => Validated.valid(None) + // Custom Decoder for FormatIn - converts string to enum + private given Decoder[FormatIn] = Decoder[String].emap { formatStr => + FormatInValidator.toValidatedFormatIn(formatStr).toEither.leftMap { errors => + errors.toList.mkString(", ") } + } - private def parseFeed(feedJson: Json): ValidatedNel[String, Feed] = - feedJson.asObject.map(_.toMap) match { - case None => Validated.invalidNel("Feed entry is not a valid JSON object") - case Some(feedFields) => - val nameValidated = parseOptionalString( - feedFields, - "name" - ) - val commandsValidated = parseListString(feedFields, "commands") - val inlineInputValidated = - parseOptionalString(feedFields, "inlineInput") - val filterValidated = parseOptionalQueryAST(feedFields, "filter") - val formatInValidated - : Validated[NonEmptyList[String], Option[FormatIn]] = - parseOptionalFormatIn(feedFields, "formatIn") - val fieldNamesValidated = - parseOptionalFieldNames(feedFields, "fieldNames") - val rawIncludeValidated = - parseOptionalListString(feedFields, "rawInclude") - val rawExcludeValidated = - parseOptionalListString(feedFields, "rawExclude") - val fuzzyIncludeValidated = - parseOptionalListString(feedFields, "fuzzyInclude") - val fuzzyExcludeValidated = - parseOptionalListString(feedFields, "fuzzyExclude") - val excludeFieldsValidated = - parseOptionalListString( - feedFields, - "excludeFields" - ) - val showEmptyFieldsValidated = - parseOptionalBoolean(feedFields, "showEmptyFields") - - ( - nameValidated, - commandsValidated, - inlineInputValidated, - filterValidated, - formatInValidated, - fieldNamesValidated, - rawIncludeValidated, - rawExcludeValidated, - fuzzyIncludeValidated, - fuzzyExcludeValidated, - excludeFieldsValidated, - showEmptyFieldsValidated - ) - .mapN(Feed.apply) - } + // Automatic derivation for case classes + private given Decoder[FieldNames] = deriveDecoder[FieldNames] + private given Decoder[Feed] = deriveDecoder[Feed] + private given Decoder[ConfigYaml] = deriveDecoder[ConfigYaml] def parseYamlFile(content: String): ValidatedNel[String, ConfigYaml] = { val uncommentedContent = content.linesIterator .filterNot(line => line.trim.startsWith("#")) .mkString("\n") .trim + if (uncommentedContent.isEmpty) { - Validated.valid(ConfigYaml.empty) + cats.data.Validated.valid(ConfigYaml.empty) } else { parser.parse(content) match { case Left(error) => - Validated.invalidNel(s"YAML parsing error: ${error.getMessage}") + cats.data.Validated.invalidNel( + s"YAML parsing error: ${error.getMessage}" + ) case Right(json) => - json.asObject.map(_.toMap) match { - case None => Validated.invalidNel("YAML is not a valid JSON object") - case Some(fields) => - val feedsValidated: ValidatedNel[String, Option[List[Feed]]] = - parseOptionalFeeds(fields, "feeds") - val fieldNamesValidated = - parseOptionalFieldNames(fields, "fieldNames") - val showEmptyFieldsValidated = - parseOptionalBoolean(fields, "showEmptyFields") - - (fieldNamesValidated, feedsValidated, showEmptyFieldsValidated) - .mapN(ConfigYaml.apply) + json.as[ConfigYaml] match { + case Right(config) => + cats.data.Validated.valid(config) + case Left(error) => + cats.data.Validated.invalidNel( + s"YAML validation error: ${error.getMessage}" + ) } } } diff --git a/json-log-viewer/jvm/src/test/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderTest.scala b/json-log-viewer/jvm/src/test/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderTest.scala index ec6cd82..67ce4e3 100644 --- a/json-log-viewer/jvm/src/test/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderTest.scala +++ b/json-log-viewer/jvm/src/test/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderTest.scala @@ -66,10 +66,11 @@ class ConfigYamlLoaderTest extends FunSuite { assert(result.isInvalid, s"Result should be invalid: $result") val errors = result.swap.toOption.get + // Check that error is related to 'feeds' field + // Circe's automatic decoder will produce a different but still clear error message assert( - errors.exists( - _.contains("Invalid 'feeds' field format, should be a list") - ) + errors.exists(e => e.contains("feeds") || e.contains("validation")), + s"Error should mention 'feeds' or 'validation', but got: ${errors.toList}" ) } From 052a8a60a55d337ace29e07630ede1d7d0987148 Mon Sep 17 00:00:00 2001 From: Andrey Stolyarov Date: Thu, 20 Nov 2025 20:36:58 +0300 Subject: [PATCH 2/2] Simplify ConfigYamlLoaderImpl --- .../d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala | 2 -- 1 file changed, 2 deletions(-) diff --git a/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala b/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala index 9bd3013..313bcda 100644 --- a/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala +++ b/json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala @@ -27,14 +27,12 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader { } } - // Custom Decoder for FormatIn - converts string to enum private given Decoder[FormatIn] = Decoder[String].emap { formatStr => FormatInValidator.toValidatedFormatIn(formatStr).toEither.leftMap { errors => errors.toList.mkString(", ") } } - // Automatic derivation for case classes private given Decoder[FieldNames] = deriveDecoder[FieldNames] private given Decoder[Feed] = deriveDecoder[Feed] private given Decoder[ConfigYaml] = deriveDecoder[ConfigYaml]