All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.adrianfilip.zio.properties.ZioProperties.scala Maven / Gradle / Ivy

package com.adrianfilip.zio.properties

import java.io.File

import com.typesafe.config.ConfigFactory
import izumi.reflect.Tags.Tag
import zio.config.magnolia.DeriveConfigDescriptor.descriptor
import zio.config.typesafe._
import zio.config.{ ConfigSource, _ }
import zio.{ Has, Task, ZIO, ZLayer }

/**
 * Property resolution order:
 * - command line arguments
 * - system properties
 * - environment variables
 * - HOCON file
 * - properties file
 */
object ZioProperties {

  def createPropertiesLayer[T: Tag](
    args: List[String],
    descriptor: ConfigDescriptor[String, String, T]
  ): ZIO[Any, Throwable, ZLayer[Any, ReadError[String], Has[T]]] =
    for {
      sources <- createSources(args)
      desc    = descriptor.from(unifySources(sources))
      l       = ZLayer.fromEffect(ZIO.fromEither(read(desc)))
    } yield l

  private def unifySources(sources: List[ConfigSource[String, String]]): ConfigSource[String, String] =
    sources.reduce((s1, s2) => s1.orElse(s2))

  private def createSources(args: List[String]): ZIO[Any, Throwable, List[ConfigSource[String, String]]] = {
    val NO_PROFILE = ""
    val PROD       = "prod"
    for {
      argsConfigSource  <- ZIO.succeed(ConfigSource.fromCommandLineArgs(args, Some('.'), Some(',')))
      systemPropsSource <- ConfigSource.fromSystemProperties(Some('_'), Some(','))
      envPropsSource    <- ConfigSource.fromSystemEnv(Some('_'), Some(','))
      profile           = getProfile(unifySources(List(argsConfigSource, systemPropsSource, envPropsSource)))
      appHoconSource <- profile.hoconFile match {
                         case Some(value) =>
                           fromHoconResource(s"/$value")
                         case None =>
                           fromHoconResourceIfPresent(
                             profile.profile.map(_.toLowerCase()).getOrElse(NO_PROFILE) match {
                               case NO_PROFILE => "/application.conf"
                               case PROD       => "/application.conf"
                               case profile    => s"/application-$profile.conf"
                             }
                           )
                       }
      appPropsSource <- profile.propertiesFile match {
                         case Some(value) =>
                           fromPropertiesResource(s"/$value", Some('.'), Some(','))
                         case None =>
                           fromPropertiesResourceIfPresent(
                             profile.profile.map(_.toLowerCase()).getOrElse(NO_PROFILE) match {
                               case NO_PROFILE => "/application.properties"
                               case PROD       => "/application.properties"
                               case profile    => s"/application-$profile.properties"
                             },
                             Some('.'),
                             Some(',')
                           )
                       }
    } yield List(argsConfigSource, systemPropsSource, envPropsSource, appHoconSource, appPropsSource)
  }

  /**
   * Will fail if the file is not found.
   *
   * @param file
   * @param keyDelimiter
   * @param valueDelimiter
   * @return
   */
  private def fromPropertiesResource[A](
    file: String,
    keyDelimiter: Option[Char],
    valueDelimiter: Option[Char]
  ): Task[ConfigSource[String, String]] =
    for {
      properties <- ZIO.bracket(
                     ZIO.effect(getClass.getResourceAsStream(file))
                   )(r => ZIO.effectTotal(r.close())) { inputStream =>
                     ZIO.effect {
                       val properties = new java.util.Properties()
                       properties.load(inputStream)
                       properties
                     }
                   }
    } yield ConfigSource.fromProperties(
      properties,
      file,
      keyDelimiter,
      valueDelimiter
    )

  /**
   * Will not fail if file is not found. Instead it will create a ConfigSource from an empty java.util.Properties
   *
   * @param file
   * @param keyDelimiter
   * @param valueDelimiter
   * @return
   */
  private def fromPropertiesResourceIfPresent[A](
    file: String,
    keyDelimiter: Option[Char],
    valueDelimiter: Option[Char]
  ): Task[ConfigSource[String, String]] =
    for {
      properties <- ZIO.bracket(
                     ZIO.effect(getClass.getResourceAsStream(file))
                   )(r => ZIO.effectTotal(if (r != null) r.close())) { inputStream =>
                     ZIO.effect {
                       val properties = new java.util.Properties()
                       if (inputStream != null) {
                         properties.load(inputStream)
                       }
                       properties
                     }
                   }
    } yield ConfigSource.fromProperties(
      properties,
      file,
      keyDelimiter,
      valueDelimiter
    )

  /**
   * Will fail if the file is not found.
   *
   * @param file
   * @return
   */
  private def fromHoconResource[A](file: String): Task[ConfigSource[String, String]] =
    for {
      resourceURI <- ZIO
                      .fromOption(Option(getClass.getResource(file)).map(_.toURI))
                      .mapError(_ => new RuntimeException(s"$file not found in classpath!"))
      fileInstance <- Task(new File(resourceURI))
      configSource <- ZIO
                       .fromEither(
                         TypesafeConfigSource.fromTypesafeConfig(ConfigFactory.parseFile(fileInstance).resolve)
                       )
                       .mapError(error => new RuntimeException(error))
    } yield configSource

  /**
   * Will not fail if file is not found. Instead it will create a ConfigSource from an empty HOCON string
   *
   * @param file
   * @return
   */
  private def fromHoconResourceIfPresent[A](file: String): Task[ConfigSource[String, String]] =
    Option(getClass.getResource(file)).map(_.toURI) match {
      case Some(uri) =>
        Task(new File(uri)).flatMap(fileInstance =>
          TypesafeConfigSource
            .fromTypesafeConfig(ConfigFactory.parseFile(fileInstance).resolve) match {
            case Left(value)  => Task.fail(new RuntimeException(value))
            case Right(value) => Task.succeed(value)
          }
        )
      case None => Task.succeed(ConfigSource.empty)
    }

  private final case class Profile(
    profile: Option[String],
    hoconFile: Option[String],
    propertiesFile: Option[String]
  )

  private def getProfile(configSource: ConfigSource[String, String]): Profile = {
    val desc   = descriptor[Profile]
    val params = desc.from(configSource)
    read(params) match {
      case Left(_)      => Profile(None, None, None)
      case Right(value) => value
    }
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy