diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 4489a18..fbcf79c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,6 +11,6 @@ repos: description: Enforces using only 'LF' line endings. - id: trailing-whitespace - repo: https://git.garrity.co/garrity-software/gs-pre-commit-scala - rev: v1.0.0 + rev: v1.0.1 hooks: - id: scalafmt diff --git a/.scalafmt.conf b/.scalafmt.conf index 80e6cc9..9c7929b 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,5 +1,5 @@ // See: https://github.com/scalameta/scalafmt/tags for the latest tags. -version = 3.7.17 +version = 3.8.1 runner.dialect = scala3 maxColumn = 80 diff --git a/build.sbt b/build.sbt index bff7aca..3c2cec3 100644 --- a/build.sbt +++ b/build.sbt @@ -16,7 +16,7 @@ lazy val sharedSettings = Seq( lazy val testSettings = Seq( libraryDependencies ++= Seq( - "org.scalameta" %% "munit" % "1.0.0-M10" % Test + "org.scalameta" %% "munit" % "1.0.0-RC1" % Test ) ) diff --git a/src/main/scala/gs/config/v0/AuditedConfiguration.scala b/src/main/scala/gs/config/v0/AuditedConfiguration.scala index 6835487..8589c50 100644 --- a/src/main/scala/gs/config/v0/AuditedConfiguration.scala +++ b/src/main/scala/gs/config/v0/AuditedConfiguration.scala @@ -89,7 +89,7 @@ object AuditedConfiguration: */ def forSources[F[_]: Sync]( sources: NonEmptyList[ConfigSource[F]] - ): F[Configuration[F]] = + ): F[AuditedConfiguration[F]] = ConfigManifest .standard[F] .map(manifest => @@ -119,7 +119,7 @@ object AuditedConfiguration: def withSource(source: ConfigSource[F]): Builder[F] = copy(sources = this.sources.append(source)) - def build(): F[Configuration[F]] = + def build(): F[AuditedConfiguration[F]] = ConfigManifest .standard[F] .map(manifest => diff --git a/src/main/scala/gs/config/v0/ConfigError.scala b/src/main/scala/gs/config/v0/ConfigError.scala index 887660b..7c942ce 100644 --- a/src/main/scala/gs/config/v0/ConfigError.scala +++ b/src/main/scala/gs/config/v0/ConfigError.scala @@ -6,6 +6,8 @@ package gs.config.v0 sealed trait ConfigError object ConfigError: + given CanEqual[ConfigError, ConfigError] = CanEqual.derived + /** Attempted to retreive the value for some [[ConfigKey]], but no value could * be found. * diff --git a/src/main/scala/gs/config/v0/Configurable.scala b/src/main/scala/gs/config/v0/Configurable.scala index 1a3489b..e9c1070 100644 --- a/src/main/scala/gs/config/v0/Configurable.scala +++ b/src/main/scala/gs/config/v0/Configurable.scala @@ -24,23 +24,33 @@ object Configurable: ): Configurable[A] = C given Configurable[String] with + /** @inheritDocs + */ def parse(raw: String): Option[String] = Some(raw) given Configurable[Int] with + /** @inheritDocs + */ def parse(raw: String): Option[Int] = Try(raw.toInt).toOption given Configurable[Long] with + /** @inheritDocs + */ def parse(raw: String): Option[Long] = Try(raw.toLong).toOption given Configurable[Boolean] with + /** @inheritDocs + */ def parse(raw: String): Option[Boolean] = Try(raw.toBoolean).toOption given Configurable[LocalDate] with + /** @inheritDocs + */ def parse(raw: String): Option[LocalDate] = Try(LocalDate.parse(raw)).toOption given Configurable[Instant] with - - def parse(raw: String): Option[Instant] = - Try(Instant.parse(raw)).toOption + /** @inheritDocs + */ + def parse(raw: String): Option[Instant] = Try(Instant.parse(raw)).toOption diff --git a/src/main/scala/gs/config/v0/Configuration.scala b/src/main/scala/gs/config/v0/Configuration.scala index 486e2fd..bfa559c 100644 --- a/src/main/scala/gs/config/v0/Configuration.scala +++ b/src/main/scala/gs/config/v0/Configuration.scala @@ -1,5 +1,6 @@ package gs.config.v0 +import cats.data.EitherT import cats.effect.Sync import gs.config.v0.source.ConfigSource @@ -16,6 +17,19 @@ trait Configuration[F[_]]: */ def getValue[A: Configurable](key: ConfigKey[A]): F[Either[ConfigError, A]] + /** Retrieve a value based on some key. Return an `EitherT` as the response, + * rather than `F[Either[ConfigError, A]]` + * + * @param key + * The key that identifies the piece of configuration to retrieve. + * @return + * The value, or an error if no value is present. Expressed as an + * `EitherT`. + */ + def getValueT[A: Configurable]( + key: ConfigKey[A] + ): EitherT[F, ConfigError, A] = EitherT(getValue(key)) + object Configuration: /** Start building a new [[AuditedConfiguration]]. @@ -34,7 +48,7 @@ object Configuration: * @return * The new [[Configuration]]. */ - def auditedEnvironmentOnly[F[_]: Sync]: F[Configuration[F]] = + def auditedEnvironmentOnly[F[_]: Sync]: F[AuditedConfiguration[F]] = AuditedConfiguration.forEnvironmentSource[F].build() end Configuration diff --git a/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala b/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala index 8be2199..a78f24f 100644 --- a/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala +++ b/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala @@ -9,6 +9,8 @@ sealed trait ConfigQueryResult object ConfigQueryResult: + given CanEqual[ConfigQueryResult, ConfigQueryResult] = CanEqual.derived + /** Represents a query for some configuration that completed successfully. * * @param source diff --git a/src/main/scala/gs/config/v0/source/ConfigSource.scala b/src/main/scala/gs/config/v0/source/ConfigSource.scala index ecf0be2..5b713f9 100644 --- a/src/main/scala/gs/config/v0/source/ConfigSource.scala +++ b/src/main/scala/gs/config/v0/source/ConfigSource.scala @@ -40,9 +40,13 @@ object ConfigSource: final class Empty[F[_]: Applicative] extends ConfigSource[F]: + /** @inheritDocs + */ override def getValue(key: ConfigKey[?]): F[Option[String]] = Applicative[F].pure(None) + /** @inheritDocs + */ override val name: String = "empty" end ConfigSource diff --git a/src/main/scala/gs/config/v0/source/EnvironmentConfigSource.scala b/src/main/scala/gs/config/v0/source/EnvironmentConfigSource.scala index 8d21adc..e0dd5a9 100644 --- a/src/main/scala/gs/config/v0/source/EnvironmentConfigSource.scala +++ b/src/main/scala/gs/config/v0/source/EnvironmentConfigSource.scala @@ -8,9 +8,13 @@ import gs.config.v0.ConfigKey */ final class EnvironmentConfigSource[F[_]: Sync] extends ConfigSource[F]: + /** @inheritDocs + */ override def getValue( key: ConfigKey[?] ): F[Option[String]] = Sync[F].delay(sys.env.get(key.name.toEnvironmentVariable())) + /** @inheritDocs + */ override val name: String = "environment" diff --git a/src/main/scala/gs/config/v0/source/MemoryConfigSource.scala b/src/main/scala/gs/config/v0/source/MemoryConfigSource.scala index 2e9c73e..29d6c94 100644 --- a/src/main/scala/gs/config/v0/source/MemoryConfigSource.scala +++ b/src/main/scala/gs/config/v0/source/MemoryConfigSource.scala @@ -16,6 +16,8 @@ final class MemoryConfigSource[F[_]: Applicative]( ) extends ConfigSource[F]: val id: UUID = UUID.randomUUID() + /** @inheritDocs + */ override def getValue( key: ConfigKey[?] ): F[Option[String]] = @@ -24,4 +26,6 @@ final class MemoryConfigSource[F[_]: Applicative]( .get(key.name.toRawString()) ) + /** @inheritDocs + */ override lazy val name: String = s"in-memory-$id" diff --git a/src/test/scala/gs/config/v0/GsSuite.scala b/src/test/scala/gs/config/v0/GsSuite.scala new file mode 100644 index 0000000..aa09088 --- /dev/null +++ b/src/test/scala/gs/config/v0/GsSuite.scala @@ -0,0 +1,17 @@ +package gs.config.v0 + +import cats.effect.IO +import cats.effect.unsafe.IORuntime + +abstract class GsSuite extends munit.FunSuite: + given IORuntime = IORuntime.global + + def iotest( + name: String + )( + body: => IO[Any] + )( + implicit + loc: munit.Location + ): Unit = + test(new munit.TestOptions(name))(body.unsafeRunSync()) diff --git a/src/test/scala/gs/config/v0/audit/AuditedConfigurationTests.scala b/src/test/scala/gs/config/v0/audit/AuditedConfigurationTests.scala new file mode 100644 index 0000000..586b73d --- /dev/null +++ b/src/test/scala/gs/config/v0/audit/AuditedConfigurationTests.scala @@ -0,0 +1,51 @@ +package gs.config.v0.audit + +import cats.effect.IO +import gs.config.v0.ConfigError +import gs.config.v0.ConfigKey +import gs.config.v0.ConfigName +import gs.config.v0.Configuration +import gs.config.v0.GsSuite +import gs.config.v0.source.ConfigSource + +class AuditedConfigurationTests extends GsSuite: + import AuditedConfigurationTests.* + + iotest( + "should not return values, but should record attempts to find, when no config exists" + ) { + for + config <- Configuration + .audited(ConfigSource.inMemory[IO](Map.empty)) + .build() + string <- config.getValue(Keys.KString) + manifest <- config.manifest.snapshot() + yield + assert(string == Left(ConfigError.MissingValue(Names.KString))) + assert( + manifest.get(Names.KString) == Some( + List( + ConfigQueryResult.Failure( + sources = List(config.sources.head.name), + error = ConfigError.MissingValue(Names.KString) + ) + ) + ) + ) + } + +object AuditedConfigurationTests: + + object Names: + + val KString: ConfigName = ConfigName("string") + + end Names + + object Keys: + + val KString: ConfigKey[String] = ConfigKey.Required[String](Names.KString) + + end Keys + +end AuditedConfigurationTests