From 4898a522fdc32c4be61b3059ba5039b4fde56280 Mon Sep 17 00:00:00 2001 From: Pat Garrity Date: Sun, 23 Mar 2025 10:30:51 -0500 Subject: [PATCH] (minor) Version, Doc, and Minor updates. --- .forgejo/workflows/pull_request.yaml | 1 + .forgejo/workflows/release.yaml | 1 + build.sbt | 13 +++-- project/build.properties | 2 +- project/plugins.sbt | 4 +- .../gs/config/v0/AuditedConfiguration.scala | 54 ++++++++++++++++++- .../gs/config/v0/BaseConfiguration.scala | 2 +- src/main/scala/gs/config/v0/ConfigError.scala | 3 +- src/main/scala/gs/config/v0/ConfigKey.scala | 2 +- src/main/scala/gs/config/v0/ConfigName.scala | 10 ++++ .../gs/config/v0/audit/ConfigManifest.scala | 13 +++++ .../config/v0/audit/ConfigQueryResult.scala | 2 +- .../gs/config/v0/source/ConfigSource.scala | 15 ++++++ .../scala/gs/config/v0/ConfigNameTests.scala | 3 ++ .../v0/audit/AuditedConfigurationTests.scala | 4 +- 15 files changed, 113 insertions(+), 16 deletions(-) diff --git a/.forgejo/workflows/pull_request.yaml b/.forgejo/workflows/pull_request.yaml index a98df61..e7f2b03 100644 --- a/.forgejo/workflows/pull_request.yaml +++ b/.forgejo/workflows/pull_request.yaml @@ -64,5 +64,6 @@ jobs: sbtn coverageOff sbtn clean sbtn compile + sbtn doc sbtn publish fi diff --git a/.forgejo/workflows/release.yaml b/.forgejo/workflows/release.yaml index 72fac7d..70f2047 100644 --- a/.forgejo/workflows/release.yaml +++ b/.forgejo/workflows/release.yaml @@ -71,6 +71,7 @@ jobs: sbtn coverageOff sbtn clean sbtn semVerWriteVersionToFile + sbtn doc sbtn publish fi - name: 'Create Git Tag' diff --git a/build.sbt b/build.sbt index 3c2cec3..fcee4e4 100644 --- a/build.sbt +++ b/build.sbt @@ -1,4 +1,4 @@ -val scala3: String = "3.4.1" +val scala3: String = "3.6.4" externalResolvers := Seq( "Garrity Software Mirror" at "https://maven.garrity.co/releases", @@ -10,13 +10,16 @@ ThisBuild / versionScheme := Some("semver-spec") ThisBuild / gsProjectName := "gs-config" lazy val sharedSettings = Seq( - scalaVersion := scala3, - version := semVerSelected.value + scalaVersion := scala3, + version := semVerSelected.value, + coverageFailOnMinimum := true, + coverageMinimumStmtTotal := 100, + coverageMinimumBranchTotal := 100 ) lazy val testSettings = Seq( libraryDependencies ++= Seq( - "org.scalameta" %% "munit" % "1.0.0-RC1" % Test + "org.scalameta" %% "munit" % "1.1.0" % Test ) ) @@ -27,6 +30,6 @@ lazy val `gs-config` = project .settings(name := s"${gsProjectName.value}-v${semVerMajor.value}") .settings( libraryDependencies ++= Seq( - "org.typelevel" %% "cats-effect" % "3.5.2" + "org.typelevel" %% "cats-effect" % "3.5.7" ) ) diff --git a/project/build.properties b/project/build.properties index 04267b1..cc68b53 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.9 +sbt.version=1.10.11 diff --git a/project/plugins.sbt b/project/plugins.sbt index e897854..cac78e0 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -28,6 +28,6 @@ externalResolvers := Seq( "Garrity Software Releases" at "https://maven.garrity.co/gs" ) -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") -addSbtPlugin("gs" % "sbt-garrity-software" % "0.3.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1") +addSbtPlugin("gs" % "sbt-garrity-software" % "0.5.0") addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0") diff --git a/src/main/scala/gs/config/v0/AuditedConfiguration.scala b/src/main/scala/gs/config/v0/AuditedConfiguration.scala index 20f3bd9..e3f27ce 100644 --- a/src/main/scala/gs/config/v0/AuditedConfiguration.scala +++ b/src/main/scala/gs/config/v0/AuditedConfiguration.scala @@ -9,7 +9,7 @@ import gs.config.v0.source.ConfigSource import gs.config.v0.source.EnvironmentConfigSource import gs.config.v0.source.MemoryConfigSource -/** Implementation of [[gs.config.Configuration]] that tracks every call to +/** Implementation of [[gs.config.v0.Configuration]] that tracks every call to * `getValue` and reports on whether the query succeeded or failed. * * @param sources @@ -23,6 +23,8 @@ final class AuditedConfiguration[F[_]: Sync]( ) extends BaseConfiguration[F]: import AuditedConfiguration.Acc + /** @inheritDocs + */ override def getValue[A: Configurable]( key: ConfigKey[A] ): F[Either[ConfigError, A]] = @@ -93,7 +95,8 @@ object AuditedConfiguration: * sources. * * @param sources - * The list of [[ConfigSource]] backing this configuration. + * The list of [[gs.config.v0.source.ConfigSource]] backing this + * configuration. * @return * The new [[Configuration]] instance. */ @@ -109,26 +112,73 @@ object AuditedConfiguration: ) ) + /** Start building a new [[AuditedConfiguration]] for some source. + * + * @param source + * The [[gs.config.v0.source.ConfigSource]] backing this configuration. + * @return + * New builder based on the given source. + */ def forSource[F[_]: Sync](source: ConfigSource[F]): Builder[F] = Builder[F](NonEmptyList.of(source)) + /** Start building a new [[AuditedConfiguration]] based on the environment. + * + * @return + * New builder based on the environment. + */ def forEnvironmentSource[F[_]: Sync]: Builder[F] = Builder[F](NonEmptyList.of(new EnvironmentConfigSource[F])) + /** Start building a new [[AuditedConfiguration]] based on the given in-memory + * map of values. + * + * @param configs + * The configuration map. + * @return + * New builder based on the config map. + */ def forMemorySource[F[_]: Sync](configs: Map[String, String]): Builder[F] = Builder[F](NonEmptyList.of(new MemoryConfigSource[F](configs))) + /** Builder for [[AuditedConfiguration]]. + * + * @param sources + * The (non-empty) list of sources for the eventual configuration. + */ case class Builder[F[_]: Sync](sources: NonEmptyList[ConfigSource[F]]): + /** Add the environment as a source to this builder. + * + * @return + * This builder. + */ def withEnvironmentSource(): Builder[F] = copy(sources = this.sources.append(new EnvironmentConfigSource[F])) + /** Add a memory map as a source to this builder. + * + * @param configs + * The configuration map. + * @return + * This builder. + */ def withMemorySource(configs: Map[String, String]): Builder[F] = copy(sources = this.sources.append(new MemoryConfigSource[F](configs))) + /** Add the given source to this builder. + * + * @param source + * The [[gs.config.v0.source.ConfigSource]] to add. + * @return + * This builder. + */ def withSource(source: ConfigSource[F]): Builder[F] = copy(sources = this.sources.append(source)) + /** @return + * A new [[AuditedConfiguration]] based on the configured sources. + */ def build(): F[AuditedConfiguration[F]] = ConfigManifest .standard[F] diff --git a/src/main/scala/gs/config/v0/BaseConfiguration.scala b/src/main/scala/gs/config/v0/BaseConfiguration.scala index d5dab6d..99662b1 100644 --- a/src/main/scala/gs/config/v0/BaseConfiguration.scala +++ b/src/main/scala/gs/config/v0/BaseConfiguration.scala @@ -11,7 +11,7 @@ abstract class BaseConfiguration[F[_]] extends Configuration[F]: ): Either[ConfigError, A] = key match case ConfigKey.WithDefaultValue(_, defaultValue) => - Right(defaultValue) + Right(defaultValue()) case _ => Left(ConfigError.MissingValue(key.name)) diff --git a/src/main/scala/gs/config/v0/ConfigError.scala b/src/main/scala/gs/config/v0/ConfigError.scala index bd9154c..0a330b9 100644 --- a/src/main/scala/gs/config/v0/ConfigError.scala +++ b/src/main/scala/gs/config/v0/ConfigError.scala @@ -24,7 +24,8 @@ object ConfigError: * @param candidateValue * The raw value that could not be parsed. * @param source - * The [[ConfigSource]] which provided the candidate value. + * The name of the [[gs.config.v0.source.ConfigSource]] which provided the + * candidate value. */ case class CannotParseValue( configName: ConfigName, diff --git a/src/main/scala/gs/config/v0/ConfigKey.scala b/src/main/scala/gs/config/v0/ConfigKey.scala index 052c087..6f04389 100644 --- a/src/main/scala/gs/config/v0/ConfigKey.scala +++ b/src/main/scala/gs/config/v0/ConfigKey.scala @@ -37,5 +37,5 @@ object ConfigKey: */ case class WithDefaultValue[A: Configurable]( name: ConfigName, - defaultValue: A + defaultValue: () => A ) extends ConfigKey[A] diff --git a/src/main/scala/gs/config/v0/ConfigName.scala b/src/main/scala/gs/config/v0/ConfigName.scala index 582ed0a..21bae85 100644 --- a/src/main/scala/gs/config/v0/ConfigName.scala +++ b/src/main/scala/gs/config/v0/ConfigName.scala @@ -1,5 +1,8 @@ package gs.config.v0 +import cats.Eq +import cats.Show + /** Uniquely names some piece of configuration. This structure does _not_ * attempt to support every possible use case, but supports some common cases * for users that follow some basic rules. Please review conversion functions @@ -32,6 +35,13 @@ object ConfigName: given CanEqual[ConfigName, ConfigName] = CanEqual.derived + given Eq[ConfigName] = ( + x, + y + ) => x == y + + given Show[ConfigName] = _.unwrap() + extension (name: ConfigName) /** Extract the unmodified string that backs this name. * diff --git a/src/main/scala/gs/config/v0/audit/ConfigManifest.scala b/src/main/scala/gs/config/v0/audit/ConfigManifest.scala index 0a85bb4..da04008 100644 --- a/src/main/scala/gs/config/v0/audit/ConfigManifest.scala +++ b/src/main/scala/gs/config/v0/audit/ConfigManifest.scala @@ -5,6 +5,15 @@ import cats.effect.Sync import cats.syntax.all.* import gs.config.v0.ConfigName +/** A `ConfigManifest` tracks all queries for individual pieces of + * configuration. It is used in conjunction with an [[AuditedConfiguration]]. + * Manifests can be queried by producing a _snapshot_ of interactions so far. + * + * See: + * + * - [[AuditedConfiguration]] + * - [[ConfigQueryResult]] + */ trait ConfigManifest[F[_]]: /** Retrieve a snapshot of the current state of this configuration manifest. * This state tracks all configuration names that the caller attempted to @@ -46,9 +55,13 @@ object ConfigManifest: private val manifest: Ref[F, Map[ConfigName, List[ConfigQueryResult]]] ) extends ConfigManifest[F]: + /** @inheritDocs + */ override def snapshot(): F[Map[ConfigName, List[ConfigQueryResult]]] = manifest.get + /** @inheritDocs + */ override def record( name: ConfigName, query: ConfigQueryResult diff --git a/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala b/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala index 7f91811..a14048c 100644 --- a/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala +++ b/src/main/scala/gs/config/v0/audit/ConfigQueryResult.scala @@ -14,7 +14,7 @@ object ConfigQueryResult: /** Represents a query for some configuration that completed successfully. * * @param source - * The source which provided the value. + * The name of the source which provided the value. * @param rawValue * The raw value that the source returned. */ diff --git a/src/main/scala/gs/config/v0/source/ConfigSource.scala b/src/main/scala/gs/config/v0/source/ConfigSource.scala index 5b713f9..9e228b2 100644 --- a/src/main/scala/gs/config/v0/source/ConfigSource.scala +++ b/src/main/scala/gs/config/v0/source/ConfigSource.scala @@ -27,17 +27,32 @@ trait ConfigSource[F[_]]: object ConfigSource: + /** Instantiate a new [[MemoryConfigSource]] based on the given map. + * + * @param configs + * The config map that should back the source. + * @return + * The new [[ConfigSource]]. + */ def inMemory[F[_]: Applicative]( configs: Map[String, String] ): ConfigSource[F] = new MemoryConfigSource[F](configs) + /** @return + * A new [[EnvironmentConfigSource]]. + */ def environment[F[_]: Sync]: ConfigSource[F] = new EnvironmentConfigSource[F] + /** @return + * An empty [[ConfigSource]]. + */ def empty[F[_]: Applicative]: ConfigSource[F] = new Empty[F] + /** Default [[ConfigSource]] implementation that never returns a value. + */ final class Empty[F[_]: Applicative] extends ConfigSource[F]: /** @inheritDocs diff --git a/src/test/scala/gs/config/v0/ConfigNameTests.scala b/src/test/scala/gs/config/v0/ConfigNameTests.scala index 42b4da6..0a2449c 100644 --- a/src/test/scala/gs/config/v0/ConfigNameTests.scala +++ b/src/test/scala/gs/config/v0/ConfigNameTests.scala @@ -1,5 +1,7 @@ package gs.config.v0 +import cats.syntax.all.* + class ConfigNameTests extends munit.FunSuite: test("should express some name as an environment variable") { @@ -14,6 +16,7 @@ class ConfigNameTests extends munit.FunSuite: val expected = raw val name = ConfigName(raw) assertEquals(name.unwrap(), expected) + assertEquals(name.show, expected) } test("should support equality") { diff --git a/src/test/scala/gs/config/v0/audit/AuditedConfigurationTests.scala b/src/test/scala/gs/config/v0/audit/AuditedConfigurationTests.scala index 7158746..38211b6 100644 --- a/src/test/scala/gs/config/v0/audit/AuditedConfigurationTests.scala +++ b/src/test/scala/gs/config/v0/audit/AuditedConfigurationTests.scala @@ -143,7 +143,7 @@ class AuditedConfigurationTests extends GsSuite: iotest("should audit the use of default values") { val name = Names.KString - val defaultValue = "value" + val defaultValue = () => "value" val key = ConfigKey.WithDefaultValue(name, defaultValue) for config <- Configuration @@ -153,7 +153,7 @@ class AuditedConfigurationTests extends GsSuite: value <- config.getValue(key) manifest <- config.manifest.snapshot() yield - assertEquals(value, Right(defaultValue)) + assertEquals(value, Right(defaultValue())) assertEquals( manifest.get(name), Some(