Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add introspectable ConfigSpec #770

Open
wants to merge 40 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
e0db591
vcs: Ignore Metals/VSC files
Iltotore Aug 20, 2024
6ab2ffc
refactor: Change internal structure of ConfigValue
Iltotore Aug 23, 2024
027f3ee
feat: Allow introspection on evalMap
Iltotore Aug 23, 2024
66cddfb
feat: Allow introspection on Or
Iltotore Aug 23, 2024
07b93f0
feat: Enable to get the fields of a ConfigValue
Iltotore Aug 26, 2024
9141cc0
feat: Add isomorphic (ish) methods
Iltotore Aug 26, 2024
f5999d2
misc: Deprecate unidirectional map operations
Iltotore Aug 26, 2024
9b3a368
fix: Wrong "since" version for deprecations
Iltotore Aug 26, 2024
eaba692
feat: Add ConfigCodec
Iltotore Aug 27, 2024
e3ad0c2
refactor: Replace as(ConfigDecoder) with as(ConfigCodec)
Iltotore Aug 27, 2024
acc8f7c
feat: Track optionality in ConfigValue#fields
Iltotore Aug 27, 2024
90d6cb9
fix: Remove deprecation warnings in ConfigFieldSpec
Iltotore Aug 27, 2024
1579255
feat: Add codecs for enumeratum
Iltotore Aug 27, 2024
c6d405c
feat: Add codecs for http4s
Iltotore Aug 27, 2024
b1cbecc
feat: Add codecs for refined
Iltotore Aug 27, 2024
1bd4939
feat: Add codecs for squants
Iltotore Aug 27, 2024
65483d2
feat: Add codecs for circe
Iltotore Aug 27, 2024
22d9e50
feat: Add codecs for circe-yaml
Iltotore Aug 27, 2024
065ba0c
fix: Compatibility with Scala 2.12.x
Iltotore Aug 28, 2024
5908eb5
feat: Add ConfigCodec.pure
Iltotore Aug 30, 2024
5435c88
Change to sort gitignores alphabetically
vlovgr Sep 18, 2024
a54f462
Change yamlConfigEncoder to yamlConfigCodec
vlovgr Sep 18, 2024
cbf0bfb
Update circe deprecation message and tests
vlovgr Sep 18, 2024
7f957c1
Update enumeratum deprecation messages
vlovgr Sep 18, 2024
b3adb2c
Refactor and document ConfigField
vlovgr Sep 18, 2024
7870e09
Update ConfigValue after ConfigField refactorings
vlovgr Sep 18, 2024
f0d7b7d
fix: default/option priority
Iltotore Sep 26, 2024
cc41d61
fix: Restore flatMap for retrocompat reasons
Iltotore Sep 26, 2024
d87c95e
fix: Restore nonEmptyParallel for retrocompatibility reasons
Iltotore Sep 26, 2024
f28e6d6
refactor: Restore ConfigDecoder-as and use `asIso` for `ConfigCodec`
Iltotore Sep 26, 2024
d00be0d
fix: Restore evalFlatMap for retrocompatibility reasons
Iltotore Sep 26, 2024
bd90c5f
fix: Add configValueFlatMap alias to preserve binary compatibility
Iltotore Sep 27, 2024
37fd471
Exclude ConfigValue#fieldsRec from MiMa since class is sealed
vlovgr Sep 30, 2024
9b12e67
Exclude CirisValueEnum#cirisConfigCodec from MiMa since trait is sealed
vlovgr Sep 30, 2024
b8e091d
Add back law tests for ConfigValue
vlovgr Sep 30, 2024
99d1b5e
Remove leftover println in ConfigValueSpec
vlovgr Sep 30, 2024
cbfc04d
Remove ConfigValue#decodeAs
vlovgr Sep 30, 2024
1a6ee0d
Make ConfigValue cases private and final
vlovgr Sep 30, 2024
a28bad7
Make ConfigValue#fieldsRec private to ciris
vlovgr Sep 30, 2024
277643f
Change ConfigValue#fields to def
vlovgr Sep 30, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
.bloop/
.bsp/
.metals/
.vscode/
/docs/_site/
/website/blog/
/website/build/
/website/node_modules/
/website/static/api/
/website/variables.js
/website/yarn.lock
metals.sbt
target/
4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,9 @@ lazy val mimaSettings = Seq(
// format: off
Seq(
ProblemFilters.exclude[DirectMissingMethodProblem]("ciris.ConfigKey.file"),
ProblemFilters.exclude[DirectMissingMethodProblem]("ciris.package.file")
ProblemFilters.exclude[DirectMissingMethodProblem]("ciris.package.file"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("ciris.ConfigValue.fieldsRec"),
ProblemFilters.exclude[ReversedMissingMethodProblem]("enumeratum.values.CirisValueEnum.cirisConfigCodec")
)
// format: on
}
Expand Down
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 @@ -8,11 +8,15 @@ package ciris.circe

import cats.syntax.all._
import ciris.{ConfigDecoder, ConfigError}
import ciris.ConfigCodec
import io.circe.{Decoder, Json}
import io.circe.{DecodingFailure, ParsingFailure}
import io.circe.Encoder
import io.circe.yaml.parser.parse

package object yaml {

@deprecated("Use ConfigCodec and circeYamlConfigCodec 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 yamlConfigCodec: ConfigCodec[String, Json] =
circeYamlConfigCodec("Yaml")
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,97 +16,97 @@ 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"))
.asIso[Int](circeYamlConfigCodec("Int"))
.load[IO]
.assertEquals(123)
}

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

test("circeYamlConfigDecoder.success.quotes") {
test("circeYamlConfigCodec.success.quotes") {
default("\"abc\"")
.as[String](circeYamlConfigDecoder("String"))
.asIso[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").asIso[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\"").asIso[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\"").asIso[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\"").asIso[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"))
.asIso[Int](circeYamlConfigCodec("Int"))
.redacted,
"Key cannot be decoded to Int"
)
}

test("yamlConfigDecoder.success") {
test("yamlConfigCodec.success") {
default("123")
.as[Json]
.asIso[Json]
.load[IO]
.map(_.asNumber.flatMap(_.toInt).contains(123))
.assert
}

test("yamlConfigDecoder.invalid") {
test("yamlConfigCodec.invalid") {
checkError(
ConfigValue.default("\"no\"").as[Boolean],
ConfigValue.default("\"no\"").asIso[Boolean],
"Unable to convert value \"no\" to Boolean"
)
}

test("yamlConfigDecoder.invalid.redacted") {
test("yamlConfigCodec.invalid.redacted") {
checkError(
ConfigValue.default("\"no\"").as[Boolean].redacted,
ConfigValue.default("\"no\"").asIso[Boolean].redacted,
"Unable to convert value to Boolean"
)
}

test("yamlConfigDecoder.invalid.loaded") {
test("yamlConfigCodec.invalid.loaded") {
checkError(
ConfigValue.loaded(ConfigKey("key"), "\"no\"").as[Boolean],
ConfigValue.loaded(ConfigKey("key"), "\"no\"").asIso[Boolean],
"Key with value \"no\" cannot be converted to Boolean"
)
}

test("yamlConfigDecoder.invalid.loaded.redacted") {
test("yamlConfigCodec.invalid.loaded.redacted") {
checkError(
ConfigValue.loaded(ConfigKey("key"), "\"no\"").as[Boolean].redacted,
ConfigValue.loaded(ConfigKey("key"), "\"no\"").asIso[Boolean].redacted,
"Key cannot be converted to Boolean"
)
}
Expand Down
69 changes: 69 additions & 0 deletions modules/circe/shared/src/main/scala/ciris/circe/circe.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ package ciris
import cats.syntax.all._
import io.circe.{Decoder, Json}
import io.circe.{DecodingFailure, ParsingFailure}
import io.circe.Encoder
import io.circe.parser.parse

package object circe {

@deprecated("Use ConfigCodec and circeConfigCodec instead", "3.7.0")
final def circeConfigDecoder[A](
typeName: String
)(implicit decoder: Decoder[A]): ConfigDecoder[String, A] =
Expand Down Expand Up @@ -74,6 +77,72 @@ package object circe {
} yield a
}

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

final def circeConfigCodec[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 jsonConfigCodec: ConfigCodec[String, Json] =
circeConfigCodec("Json")
}
Loading
Loading