Skip to content

Commit

Permalink
feat: Add codecs for circe-yaml
Browse files Browse the repository at this point in the history
  • Loading branch information
Iltotore committed Aug 27, 2024
1 parent 65483d2 commit 22d9e50
Show file tree
Hide file tree
Showing 2 changed files with 86 additions and 16 deletions.
70 changes: 70 additions & 0 deletions modules/circe-yaml/src/main/scala/ciris/circe/yaml/yaml.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,12 @@ import ciris.{ConfigDecoder, ConfigError}
import io.circe.{Decoder, Json}
import io.circe.{DecodingFailure, ParsingFailure}
import io.circe.yaml.parser.parse
import io.circe.Encoder
import ciris.ConfigCodec

package object yaml {

@deprecated("Use ConfigCodec instead", "3.7.0")
final def circeYamlConfigDecoder[A](
typeName: String
)(implicit decoder: Decoder[A]): ConfigDecoder[String, A] =
Expand Down Expand Up @@ -75,6 +79,72 @@ package object yaml {
} yield a
}

@deprecated("Use ConfigCodec instead", "3.7.0")
implicit final val yamlConfigDecoder: ConfigDecoder[String, Json] =
circeYamlConfigDecoder("Yaml")

final def circeYamlConfigCodec[A](
typeName: String
)(implicit decoder: Decoder[A], encoder: Encoder[A]): ConfigCodec[String, A] =
ConfigCodec[String].imapEither { (key, value) =>
def decodeError(json: Json, decodingFailure: DecodingFailure): ConfigError = {
def message(valueShown: Option[String], decodingFailureMessage: Option[String]): String = {
def trailingDecodingFailureMessage =
decodingFailureMessage match {
case Some(message) => s": $message"
case None => ""
}

(key, valueShown) match {
case (Some(key), Some(value)) =>
s"${key.description.capitalize} with json $value cannot be decoded to $typeName$trailingDecodingFailureMessage"
case (Some(key), None) =>
s"${key.description.capitalize} cannot be decoded to $typeName$trailingDecodingFailureMessage"
case (None, Some(value)) =>
s"Unable to decode json $value to $typeName$trailingDecodingFailureMessage"
case (None, None) =>
s"Unable to decode json to $typeName$trailingDecodingFailureMessage"
}
}

ConfigError.sensitive(
message = message(Some(json.noSpaces), Some(decodingFailure.getMessage)),
redactedMessage = message(None, None)
)
}

def parseError(parsingFailure: ParsingFailure): ConfigError = {
def message(valueShown: Option[String], parsingFailureMessage: Option[String]): String = {
def trailingParsingFailureMessage =
parsingFailureMessage match {
case Some(message) => s": $message"
case None => ""
}

(key, valueShown) match {
case (Some(key), Some(value)) =>
s"${key.description.capitalize} with value $value cannot be parsed as json$trailingParsingFailureMessage"
case (Some(key), None) =>
s"${key.description.capitalize} cannot be parsed as json$trailingParsingFailureMessage"
case (None, Some(value)) =>
s"Unable to parse value $value as json$trailingParsingFailureMessage"
case (None, None) =>
s"Unable to parse value as json$trailingParsingFailureMessage"
}
}

ConfigError.sensitive(
message = message(Some(value), Some(parsingFailure.getMessage)),
redactedMessage = message(None, None)
)
}

for {
json <- parse(value).leftMap(parseError)
a <- json.as[A].leftMap(decodeError(json, _))
} yield a
}(encoder(_).toString)

implicit final val yamlConfigEncoder: ConfigCodec[String, Json] =
circeYamlConfigCodec("Yaml")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,60 +16,60 @@ import io.circe.yaml.syntax._
import munit.CatsEffectSuite

final class CirceYamlSpec extends CatsEffectSuite {
test("circeYamlConfigDecoder.success") {
test("circeYamlConfigCodec.success") {
default("123")
.as[Int](circeYamlConfigDecoder("Int"))
.as[Int](circeYamlConfigCodec("Int"))
.load[IO]
.assertEquals(123)
}

test("circeYamlConfigDecoder.success.noquotes") {
test("circeYamlConfigCodec.success.noquotes") {
default("abc")
.as[String](circeYamlConfigDecoder("String"))
.as[String](circeYamlConfigCodec("String"))
.load[IO]
.assertEquals("abc")
}

test("circeYamlConfigDecoder.success.quotes") {
test("circeYamlConfigCodec.success.quotes") {
default("\"abc\"")
.as[String](circeYamlConfigDecoder("String"))
.as[String](circeYamlConfigCodec("String"))
.load[IO]
.assertEquals("abc")
}

test("circeYamlConfigDecoder.invalid.noquotes") {
test("circeYamlConfigCodec.invalid.noquotes") {
checkError(
ConfigValue.default("abc").as[Int](circeYamlConfigDecoder("Int")),
ConfigValue.default("abc").as[Int](circeYamlConfigCodec("Int")),
"""Unable to decode json "abc" to Int: DecodingFailure at : Int"""
)
}

test("circeYamlConfigDecoder.invalid") {
test("circeYamlConfigCodec.invalid") {
checkError(
ConfigValue.default("\"abc\"").as[Int](circeYamlConfigDecoder("Int")),
ConfigValue.default("\"abc\"").as[Int](circeYamlConfigCodec("Int")),
"""Unable to decode json "abc" to Int: DecodingFailure at : Int"""
)
}

test("circeYamlConfigDecoder.invalid.redacted") {
test("circeYamlConfigCodec.invalid.redacted") {
checkError(
ConfigValue.default("\"abc\"").as[Int](circeYamlConfigDecoder("Int")).redacted,
ConfigValue.default("\"abc\"").as[Int](circeYamlConfigCodec("Int")).redacted,
"Unable to decode json to Int"
)
}

test("circeYamlConfigDecoder.invalid.loaded") {
test("circeYamlConfigCodec.invalid.loaded") {
checkError(
ConfigValue.loaded(ConfigKey("key"), "\"abc\"").as[Int](circeYamlConfigDecoder("Int")),
ConfigValue.loaded(ConfigKey("key"), "\"abc\"").as[Int](circeYamlConfigCodec("Int")),
"""Key with json "abc" cannot be decoded to Int: DecodingFailure at : Int"""
)
}

test("circeYamlConfigDecoder.invalid.loaded.redacted") {
test("circeYamlConfigCodec.invalid.loaded.redacted") {
checkError(
ConfigValue
.loaded(ConfigKey("key"), "\"abc\"")
.as[Int](circeYamlConfigDecoder("Int"))
.as[Int](circeYamlConfigCodec("Int"))
.redacted,
"Key cannot be decoded to Int"
)
Expand Down

0 comments on commit 22d9e50

Please sign in to comment.