This library uses shapeless and cats to provide a neat syntax to instantiate Scala case classes from a configuration source.
Rules for configuration resolution are specified in the declaration of the case class itself:
case class ApplicationConfig(default: Int = 100, noDefault: String, optional: Option[Double])
val config: ValidatedNel[ValidationFailure, ApplicationConfig] = resolve[ApplicationConfig, Resolvers](Resolvers)
In ApplicationConfig
above default
will be set to 100
, noDefault
will cause a validation failure to be logged and optional
will be set to None
, should the configuration source not contain a value for each parameter.
- Modules
- Motivation
- Supported Functionality
- Unsupported Functionality
- Similar Projects
- Quick Start Guide
- Extending
- Participation
Module | Description | Download |
---|---|---|
Extruder | Main module, includes core functionality and basic resolvers. | |
Typesafe Config | Support for resolution from Typesafe Config. | |
Fetch | Support for lookup using Fetch. See the readme for more info. |
Add the following to your sbt project/plugins.sbt
file:
addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0")
Then add the following to your build.sbt
:
resolvers += Resolver.bintrayRepo("janstenpickle", "maven")
libraryDependencies += "extruder" %% "extruder" % "0.2.0"
// only if you require support for Typesafe config
libraryDependencies += "extruder" %% "extruder-typesafe" % "0.2.0"
// only if you require support for Fetch
libraryDependencies += "extruder" %% "extruder-fetch" % "0.2.0"
To learn more about shapeless having read Dave Gurnell's excellent introduction: "The Type Astronaut's Guide to Shapeless".
Try out Grafter. This project complements applications which use Grafter or other dependency injection frameworks and techniques by providing a way of resolving values in case classes from a configuration source.
Specifically Grafter requires that all configuration be part single case class to be passed to the entry point of the application. Structuring the config in classes like this works well, but leaves the question of how are these classes populated with config?
This is where Extruder comes in, the example here shows how they may be used together.
- Parsing of primitive types:
- String
- Int
- Long
- Double
- Float
- Short
- Byte
- Boolean
- URL
- Duration
- Finite Duration
- Case class resolution
- Sealed type member resolution (ADTs)
- Resolution from multiple configuration sources:
- Pluggable configuration backends
- Addition of more types
Cyclical references
case class Example(e: Example)
resolve[Example, SystemPropertiesResolvers](SystemPropertiesResolvers) // won't compile
case class NestedOne(n: NestedTwo)
case class NestedTwo(n: NestedOne)
resolve[NestedOne, SystemPropertiesResolvers](SystemPropertiesResolvers) // won't compile
Type Parameters on Case Classes
case class Typed[T](t: T)
resolve[Typed[Int], SystemPropertiesResolvers](SystemPropertiesResolvers) // won't compile
PureConfig uses a similar technique to create case classes from Typesafe Config.
- Extruder accumilates configuration resolution errors, so a single invocation will highlight all problems with the configuration source
- Extruder supports pluggable configuration backends - does not rely on Typesafe Config by default
- Pureconfig supports time parsing using
java.time
which Extruder does not out of the box, a date parsing module may be added in the future, until then custom resolvers may be added - Resolution of Typesafe
ConfigValue
,ConfigObject
andConfigList
is only supported by the Typesafe Config configuration backend, as it is closely tied to Typesafe Config, Pureconfig supports this by default as it is directly tied to Typesafe config - Pureconfig supports returning
Map[String, String]
, for the time being Extruder does not support this - Extruder supports control of class and parameter name formatting by overriding a method, however Pureconfig supports this via predefined configuration schemes
import extruder.core.SystemPropertiesResolvers
import extruder.resolution._
object Main extends App {
case class Example(defaultedString: String = "default", configuredString: String, optionalString: Option[String])
println(resolve[Example](SystemPropertiesResolvers)) // Invalid(NonEmptyList(ValidationFailure("Could not find configuration at 'example.configuredstring' and no default available", None)))
System.setProperty("example.configuredstring", "configured")
println(resolve[Example](SystemPropertiesResolvers)) // Valid(Example("default", "configured", None))
System.setProperty("example.optionalsting", "optional")
println(resolve[Example, SystemPropertiesResolvers](SystemPropertiesResolvers)) // Valid(Example("default", "configured", Some("optional")))
}
import extruder.core.SystemPropertiesResolvers
import extruder.resolution._
object Main extends App {
case class Example(a: NestedOne, b: NestedTwo)
case class NestedOne(value: String, nested: NestedTwo)
case class NestedTwo(value: String)
System.setProperty("example.a.nestedone.value", "nested-one")
System.setProperty("example.a.nestedone.nested.nestedtwo.value", "nested-one-nested-two")
System.setProperty("example.b.nestedtwo.value", "nested-two")
println(resolve[Example, SystemPropertiesResolvers](SystemPropertiesResolvers)) // Valid(Example(NestedOne("nested-one", NestedTwo("nested-one-nested-two")), NestedTwo("nested-two"))
}
import extruder.core.SystemPropertiesResolvers
import extruder.resolution._
object TopLevelSealed extends App {
sealed trait Sealed
case object ExamplObj
case class ExampleCC(a: Int)
System.setProperty("type", "ExampleObj")
println(resolve[Example, SystemPropertiesResolvers](SystemPropertiesResolvers)) // Valid(ExampleObj)
System.setProperty("type", "ExampleCC")
System.setProperty("examplecc.a", "1")
println(resolve[Example, SystemPropertiesResolvers](SystemPropertiesResolvers)) // Valid(ExampleCC(1))
}
import extruder.core.SystemPropertiesResolvers
import extruder.resolution._
object NestedSealed extends App {
sealed trait Sealed
case object ExamplObj
case class ExampleCC(a: Int)
case class Example(a: Sealed)
System.setProperty("example.a.type", "ExampleObj")
println(resolve[Example, SystemPropertiesResolvers](SystemPropertiesResolvers)) // Valid(Example(ExampleObj))
System.setProperty("example.a.type", "ExampleCC")
System.setProperty("example.a.examplecc.a", "1")
println(resolve[Example, SystemPropertiesResolvers](SystemPropertiesResolvers)) // Valid(Example(ExampleCC(1)))
}
The core project ships with a Resolvers
trait and two implementations which use a map of strings and Java system properties as a configuration sources.
The Resolvers
trait is responsible for providing a set of implicit Resolver
instances for primitive Scala types. These resolvers are used during the case class construction in Extruder
, if a resolver cannot be found for a certain type then the compiler will produce an error.
###Naive Implementation - Use a Map
For every simple configuration sources, which can be loaded directly into memory, the most simple possible implementation may be to convert it to a map and pass that to MapResolvers
:
import extruder.core.MapResolvers
object MyConfigSourceResolvers {
def apply(source: MyConfigSource): MapResolvers = MapResolvers(convertToMap(source))
def convertToMap(source: MyConfigSource): Map[String, String] = ???
}
The Resolvers
trait assumes your configuration source will simply be providing string values and includes Resolver
implementations for most Scala primitives, based on the Mouse library's string parsing. Therefore you only have to implement two methods to add a new simple config source:
object MyResolvers extends Resolvers {
override def pathToString(path: Seq[String]): String = ???
override def lookupValue(path: Seq[String]): ConfigValidation[Option[String]] = ???
override def lookupList(path: Seq[String]): ConfigValidation[Option[List[String]]] = ???
}
Notice both methods accept the parameter path
which is a Seq[String]
, this is essentially the long name of the configuration you are trying to resolve. It is made up of the name of the case class and the name of the parameter.
For example case class Example(a: String, b: Int)
will evaluate to the paths Seq("Example", "a")
and Seq("Example", "b")
, at this point everything is case sensitive, it is up to the implementer as to whether they want their configuration to base case sensitive or not.
####Example Implementation
What will follow is a break down of the implementation of the SystemPropertiesResolver
provided by the core library.
First create a map from system properties to act as the configuration source, notice that in this implementation we have chosen to be case insensitive by making all the property keys lower case:
val props: Map[String, String] = System.getProperties.asScala.toMap.map { case (k, v) => k.toLowerCase -> v }
#####Implementing pathToString
The pathToString
method is used internally for meaningful error messages on failure of config resolution, using it in resolveConfig
is optional.
Again, we're using case insensitivity here so convert the path to lower case.
override def pathToString(path: Seq[String]): String = path.mkString(".").toLowerCase
Note the return type of lookupValue
is ConfigValidation[Option[String]]
which expands to cats.data.ValidatedNel[ValidationFailure, Option[String]]
. This allows for any errors in looking up configuration to be handled differently to the configuration value not being present. For example, a connection error to a remote configuration source should be handled as an InvalidNel[String]
, where the error message is a string. Whereas the configuration value not being present should be an empty Option[String]
.
override def lookupValue(path: Seq[String]): ConfigValidation[Option[String]] = props.get(pathToString(path)).validNel
The .validNel
on the end of the lookup is from the cats.syntax.validated._
package and simply lifts the result of props.get(pathToString(path))
into the right of cats.data.ValidatedNel[String, Option[String]]
.
#####Overriding lookupList
Note that this will override the default implementation in Resolvers
, if you're happy with the default implementation and just want to change the separator see Overriding listSeparator
.
The return type for this function is ConfigValidation[Option[List[String]]]
, this allows for any errors looking up or converting to a list the value. Some sources may provide list or array types natively, if they don't you may implement it here.
override def lookupList(path: Seq[String]): ConfigValidation[Option[List[String]]] =
lookupValue(path).map(_.map(_.split(",").toList.map(_.trim)))
As this config source does not provide a means of looking up a list we look up the value using lookupValue
and turn the string inside ConfigValidation[Option[String]]
into a list of strings, where each element is separated by a ,
.
It is also possible to add some validation when converting the string value to a list:
override def lookupList(path: Seq[String]): ConfigValidation[Option[List[String]]] =
lookupValue(path).fold(
_.invalid,
_.fold[ConfigValidation[Option[List[String]]]](None.validNel)(value =>
if (value.contains(",")) Some(value.split(",").toList.map(_.trim)).validNel
else ValidationFailure(
s"No separator (,) found in value '$value' when attempting to create a list for '${pathToString(path)}'"
)
)
)
If you are happy with the default implementation of lookupList
, but want to change the list separator then you can simply override listSeparator
and set it to anything you like.
Say you wanted to add a resolver for a certain type it is possible to extend an existing implementation of Resolvers
to parse the new type. Below is an example adding a new resolver for URL
:
import cats.syntax.either._
import java.net.URL
import extruder.core.SystemPropertiesResolvers
import extruder.core.Parser
class WithURL extends SystemPropertiesResolvers {
implicit val url: Parser[URL] = value => Either.catchNonFatal(new URL(value))
}
This project supports the Typelevel code of conduct and aims that its channels (mailing list, Gitter, github, etc.) to be welcoming environments for everyone.