Skip to content

Commit a1f2b54

Browse files
authored
Simplify ConfigYamlLoaderImpl (#34)
* Simplify ConfigYamlLoaderImpl * Simplify ConfigYamlLoaderImpl
1 parent 69ac6b7 commit a1f2b54

File tree

2 files changed

+29
-223
lines changed

2 files changed

+29
-223
lines changed

json-log-viewer/jvm/src/main/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderImpl.scala

Lines changed: 25 additions & 220 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
package ru.d10xa.jsonlogviewer.decline.yaml
22

3-
import cats.data.NonEmptyList
4-
import cats.data.Validated
53
import cats.data.ValidatedNel
64
import cats.syntax.all.*
75
import io.circe.*
6+
import io.circe.generic.semiauto.*
87
import io.circe.yaml.scalayaml.parser
98
import ru.d10xa.jsonlogviewer.decline.Config.FormatIn
109
import ru.d10xa.jsonlogviewer.decline.FormatInValidator
@@ -21,239 +20,45 @@ class ConfigYamlLoaderImpl extends ConfigYamlLoader {
2120
.replace("\\n", " ")
2221
.trim
2322

24-
private def parseOptionalQueryAST(
25-
fields: Map[String, Json],
26-
fieldName: String
27-
): ValidatedNel[String, Option[QueryAST]] =
28-
parseOptionalStringField(
29-
fields,
30-
fieldName,
31-
s"Invalid '$fieldName' field format"
32-
).andThen {
33-
case Some(str) =>
34-
val trimmed = trimCommentedLines(str)
35-
QueryASTValidator.toValidatedQueryAST(trimmed).map(Some(_))
36-
case None => Validated.valid(None)
37-
}
38-
39-
private def parseOptionalFormatIn(
40-
fields: Map[String, Json],
41-
fieldName: String
42-
): ValidatedNel[String, Option[FormatIn]] =
43-
parseOptionalStringField(
44-
fields,
45-
fieldName,
46-
s"Invalid '$fieldName' field format"
47-
).andThen {
48-
case Some(formatStr) =>
49-
FormatInValidator.toValidatedFormatIn(formatStr).map(Some(_))
50-
case None => Validated.valid(None)
51-
}
52-
53-
private def parseOptionalListString(
54-
fields: Map[String, Json],
55-
fieldName: String
56-
): ValidatedNel[String, Option[List[String]]] =
57-
fields.get(fieldName) match {
58-
case Some(jsonValue) =>
59-
jsonValue
60-
.as[List[String]]
61-
.leftMap(_ => s"Invalid '$fieldName' field format")
62-
.toValidatedNel
63-
.map(Some(_))
64-
case None => Validated.valid(None)
65-
}
66-
67-
private def parseOptionalFeeds(
68-
fields: Map[String, Json],
69-
fieldName: String
70-
): ValidatedNel[String, Option[List[Feed]]] =
71-
fields.get(fieldName) match {
72-
case Some(jsonValue) =>
73-
jsonValue
74-
.as[List[Json]]
75-
.leftMap(_ => s"Invalid '$fieldName' field format, should be a list")
76-
.toValidatedNel
77-
.andThen(_.traverse(parseFeed))
78-
.map(Some(_))
79-
case None => Validated.valid(None)
80-
}
81-
82-
private def parseOptionalStringField(
83-
fields: Map[String, Json],
84-
fieldName: String,
85-
errorMsg: String
86-
): ValidatedNel[String, Option[String]] =
87-
fields.get(fieldName) match {
88-
case Some(jsonValue) =>
89-
jsonValue.as[String].leftMap(_ => errorMsg).toValidatedNel.map(Some(_))
90-
case None => Validated.valid(None)
91-
}
92-
93-
private def parseString(
94-
fields: Map[String, Json],
95-
fieldName: String,
96-
errorMsg: String
97-
): ValidatedNel[String, String] =
98-
fields.get(fieldName) match {
99-
case Some(j) =>
100-
j.as[String].leftMap(_ => errorMsg).toValidatedNel
101-
case None =>
102-
Validated.invalidNel(s"Missing '$fieldName' field in feed")
103-
}
104-
105-
private def parseListString(
106-
fields: Map[String, Json],
107-
fieldName: String
108-
): ValidatedNel[String, List[String]] =
109-
fields.get(fieldName) match {
110-
case Some(c) =>
111-
c.as[List[String]]
112-
.leftMap(_ => s"Invalid '$fieldName' field in feed")
113-
.toValidatedNel
114-
case None =>
115-
Validated.invalidNel(s"Missing '$fieldName' field in feed")
116-
}
117-
118-
private def parseOptionalString(
119-
fields: Map[String, Json],
120-
fieldName: String
121-
): ValidatedNel[String, Option[String]] =
122-
fields.get(fieldName) match {
123-
case Some(c) =>
124-
c.as[Option[String]]
125-
.leftMap(_ => s"Invalid '$fieldName' field in feed")
126-
.toValidatedNel
127-
case None =>
128-
Validated.valid(None)
129-
}
130-
131-
private def parseOptionalFieldNames(
132-
fields: Map[String, Json],
133-
fieldName: String
134-
): ValidatedNel[String, Option[FieldNames]] =
135-
fields.get(fieldName) match {
136-
case Some(jsonValue) =>
137-
jsonValue.asObject.map(_.toMap) match {
138-
case None =>
139-
Validated.invalidNel(
140-
s"Invalid '$fieldName' field format, should be an object"
141-
)
142-
case Some(fieldNamesFields) =>
143-
val timestampValidated =
144-
parseOptionalString(fieldNamesFields, "timestamp")
145-
val levelValidated = parseOptionalString(fieldNamesFields, "level")
146-
val messageValidated =
147-
parseOptionalString(fieldNamesFields, "message")
148-
val stackTraceValidated =
149-
parseOptionalString(fieldNamesFields, "stackTrace")
150-
val loggerNameValidated =
151-
parseOptionalString(fieldNamesFields, "loggerName")
152-
val threadNameValidated =
153-
parseOptionalString(fieldNamesFields, "threadName")
154-
155-
(
156-
timestampValidated,
157-
levelValidated,
158-
messageValidated,
159-
stackTraceValidated,
160-
loggerNameValidated,
161-
threadNameValidated
162-
).mapN(FieldNames.apply).map(Some(_))
163-
}
164-
case None => Validated.valid(None)
23+
private given Decoder[QueryAST] = Decoder[String].emap { str =>
24+
val trimmed = trimCommentedLines(str)
25+
QueryASTValidator.toValidatedQueryAST(trimmed).toEither.leftMap { errors =>
26+
errors.toList.mkString(", ")
16527
}
28+
}
16629

167-
private def parseOptionalBoolean(
168-
fields: Map[String, Json],
169-
fieldName: String
170-
): ValidatedNel[String, Option[Boolean]] =
171-
fields.get(fieldName) match {
172-
case Some(jsonValue) =>
173-
jsonValue
174-
.as[Boolean]
175-
.leftMap(_ =>
176-
s"Invalid '$fieldName' field format, should be a boolean"
177-
)
178-
.toValidatedNel
179-
.map(Some(_))
180-
case None => Validated.valid(None)
30+
private given Decoder[FormatIn] = Decoder[String].emap { formatStr =>
31+
FormatInValidator.toValidatedFormatIn(formatStr).toEither.leftMap { errors =>
32+
errors.toList.mkString(", ")
18133
}
34+
}
18235

183-
private def parseFeed(feedJson: Json): ValidatedNel[String, Feed] =
184-
feedJson.asObject.map(_.toMap) match {
185-
case None => Validated.invalidNel("Feed entry is not a valid JSON object")
186-
case Some(feedFields) =>
187-
val nameValidated = parseOptionalString(
188-
feedFields,
189-
"name"
190-
)
191-
val commandsValidated = parseListString(feedFields, "commands")
192-
val inlineInputValidated =
193-
parseOptionalString(feedFields, "inlineInput")
194-
val filterValidated = parseOptionalQueryAST(feedFields, "filter")
195-
val formatInValidated
196-
: Validated[NonEmptyList[String], Option[FormatIn]] =
197-
parseOptionalFormatIn(feedFields, "formatIn")
198-
val fieldNamesValidated =
199-
parseOptionalFieldNames(feedFields, "fieldNames")
200-
val rawIncludeValidated =
201-
parseOptionalListString(feedFields, "rawInclude")
202-
val rawExcludeValidated =
203-
parseOptionalListString(feedFields, "rawExclude")
204-
val fuzzyIncludeValidated =
205-
parseOptionalListString(feedFields, "fuzzyInclude")
206-
val fuzzyExcludeValidated =
207-
parseOptionalListString(feedFields, "fuzzyExclude")
208-
val excludeFieldsValidated =
209-
parseOptionalListString(
210-
feedFields,
211-
"excludeFields"
212-
)
213-
val showEmptyFieldsValidated =
214-
parseOptionalBoolean(feedFields, "showEmptyFields")
215-
216-
(
217-
nameValidated,
218-
commandsValidated,
219-
inlineInputValidated,
220-
filterValidated,
221-
formatInValidated,
222-
fieldNamesValidated,
223-
rawIncludeValidated,
224-
rawExcludeValidated,
225-
fuzzyIncludeValidated,
226-
fuzzyExcludeValidated,
227-
excludeFieldsValidated,
228-
showEmptyFieldsValidated
229-
)
230-
.mapN(Feed.apply)
231-
}
36+
private given Decoder[FieldNames] = deriveDecoder[FieldNames]
37+
private given Decoder[Feed] = deriveDecoder[Feed]
38+
private given Decoder[ConfigYaml] = deriveDecoder[ConfigYaml]
23239

23340
def parseYamlFile(content: String): ValidatedNel[String, ConfigYaml] = {
23441
val uncommentedContent = content.linesIterator
23542
.filterNot(line => line.trim.startsWith("#"))
23643
.mkString("\n")
23744
.trim
45+
23846
if (uncommentedContent.isEmpty) {
239-
Validated.valid(ConfigYaml.empty)
47+
cats.data.Validated.valid(ConfigYaml.empty)
24048
} else {
24149
parser.parse(content) match {
24250
case Left(error) =>
243-
Validated.invalidNel(s"YAML parsing error: ${error.getMessage}")
51+
cats.data.Validated.invalidNel(
52+
s"YAML parsing error: ${error.getMessage}"
53+
)
24454
case Right(json) =>
245-
json.asObject.map(_.toMap) match {
246-
case None => Validated.invalidNel("YAML is not a valid JSON object")
247-
case Some(fields) =>
248-
val feedsValidated: ValidatedNel[String, Option[List[Feed]]] =
249-
parseOptionalFeeds(fields, "feeds")
250-
val fieldNamesValidated =
251-
parseOptionalFieldNames(fields, "fieldNames")
252-
val showEmptyFieldsValidated =
253-
parseOptionalBoolean(fields, "showEmptyFields")
254-
255-
(fieldNamesValidated, feedsValidated, showEmptyFieldsValidated)
256-
.mapN(ConfigYaml.apply)
55+
json.as[ConfigYaml] match {
56+
case Right(config) =>
57+
cats.data.Validated.valid(config)
58+
case Left(error) =>
59+
cats.data.Validated.invalidNel(
60+
s"YAML validation error: ${error.getMessage}"
61+
)
25762
}
25863
}
25964
}

json-log-viewer/jvm/src/test/scala/ru/d10xa/jsonlogviewer/decline/yaml/ConfigYamlLoaderTest.scala

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,11 @@ class ConfigYamlLoaderTest extends FunSuite {
6666
assert(result.isInvalid, s"Result should be invalid: $result")
6767

6868
val errors = result.swap.toOption.get
69+
// Check that error is related to 'feeds' field
70+
// Circe's automatic decoder will produce a different but still clear error message
6971
assert(
70-
errors.exists(
71-
_.contains("Invalid 'feeds' field format, should be a list")
72-
)
72+
errors.exists(e => e.contains("feeds") || e.contains("validation")),
73+
s"Error should mention 'feeds' or 'validation', but got: ${errors.toList}"
7374
)
7475
}
7576

0 commit comments

Comments
 (0)