diff --git a/modules/circe-yaml/src/main/scala/ciris/circe/yaml/yaml.scala b/modules/circe-yaml/src/main/scala/ciris/circe/yaml/yaml.scala index 0be1abd6..dca86ad6 100644 --- a/modules/circe-yaml/src/main/scala/ciris/circe/yaml/yaml.scala +++ b/modules/circe-yaml/src/main/scala/ciris/circe/yaml/yaml.scala @@ -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] = @@ -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") } diff --git a/modules/circe-yaml/src/test/scala/ciris/circe/yaml/CirceYamlSpec.scala b/modules/circe-yaml/src/test/scala/ciris/circe/yaml/CirceYamlSpec.scala index fb9e3f36..f039b516 100644 --- a/modules/circe-yaml/src/test/scala/ciris/circe/yaml/CirceYamlSpec.scala +++ b/modules/circe-yaml/src/test/scala/ciris/circe/yaml/CirceYamlSpec.scala @@ -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" )