Compare commits

...

1 commit
0.1.1 ... main

Author SHA1 Message Date
e44334b364 (minor) Version, Doc, and Minor updates. (#2)
All checks were successful
/ Build and Release Library (push) Successful in 2m11s
Reviewed-on: #2
2025-03-23 15:34:25 +00:00
15 changed files with 113 additions and 16 deletions

View file

@ -64,5 +64,6 @@ jobs:
sbtn coverageOff sbtn coverageOff
sbtn clean sbtn clean
sbtn compile sbtn compile
sbtn doc
sbtn publish sbtn publish
fi fi

View file

@ -71,6 +71,7 @@ jobs:
sbtn coverageOff sbtn coverageOff
sbtn clean sbtn clean
sbtn semVerWriteVersionToFile sbtn semVerWriteVersionToFile
sbtn doc
sbtn publish sbtn publish
fi fi
- name: 'Create Git Tag' - name: 'Create Git Tag'

View file

@ -1,4 +1,4 @@
val scala3: String = "3.4.1" val scala3: String = "3.6.4"
externalResolvers := Seq( externalResolvers := Seq(
"Garrity Software Mirror" at "https://maven.garrity.co/releases", "Garrity Software Mirror" at "https://maven.garrity.co/releases",
@ -10,13 +10,16 @@ ThisBuild / versionScheme := Some("semver-spec")
ThisBuild / gsProjectName := "gs-config" ThisBuild / gsProjectName := "gs-config"
lazy val sharedSettings = Seq( lazy val sharedSettings = Seq(
scalaVersion := scala3, scalaVersion := scala3,
version := semVerSelected.value version := semVerSelected.value,
coverageFailOnMinimum := true,
coverageMinimumStmtTotal := 100,
coverageMinimumBranchTotal := 100
) )
lazy val testSettings = Seq( lazy val testSettings = Seq(
libraryDependencies ++= 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(name := s"${gsProjectName.value}-v${semVerMajor.value}")
.settings( .settings(
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.typelevel" %% "cats-effect" % "3.5.2" "org.typelevel" %% "cats-effect" % "3.5.7"
) )
) )

View file

@ -1 +1 @@
sbt.version=1.9.9 sbt.version=1.10.11

View file

@ -28,6 +28,6 @@ externalResolvers := Seq(
"Garrity Software Releases" at "https://maven.garrity.co/gs" "Garrity Software Releases" at "https://maven.garrity.co/gs"
) )
addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.11") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.3.1")
addSbtPlugin("gs" % "sbt-garrity-software" % "0.3.0") addSbtPlugin("gs" % "sbt-garrity-software" % "0.5.0")
addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0") addSbtPlugin("gs" % "sbt-gs-semver" % "0.3.0")

View file

@ -9,7 +9,7 @@ import gs.config.v0.source.ConfigSource
import gs.config.v0.source.EnvironmentConfigSource import gs.config.v0.source.EnvironmentConfigSource
import gs.config.v0.source.MemoryConfigSource 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. * `getValue` and reports on whether the query succeeded or failed.
* *
* @param sources * @param sources
@ -23,6 +23,8 @@ final class AuditedConfiguration[F[_]: Sync](
) extends BaseConfiguration[F]: ) extends BaseConfiguration[F]:
import AuditedConfiguration.Acc import AuditedConfiguration.Acc
/** @inheritDocs
*/
override def getValue[A: Configurable]( override def getValue[A: Configurable](
key: ConfigKey[A] key: ConfigKey[A]
): F[Either[ConfigError, A]] = ): F[Either[ConfigError, A]] =
@ -93,7 +95,8 @@ object AuditedConfiguration:
* sources. * sources.
* *
* @param sources * @param sources
* The list of [[ConfigSource]] backing this configuration. * The list of [[gs.config.v0.source.ConfigSource]] backing this
* configuration.
* @return * @return
* The new [[Configuration]] instance. * 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] = def forSource[F[_]: Sync](source: ConfigSource[F]): Builder[F] =
Builder[F](NonEmptyList.of(source)) 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] = def forEnvironmentSource[F[_]: Sync]: Builder[F] =
Builder[F](NonEmptyList.of(new EnvironmentConfigSource[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] = def forMemorySource[F[_]: Sync](configs: Map[String, String]): Builder[F] =
Builder[F](NonEmptyList.of(new MemoryConfigSource[F](configs))) 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]]): 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] = def withEnvironmentSource(): Builder[F] =
copy(sources = this.sources.append(new EnvironmentConfigSource[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] = def withMemorySource(configs: Map[String, String]): Builder[F] =
copy(sources = this.sources.append(new MemoryConfigSource[F](configs))) 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] = def withSource(source: ConfigSource[F]): Builder[F] =
copy(sources = this.sources.append(source)) copy(sources = this.sources.append(source))
/** @return
* A new [[AuditedConfiguration]] based on the configured sources.
*/
def build(): F[AuditedConfiguration[F]] = def build(): F[AuditedConfiguration[F]] =
ConfigManifest ConfigManifest
.standard[F] .standard[F]

View file

@ -11,7 +11,7 @@ abstract class BaseConfiguration[F[_]] extends Configuration[F]:
): Either[ConfigError, A] = ): Either[ConfigError, A] =
key match key match
case ConfigKey.WithDefaultValue(_, defaultValue) => case ConfigKey.WithDefaultValue(_, defaultValue) =>
Right(defaultValue) Right(defaultValue())
case _ => case _ =>
Left(ConfigError.MissingValue(key.name)) Left(ConfigError.MissingValue(key.name))

View file

@ -24,7 +24,8 @@ object ConfigError:
* @param candidateValue * @param candidateValue
* The raw value that could not be parsed. * The raw value that could not be parsed.
* @param source * @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( case class CannotParseValue(
configName: ConfigName, configName: ConfigName,

View file

@ -37,5 +37,5 @@ object ConfigKey:
*/ */
case class WithDefaultValue[A: Configurable]( case class WithDefaultValue[A: Configurable](
name: ConfigName, name: ConfigName,
defaultValue: A defaultValue: () => A
) extends ConfigKey[A] ) extends ConfigKey[A]

View file

@ -1,5 +1,8 @@
package gs.config.v0 package gs.config.v0
import cats.Eq
import cats.Show
/** Uniquely names some piece of configuration. This structure does _not_ /** Uniquely names some piece of configuration. This structure does _not_
* attempt to support every possible use case, but supports some common cases * attempt to support every possible use case, but supports some common cases
* for users that follow some basic rules. Please review conversion functions * for users that follow some basic rules. Please review conversion functions
@ -32,6 +35,13 @@ object ConfigName:
given CanEqual[ConfigName, ConfigName] = CanEqual.derived given CanEqual[ConfigName, ConfigName] = CanEqual.derived
given Eq[ConfigName] = (
x,
y
) => x == y
given Show[ConfigName] = _.unwrap()
extension (name: ConfigName) extension (name: ConfigName)
/** Extract the unmodified string that backs this name. /** Extract the unmodified string that backs this name.
* *

View file

@ -5,6 +5,15 @@ import cats.effect.Sync
import cats.syntax.all.* import cats.syntax.all.*
import gs.config.v0.ConfigName 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[_]]: trait ConfigManifest[F[_]]:
/** Retrieve a snapshot of the current state of this configuration manifest. /** Retrieve a snapshot of the current state of this configuration manifest.
* This state tracks all configuration names that the caller attempted to * 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]]] private val manifest: Ref[F, Map[ConfigName, List[ConfigQueryResult]]]
) extends ConfigManifest[F]: ) extends ConfigManifest[F]:
/** @inheritDocs
*/
override def snapshot(): F[Map[ConfigName, List[ConfigQueryResult]]] = override def snapshot(): F[Map[ConfigName, List[ConfigQueryResult]]] =
manifest.get manifest.get
/** @inheritDocs
*/
override def record( override def record(
name: ConfigName, name: ConfigName,
query: ConfigQueryResult query: ConfigQueryResult

View file

@ -14,7 +14,7 @@ object ConfigQueryResult:
/** Represents a query for some configuration that completed successfully. /** Represents a query for some configuration that completed successfully.
* *
* @param source * @param source
* The source which provided the value. * The name of the source which provided the value.
* @param rawValue * @param rawValue
* The raw value that the source returned. * The raw value that the source returned.
*/ */

View file

@ -27,17 +27,32 @@ trait ConfigSource[F[_]]:
object ConfigSource: 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]( def inMemory[F[_]: Applicative](
configs: Map[String, String] configs: Map[String, String]
): ConfigSource[F] = ): ConfigSource[F] =
new MemoryConfigSource[F](configs) new MemoryConfigSource[F](configs)
/** @return
* A new [[EnvironmentConfigSource]].
*/
def environment[F[_]: Sync]: ConfigSource[F] = def environment[F[_]: Sync]: ConfigSource[F] =
new EnvironmentConfigSource[F] new EnvironmentConfigSource[F]
/** @return
* An empty [[ConfigSource]].
*/
def empty[F[_]: Applicative]: ConfigSource[F] = def empty[F[_]: Applicative]: ConfigSource[F] =
new Empty[F] new Empty[F]
/** Default [[ConfigSource]] implementation that never returns a value.
*/
final class Empty[F[_]: Applicative] extends ConfigSource[F]: final class Empty[F[_]: Applicative] extends ConfigSource[F]:
/** @inheritDocs /** @inheritDocs

View file

@ -1,5 +1,7 @@
package gs.config.v0 package gs.config.v0
import cats.syntax.all.*
class ConfigNameTests extends munit.FunSuite: class ConfigNameTests extends munit.FunSuite:
test("should express some name as an environment variable") { test("should express some name as an environment variable") {
@ -14,6 +16,7 @@ class ConfigNameTests extends munit.FunSuite:
val expected = raw val expected = raw
val name = ConfigName(raw) val name = ConfigName(raw)
assertEquals(name.unwrap(), expected) assertEquals(name.unwrap(), expected)
assertEquals(name.show, expected)
} }
test("should support equality") { test("should support equality") {

View file

@ -143,7 +143,7 @@ class AuditedConfigurationTests extends GsSuite:
iotest("should audit the use of default values") { iotest("should audit the use of default values") {
val name = Names.KString val name = Names.KString
val defaultValue = "value" val defaultValue = () => "value"
val key = ConfigKey.WithDefaultValue(name, defaultValue) val key = ConfigKey.WithDefaultValue(name, defaultValue)
for for
config <- Configuration config <- Configuration
@ -153,7 +153,7 @@ class AuditedConfigurationTests extends GsSuite:
value <- config.getValue(key) value <- config.getValue(key)
manifest <- config.manifest.snapshot() manifest <- config.manifest.snapshot()
yield yield
assertEquals(value, Right(defaultValue)) assertEquals(value, Right(defaultValue()))
assertEquals( assertEquals(
manifest.get(name), manifest.get(name),
Some( Some(