Ciris Configuration Library

repository·main·Indexed 19 days ago

https://github.com/vlovgr/ciris

A functional, lightweight, and type-safe configuration loading library for Scala. Part of the Typelevel ecosystem, Ciris allows loading and composing configurations from sources like environment variables, system properties, and files. It leverages Cats and Cats Effect for composition using flatMap and parMapN, and provides specialized modules for integration with circe, circe-yaml, enumeratum, http4s, refined, squants, and AWS SSM Parameter Store.

Tokens
9.8K
Snippets
37
Records
42
Agent score
65%

What's inside Ciris

  1. Overview of Ciris features

    main

    Ciris is a functional programming library designed for loading configurations. Key capabilities include:

    • Multi-source loading: Load values from multiple sources and define default values.
    • Composition: Compose multiple configuration values into larger, complex configurations.
    • Security: Prevent secret values from being exposed by redacting sensitive errors.
    • Decoding: Decode configuration values into various commonly used types.
    • Error Accumulation: Accumulate errors when multiple configuration values fail to load, rather than failing on the first error.
  2. Overview of Ciris

    main
    Ciris is a functional, lightweight, and composable configuration loading library for Scala. It is part of the Typelevel ecosystem and is designed to provide a type-safe way to load and compose configurations.
  3. What is ConfigValue and how to use it

    main

    In Ciris, ConfigValue is the central abstraction representing a single configuration value or a composition of multiple values. You create them using source-specific functions like env (environment variables), file (file contents), or prop (system properties).

    To use a ConfigValue, you typically decode it using .as[T] and then load it into an effect type (like IO) using .load[F] or .attempt[F].

    import ciris._
    import cats.effect.IO
    
    // Define a configuration value
    val port: ConfigValue[Effect, Int] =
      env("API_PORT").or(prop("api.port")).as[Int]
    
    // Load it into a concrete effect
    // apiConfig.load[IO] returns IO[Int]
    // apiConfig.attempt[IO] returns IO[Either[ConfigError, Int]]
  4. Integrate Refined types with Ciris

    main

    Ciris supports refined types, allowing you to enforce complex validation logic (like regex matches or size constraints) during the configuration loading phase. If the value does not satisfy the refinement, the loading process will fail with an error.

    import ciris._
    import eu.timepit.refined.api.Refined
    import eu.timepit.refined.string.MatchesRegex
    import eu.timepit.refined.collection.MinSize
    
    // Define refined types
    type ApiKey = String Refined MatchesRegex["[a-zA-Z0-9]{25,40}"]
    type DatabasePassword = String Refined MinSize[30]
    
    // Use them in loaders
    val apiKeyLoader = env("API_KEY").as[ApiKey].secret
    val dbPassLoader = env("DB_PASS").as[DatabasePassword].secret
  5. Distinguish between .or and .alt for configuration fallbacks

    main

    When providing alternative configuration sets (e.g., switching between DevConfig and ProdConfig), choose based on how much of the first configuration was successfully loaded:

    • .or(alternative): Only attempts the alternative if the first configuration is entirely missing. If the first configuration is partially loaded (some keys found, others missing), .or will fail and not try the alternative.
    • .alt(alternative): Attempts the alternative even if the first configuration was partially loaded. This is useful for environment-based switching where one environment might define a subset of keys.
    // If API_PORT is found but API_KEY is missing, dev.or(prod) will FAIL.
    // If API_PORT is found but API_KEY is missing, dev.alt(prod) will attempt to load prod.
    val config = dev.alt(prod)
  6. Integrate Enumeratum enums with Ciris

    main

    You can load configuration values directly into enumeratum enums by making the enum object extend CirisEnum[T]. This allows you to use .as[YourEnum] to parse environment variables or properties into your sealed trait hierarchy.

    import enumeratum.{CirisEnum, Enum, EnumEntry}
    import ciris._
    
    sealed trait AppEnvironment extends EnumEntry
    
    object AppEnvironment extends Enum[AppEnvironment] with CirisEnum[AppEnvironment] {
      case object Local extends AppEnvironment
      case object Testing extends AppEnvironment
      case object Production extends AppEnvironment
    
      val values = findValues
    }
    
    // Usage
    val envConfig: ConfigValue[Effect, AppEnvironment] = env("APP_ENV").as[AppEnvironment]
  7. Handle sensitive data with Secret and Redacted

    main

    When dealing with sensitive information (like API keys), use these mechanisms to prevent accidental exposure in logs or error messages:

    • .secret: Wraps the value in a Secret[T]. When Secret is shown (e.g., in logs), it displays only the first 7 characters of its SHA-1 hash. It also redacts the actual value from error messages.
    • .redacted: Redacts sensitive details from error messages but does not wrap the value in a Secret type. Use this if you want to keep the raw type but prevent it from appearing in error strings.
    // Wraps the value in Secret[String]
    val apiKey: ConfigValue[Effect, Secret[String]] = env("API_KEY").secret
    
    // Redacts from errors but keeps the type as String
    val password: ConfigValue[Effect, String] = env("PASSWORD").redacted
  8. Migrate from Ciris v0.x to v1.0.0

    main

    Ciris v1.0.0 is a complete rewrite based on Cats and Cats Effect. It is no longer available on Scala Native.

    Key changes include:

    • Effect Types: Instead of using effectful versions of every source (like envF), use the standard env, prop, or file and specify the effect type at the end of the chain using .load[F]().
    • Decoding: Decoding is now decoupled from the source. Use .as[T] on a ConfigValue to decode a value to type T.
    • Composition: Use Cats-style combinators like flatMap and parMapN for composing configurations instead of loadConfig or withValues.
    • Simplified Concepts: ConfigEntry, ConfigValue, and ConfigResult have been unified into a single ConfigValue concept.
  9. Use fallbacks and defaults in configurations

    main

    Ciris provides several ways to handle missing values:

    1. .or(fallback): Uses the fallback ConfigValue only if the primary one is missing.
    2. .default(value): Provides a constant value if the configuration is missing.
    3. .option: Wraps the result in an Option, returning None if the value is missing.
    4. .default { ... }: Provides a default for a whole composition (e.g., a case class) if all its components are missing.

    Note: In a composition, a .default is only triggered if the entire composition fails to load.

    // 1. Fallback to a system property if env var is missing
    val port = env("API_PORT").or(prop("api.port")).as[Int]
    
    // 2. Use a constant default
    val timeout = env("API_TIMEOUT").as[Duration].default(10.seconds)
    
    // 3. Default for a whole composition
    val apiConfig = ( 
      env("API_PORT").as[Int], 
      env("API_TIMEOUT").as[Duration].option 
    ).parMapN(ApiConfig).default(ApiConfig(3000, 10.seconds.some))
  10. Add Ciris extension modules

    main

    Ciris provides several additional modules for integration with popular Scala libraries. Add the corresponding dependency to your build.sbt to enable these features.

    // circe support
    libraryDependencies += "@ORGANIZATION" %% "@CIRCE_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // circe-yaml support
    libraryDependencies += "@ORGANIZATION" %% "@CIRCE_YAML_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // enumeratum support
    libraryDependencies += "@ORGANIZATION" %% "@ENUMERATUM_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // http4s support
    libraryDependencies += "@ORGANIZATION" %% "@HTTP4S_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // http4s-aws support
    libraryDependencies += "@ORGANIZATION" %% "@HTTP4SAWS_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // refined support
    libraryDependencies += "@ORGANIZATION" %% "@REFINED_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // squants support
    libraryDependencies += "@ORGANIZATION" %% "@SQUANTS_MODULE_NAME@" % "@LATEST_VERSION@"
  11. Read decrypted values from AWS SSM Parameter Store

    main

    The http4s-aws module allows reading decrypted values from the AWS Systems Manager (SSM) Parameter Store using http4s-aws.

    Note that parameter values retrieved via AwsSsmParameters are wrapped in Secret by default to handle sensitive data.

    import cats.effect.IO
    import cats.effect.IOApp
    import ciris._
    import ciris.http4s.aws.AwsSsmParameters
    import com.magine.aws.Region
    import com.magine.http4s.aws.CredentialsProvider
    import org.http4s.ember.client.EmberClientBuilder
    
    object Main extends IOApp.Simple {
      case class Config(username: String, password: Secret[String])
    
      object Config {
        def fromParameters(parameters: AwsSsmParameters[IO]): IO[Config] =
          (
            env("USERNAME"),
            parameters("password")
          ).parMapN(apply).load[IO]
      }
    
      override def run: IO[Unit] =
        EmberClientBuilder.default[IO].build.use { client =>
          CredentialsProvider.default(client).use { provider =>
            val parameters = AwsSsmParameters(client, provider, Region.EU_WEST_1)
            Config.fromParameters(parameters).flatMap(IO.println)
          }
        }
    }
  12. Install Ciris via sbt

    main

    To use Ciris in your Scala project, add the core module to your build.sbt file.

    Note for Scala.js and Scala Native: You must use %%% instead of %% for cross-building support.

    Note for Scala 2.12: You must enable partial unification by adding the -Ypartial-unification compiler option.

    // Standard Scala
    libraryDependencies += "@ORGANIZATION" %% "@CORE_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // Scala.js or Scala Native
    libraryDependencies += "@ORGANIZATION" %%% "@CORE_MODULE_NAME@" % "@LATEST_VERSION@"
    
    // Required for Scala 2.12
    scalacOptions += "-Ypartial-unification"