Skip to content

Commit

Permalink
Implement Java property file as a source (#164)
Browse files Browse the repository at this point in the history
* Make coproduct test consistent

* Add support for java properties

* Implement java property source

* Implement java property source
  • Loading branch information
afsalthaj authored and leigh-perry committed Dec 31, 2019
1 parent a57817f commit 1ae6062
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 0 deletions.
26 changes: 26 additions & 0 deletions core/src/main/scala/zio/config/Config.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package zio.config

import java.io.{ File, FileInputStream }
import java.util.Properties

import zio.system.System
import zio.{ IO, UIO, ZIO }
import zio.system.System.Live.system.lineSeparator

trait Config[A] {
def config: Config.Service[A]
Expand Down Expand Up @@ -36,6 +40,28 @@ object Config {
): IO[ReadErrors[Vector[String], String], Config[A]] =
make[String, String, A](ConfigSource.fromMap(map), configDescriptor)

// If reading a file, this can have read errors as well as throwable when trying to read the file
def fromPropertyFile[A](
filePath: String,
configDescriptor: ConfigDescriptor[String, String, A]
): ZIO[Any, Throwable, Config[A]] =
ZIO
.bracket(ZIO.effect(new FileInputStream(new File(filePath))))(r => ZIO.effectTotal(r.close()))(inputStream => {
ZIO.effect {
val properties = new Properties()
properties.load(inputStream)
properties
}
})
.flatMap(
properties =>
lineSeparator.flatMap(
ln =>
make(ConfigSource.fromJavaProperties(properties), configDescriptor)
.mapError(r => new RuntimeException(s"${ln}${r.mkString(ln)}"))
)
)

def fromPropertyFile[K, V, A](
configDescriptor: ConfigDescriptor[String, String, A]
): ZIO[System, ReadErrors[Vector[String], String], Config[A]] =
Expand Down
36 changes: 36 additions & 0 deletions core/src/main/scala/zio/config/ConfigSource.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package zio.config

import zio.{ IO, ZIO }
import zio.system.System.Live.system
import java.{ util => ju }

final case class ConfigValue[+A](value: A, sourceDescription: String)

Expand All @@ -23,6 +24,7 @@ object ConfigSource {
val SystemProperties = "system properties"
val ConstantMap = "constant <map>"
val EmptySource = "<empty>"
val JavaProperties = "java properties"

def empty[K, V]: ConfigSource[K, V] =
ConfigSource((k: Vector[K]) => IO.fail(singleton(ReadError.MissingValue(k))), EmptySource :: Nil)
Expand Down Expand Up @@ -58,6 +60,40 @@ object ConfigSource {
SystemProperties :: Nil
)

/**
* Pass any Java Properties that you have and you get a ConfigSource.
* zio-config tries to not make assumptions on the placement of property file. It may exist
* in classpath, or it could be in cloud (AWS S3). Loading to properties file is user's
* responsiblity to make things flexible. A typical usage will be
* {{{
* ZIO
* .bracket(ZIO.effect(new FileInputStream("file location")))(file => ZIO.effectTotal(file.close()))(
* file =>
* ZIO.effect {
* val properties = new java.util.Properties()
* properties.load(file)
* properties
* }).map(r => fromJavaProperties(r))
* }}}
*/
def fromJavaProperties(
property: ju.Properties
): ConfigSource[String, String] =
ConfigSource(
(path: Vector[String]) => {
val key = path.mkString(".")
ZIO
.effect(
Option(property.getProperty(key))
)
.bimap(ReadError.FatalError(path, _), opt => opt.map(ConfigValue(_, JavaProperties)))
.flatMap(IO.fromOption(_))
.mapError(_ => singleton(ReadError.MissingValue(path)))

},
JavaProperties :: Nil
)

def fromMap(map: Map[String, String], delimiter: String = "."): ConfigSource[String, String] =
ConfigSource(
(path: Vector[String]) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package zio.config.examples

import zio.App
import zio.config._
import ConfigDescriptor._
import zio.ZIO
import zio.console.Console.Live.console._

/**
* An example of an entire application that uses java properties
*/
final case class ApplicationConfig(bridgeIp: String, userName: String)

object ApplicationConfig {
val configuration =
((string("bridgeIp")) |@| string("username"))(ApplicationConfig.apply, ApplicationConfig.unapply)
}

// The main App
object SimpleExampleMain extends App {
override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] = {
val pgm =
for {
fileLocation <- ZIO.effect(System.getProperty("user.home") + "/somefile.properties")
// there are many ways of doing this: example: {{{ read(configuration from ConfigSource.fromJavaProperties(propertyFile))) }}}, you may try that as well.
config <- Config.fromPropertyFile(fileLocation, ApplicationConfig.configuration)
_ <- SimpleExample.finalExecution.provide(config)
} yield ()

pgm.foldM(
throwable => putStr(throwable.getMessage()) *> ZIO.succeed(1),
_ => putStrLn("hurray !! Application ran successfully..") *> ZIO.succeed(0)
)
}
}

// The core application functions
object SimpleExample {
val printConfigs: ZIO[Config[ApplicationConfig], Nothing, Unit] =
for {
appConfig <- config[ApplicationConfig]
_ <- putStrLn(appConfig.bridgeIp)
_ <- putStrLn(appConfig.userName)
} yield ()

val finalExecution: ZIO[Config[ApplicationConfig], Nothing, Unit] =
for {
_ <- printConfigs
_ <- putStrLn(s"processing data......")
} yield ()
}
// A note that, with magnolia module (which is still experimental), you can skip writing the {{ configuration }} in ApplicationConfig object
// import zio.config.magnolia.ConfigDescriptorProvider_,
// val configuration = description[ApplicationConfig], and requires case class names as configuration keys

0 comments on commit 1ae6062

Please # to comment.