-
Notifications
You must be signed in to change notification settings - Fork 48
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
base: master
Are you sure you want to change the base?
Conversation
Hey, thanks for looking into this. I know there is some interest in this already. My current thinking for the next major version is to change I haven't started this work yet, so it's great to see this already. If you want to continue working on this, I would suggest you start looking into changing |
Thank you for the insight. I originally did not touch I'm not familiar with |
86060fd
to
e0db591
Compare
Should val config = (
ConfigValue.prop("user.dir"),
ConfigValue.prop("does.not.exist").or(ConfigValue.env("USERNAME"))
).mapN(ProgramEnvironment.apply)
.default(ProgramEnvironment("/", "default_user")) //We cannot map the default value back to its tuple form |
* Use named nodes (case classes) instead of anonymous classes * Remove `flatMap`
I pushed the beginning of my work on |
There's a draft pull request for cats and the mentioned issue has more details.
I think it makes sense, yes. Similarly to how
Not sure exactly what you mean, could you perhaps be a little more specific? Edit: if you mean what we should do about them, I guess we can’t really keep them, right? |
Actually, only
They still can be evaluated but they cannot be introspected. Since most of them are "constructors"/"leaves", they contaminate their entire branch: (
blocking(...).default("default_value"),
env("FOO_B").as[Int]
).tupled.fields will return only the environment variable "FOO_B". The only exception is the method def getUserId(txt: String): IO[UserId] = ???
env("USERNAME")
.evalMap(getUserId)
.default(defaultUserId)
.fields //Environment variable "USERNAME" without default value If map-like operations are turned to imaps, then default values after def getUserId(txt: String): IO[UserId] = ???
def getName(id: UserId): String = ???
env("USERNAME")
.evalIMap(getUserId)(getName)
.default(defaultUserId)
.fieldsAsync[IO] //Environment variable "USERNAME" with default value `getName(defaultUserId)` I don't know enough about how much these methods are used so I can't tell if we should keep them or not but note that despite being unable to be introspected, they would still work.
There would still be a little breaking change with |
It might help to look at an example.
I guess there is a decision to be made about
Yes, but only for custom |
I think this is the way. Deprecate old unidirectional map methods and remove them later. |
@Iltotore I noticed you marked this as ready-for-review. I'll try to have a proper look this week. 👍 |
|
||
test("ConfigValue.option.default.fields") { | ||
check( | ||
prop("user.dir").option.default(Some("discarded since option always returns a value")), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would expect this to work similar to how default values currently work, where later defaults override earlier defaults. The option
is treated as .map(_.some).default(None)
, so this is .map(_.some).default(None).default(Some(...))
and the default value is Some(...)
. Maybe I'm missing something here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
.map(_.some).default(None).default(Some(...))
So this code is supposed to return Some(...)
if there is no value passed IIUC?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, exactly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok. So I indeed implemented the wrong behaviour. Currently, configA.default(b).default(x)
will only return configA
's value or b
.
I cannot fix it at the moment but I might be able to work on it during the weekend
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no rush from my side. I still have parts left to review, but great to confirm the behaviours.
If we're going to fit this into 3.7.0, we have to fix the following compatibility issues.
I believe (6) can be ignored since |
We can keep them without any issue except not supporting introspection when |
Launching |
/** | ||
* Equivalent to `[F[_]] =>> Async[F] ?=> F[A]` in Scala 3. | ||
*/ | ||
trait AsyncEvaluation[A] { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems unused, can be removed?
@@ -121,64 +144,37 @@ sealed abstract class ConfigValue[+F[_], A] { | |||
* a composition of values, the default will be used in case all | |||
* values are either missing or are default values themselves. | |||
* | |||
* Using `.default(a)` is equivalent to using `.or(default(a))`. | |||
* Using `.default(a)` is roughly equivalent to using `.or(default(a))`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should still hold from a end user perspective, no?
value: ConfigValue[F, A] | ||
)(f: A => ConfigValue[F, B]): ConfigValue[F, B] = | ||
value.flatMap(f) | ||
implicit final def configValueMonad[F[_]]: Monad[ConfigValue[F, *]] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've avoided providing Monad
so far, because there are two theoretical candidates for pure
(default
and loaded
), thus forcing the user to choose. But I also realise, one most likely want default
as the choice for pure
, which is also what we do in tailRecM
. We should however add the law tests for Monad
.
Edit: I also added back the removed law tests and seems some of them are failing.
@@ -64,10 +68,29 @@ import ciris.ConfigEntry.{Default, Failed, Loaded} | |||
sealed abstract class ConfigValue[+F[_], A] { | |||
private[this] final val self: ConfigValue[F, A] = this | |||
|
|||
final def asIso[B](implicit codec: ConfigCodec[A, B]): ConfigValue[F, B] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm thinking we should rename ConfigValue#asIso
to a different name to avoid as
-> asIso
-> as
migration for end users. Maybe we can rename the internal to
and then use at as the new name for asIso
? So end users only migrate once from as
-> to
.
import scala.concurrent.duration.{Duration, FiniteDuration} | ||
import scala.util.Try | ||
|
||
final class ConfigCodecSpec extends DisciplineSuite with Generators { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be nice to have roundtrip tests for the ConfigCodec
s (so encode
is also tested). Same for the codecs in the integration modules.
} | ||
})(_.value) | ||
|
||
final def fields: List[ConfigField] = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think we should introduce a ConfigFields
type or similar? So we can provide some rendering methods (and control toString
) directly in the returned type?
Edit: I'm thinking we should provide some default renderers (e.g. markdown). But let's leave that for a separate pull request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure about the relevance of a ConfigFieldList
or ConfigFields
wrapper 🤔. What would be the advantage over List[ConfigField]
with function taking it as a parameter e.g renderMarkdown(fields: List[ConfigField])
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it has better discoverability (e.g. config.fields.renderMarkdown
vs. discovering the renderMarkdown
function). And as we add more renderers (or other functionality), it's convenient to find them all through autocomplete on fields.
. Plus we can control ConfigFields#toString
and do something better than List#toString
.
Add
ConfigSpec
, an higher-level equivalent ofConfigValue
that can be compiled toConfigValue
and introspected. This is useful for deriving documentation or default configuration files/maps (e.g for Kubernetes). Example of MD documentation derivation:ConfigSpecMarkdownInterpreter.scala
Output: