Skip to content

Commit

Permalink
iss #24: config parse rules & helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
maizy committed Jan 18, 2017
1 parent b4d177d commit 456559c
Show file tree
Hide file tree
Showing 13 changed files with 298 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ object WebAppConfigReader
override def appName: String = "java -jar ambient7-webapp.jar"

override def fillReader(): Unit = {
fillConfigOptions(requireUniversalConfig = true)
fillConfigOptions(requireConfig = true)
fillDbOptions()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,24 @@ package ru.maizy.ambient7.webapp
* Copyright (c) Nikita Kovaliov, maizy.ru, 2016-2017
* See LICENSE.txt for details.
*/

object WebAppLauncher extends App {

val config = WebAppConfigReader
config.fillReader()
val eitherAppConfig = config.readAppConfig(args.toIndexedSeq)
eitherAppConfig match {
case Left(parsingError) =>
// TODO: show usage and error if needed, extract to core function
Console.err.println(
s"\nUnable to launch app.\n\nErrors:\n * ${parsingError.messages.mkString("\n * ")}" +
parsingError.usage.map(u => s"Usage: $u").getOrElse("")
)
// TODO: extract to core
val sep = "\n * "
val errors = if (parsingError.messages.nonEmpty) {
s"Errors:$sep${parsingError.messages.mkString(sep)}\n"
} else {
""
}
val usage = parsingError.usage.map(u => s"Usage: $u").getOrElse("")
val userResult = List(errors, usage).filterNot(_ == "").mkString("\n")
Console.err.println(userResult)
case Right(opts) =>
println(s"Success: $opts")
}
Expand Down
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ lazy val commonDependencies = Seq(
"ch.qos.logback" % "logback-classic" % "1.1.7",
"com.typesafe.scala-logging" %% "scala-logging" % "3.5.0",
"com.typesafe" % "config" % "1.3.1",
"com.github.kxbmap" %% "configs" % "0.4.4",
"org.scalatest" %% "scalatest" % "3.0.0" % "test"
)
)
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,6 @@ trait InfluxDbConfigReader extends UniversalConfigReader {
}
}

// private def appendInfluxDbOptsPostprocessor(postprocessor: InfluxDbOptions => ParseResult): Unit =
// appendPostprocessor { appOpts =>
// appOpts.influxDb match {
// case Some(influxDbOptions) => postprocessor(influxDbOptions)
// case _ => Right(appOpts)
// }
// }

def fillInfluxDbOptions(): Unit = {

cliParser.opt[String]("influxdb-baseurl")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package ru.maizy.ambient7.core.config
* Copyright (c) Nikita Kovaliov, maizy.ru, 2016-2017
* See LICENSE.txt for details.
*/

trait MainDbConfigReader extends UniversalConfigReader {

import UniversalConfigReader._
Expand All @@ -30,18 +31,30 @@ trait MainDbConfigReader extends UniversalConfigReader {
.action { (value, opts) => mainDbOpts(opts)(_.copy(url = Some(value))) }
.text(s"URL for connecting to h2 database")

appendSimpleOptionalConfigRule[String]("db.url") { (value, opts) =>
mainDbOpts(opts)(_.copy(url = Some(value)))
}

appendDbOptsCheck{ dbOpts => Either.cond(dbOpts.url.isDefined, (), ParsingError.withMessage("db-url is required")) }


cliParser.opt[String]("db-user")
.action { (value, opts) => mainDbOpts(opts)(_.copy(user = value)) }
.text("database user")

appendSimpleOptionalConfigRule[String]("db.user") {
(value, opts) => mainDbOpts(opts)(_.copy(user = value))
}


cliParser.opt[String]("db-password")
.action { (value, opts) => mainDbOpts(opts)(_.copy(password = value)) }
.text("database password")

appendSimpleOptionalConfigRule[String]("db.password") { (value, opts) =>
mainDbOpts(opts)(_.copy(password = value))
}

()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ object ParsingError {
def withMessage(message: String): ParsingError =
ParsingError(IndexedSeq(message))

def withMessages(messages: String*): ParsingError =
def withMessages(messages: Seq[String]): ParsingError =
ParsingError(messages.toIndexedSeq)

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,20 @@ package ru.maizy.ambient7.core.config
*/

import java.io.File
import java.nio.file.Files
import java.nio.file.{ Files, Path }
import scala.annotation.tailrec
import scala.util.{ Failure, Try, Success }
import com.typesafe.config.{ Config, ConfigFactory }
import configs.Configs
import scopt.OptionParser
import ru.maizy.ambient7.core.config.helper.ConfigRuleOps.IfSuccessOp

object UniversalConfigReader {
type CheckResult = Either[ParsingError, Unit]
type Check = Ambient7Options => CheckResult
type ParseResult = Either[ParsingError, Ambient7Options]
type Postprocessor = Ambient7Options => ParseResult
type ConfigRule = (Config, Ambient7Options) => ParseResult
}

trait UniversalConfigReader {
Expand All @@ -24,7 +29,8 @@ trait UniversalConfigReader {
private var postprocessors_ = List[Postprocessor]()

private var isConfigEnabled_ = false
// private var isConfigRequired_ = false
private var isConfigRequired_ = false
private var configRules_ = List[ConfigRule]()

private val isCliOptionsEnabled_ = true

Expand Down Expand Up @@ -75,9 +81,9 @@ trait UniversalConfigReader {
.action((_, opts) => opts.copy(showHelp = true))
}

protected def fillConfigOptions(requireUniversalConfig: Boolean = false): Unit = {
protected def fillConfigOptions(requireConfig: Boolean = false): Unit = {
isConfigEnabled_ = true
// isConfigRequired_ = requireUniversalConfig
isConfigRequired_ = requireConfig

def addConfigOption(parser: OptionParser[Ambient7Options], required: Boolean = false): Unit = {
val opt = parser.opt[File]("config")
Expand All @@ -96,11 +102,13 @@ trait UniversalConfigReader {
}

addConfigOption(configPathCliParser)
addConfigOption(cliParser, required = requireUniversalConfig)
addConfigOption(cliParser, required = requireConfig)
}

def isConfigEnabled: Boolean = isConfigEnabled_

def isConfigRequired: Boolean = isConfigRequired_

def isCliOptionsEnabled: Boolean = isCliOptionsEnabled_

def appName: String
Expand All @@ -116,6 +124,30 @@ trait UniversalConfigReader {
protected def appendPostprocessor(postprocessor: Postprocessor): Unit =
postprocessors_ = postprocessor :: postprocessors_

protected def appendConfigRule(rule: ConfigRule): Unit =
configRules_ = rule :: configRules_

protected def appendSimpleConfigRule[T](configPath: String)(
saveValue: (T, Ambient7Options) => Ambient7Options)(implicit reader: Configs[T]): Unit =
{
appendConfigRule { (config, opts) =>
Configs.apply[T].get(config, configPath).ifSuccess(value => saveValue(value, opts))
}
}

protected def appendSimpleOptionalConfigRule[T](configPath: String)(
save: (T, Ambient7Options) => Ambient7Options)(implicit reader: Configs[Option[T]]): Unit =
{
appendSimpleConfigRule[Option[T]](configPath) { (mayBeValue, opts) =>
mayBeValue match {
case Some(value) => save(value, opts)
case None => opts
}
}
}

def configRules: List[ConfigRule] = configRules_.reverse

private def processHelpOption(result: ParseResult): ParseResult = {
result match {
// show usage if --help option exists
Expand All @@ -124,26 +156,37 @@ trait UniversalConfigReader {
}
}

private def safeLoadConfig(universalConfigPath: Path): Either[IndexedSeq[String], Config] = {
Try(ConfigFactory.parseFile(universalConfigPath.toFile)) match {
case Failure(exception) => Left(IndexedSeq(s"reading config error ${exception.getMessage}"))
case Success(config) => Right(config)
}
}

private def parseConfig(opts: Ambient7Options, args: IndexedSeq[String]): ParseResult = {
configLogger.debug("config enabled")
val optsWithConfigPath = configPathCliParser.parse(args, opts).getOrElse(opts)
var parseResult: ParseResult = Right(optsWithConfigPath)
parseResult = processHelpOption(parseResult)
parseResult.right.flatMap { optsWithConfigPath =>
optsWithConfigPath.universalConfigPath match {
case Some(universalConfigPath) =>
if (Files.isReadable(universalConfigPath)) {
configLogger.info(s"parse config from $universalConfigPath")

// FIXME: implements

Right(optsWithConfigPath)
case Some(configPath) =>
if (Files.isReadable(configPath)) {
configLogger.info(s"parse config from $configPath")
safeLoadConfig(configPath)
.left.map(ParsingError.withMessages(_))
.right.flatMap { config =>
val configRulesApplingResult = configRules.foldLeft[ParseResult](Right(optsWithConfigPath)) {
case (error@Left(_), _) => error
case (res@Right(_), rule) => res.right.flatMap(opts => rule(config, opts))
}
configRulesApplingResult
}
} else {
val message = s"unable to read config from $universalConfigPath"
val message = s"unable to read config from $configPath"
configLogger.error(message)
Left(ParsingError.withMessage(message))
}

case _ =>
configLogger.info("universal config path not defined")
Right(opts)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ru.maizy.ambient7.core.config.helper

/**
* Copyright (c) Nikita Kovaliov, maizy.ru, 2017
* See LICENSE.txt for details.
*/

import ru.maizy.ambient7.core.config.{ Ambient7Options, ParsingError, UniversalConfigReader }

object ConfigRuleOps {

import UniversalConfigReader._

implicit class IfSuccessOp[T](configRes: configs.Result[T]) {

def ifSuccess(saveValue: T => Ambient7Options): ParseResult = {
configRes match {
case configs.Result.Failure(error) => Left(ParsingError.withMessages(error.messages))
case configs.Result.Success(value) => Right(saveValue(value))
}
}
}
}
4 changes: 4 additions & 0 deletions core/src/test/resources/sample-config.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
sample {
string = "abcd"
array = ["a", "b"]
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ package ru.maizy.ambient7.core.tests
* See LICENSE.txt for details.
*/

import java.nio.file.Paths
import org.scalatest.{ FlatSpec, Matchers }

abstract class BaseSpec extends FlatSpec with Matchers
abstract class BaseSpec extends FlatSpec with Matchers {

def getResourcePathString(relPath: String): String = {
val normPath = if (!relPath.startsWith("/")) "/" + relPath else relPath
Paths.get(this.getClass.getResource(normPath).toURI).toString
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package ru.maizy.ambient7.core.tests.config

/**
* Copyright (c) Nikita Kovaliov, maizy.ru, 2017
* See LICENSE.txt for details.
*/

import ru.maizy.ambient7.core.config.{ DbOptions, UniversalConfigReader }

trait SampleReaders {

class ReaderWithSimpleOpts extends UniversalConfigReader {
override def appName: String = "test app"

override def fillReader(): Unit = {
cliParser.opt[String]("option")
.abbr("o")
.action { (value, opts) => opts.copy(mainDb = Some(DbOptions(url = Some(value)))) }

()
}
}

class ReaderWithConfig(requireConfig: Boolean) extends UniversalConfigReader {
override def appName: String = "test app"

override def fillReader(): Unit = {
fillConfigOptions(requireConfig)
cliParser.opt[String]("option")
.abbr("o")
.action { (value, opts) => opts.copy(mainDb = Some(DbOptions(url = Some(value)))) }

()
}
}

class ReaderWithConfigRule(requireConfig: Boolean) extends UniversalConfigReader {
override def appName: String = "test app"
override def fillReader(): Unit = {
fillConfigOptions(requireConfig)
appendConfigRule { (config, opts) =>
Right(opts.copy(mainDb = Some(DbOptions(url=Some(config.getString("sample.string"))))))
}
()
}
}

class ReaderWithSimpleConfigRule(path: String) extends UniversalConfigReader {
override def appName: String = "test app"
override def fillReader(): Unit = {
fillConfigOptions(requireConfig = true)
appendSimpleConfigRule[String](path) { (value, opts) =>
opts.copy(mainDb = Some(DbOptions(url = Some(value))))
}
()
}
}

class ReaderWithSimpleOptionalConfigRule(path: String) extends UniversalConfigReader {
override def appName: String = "test app"
override def fillReader(): Unit = {
fillConfigOptions(requireConfig = true)
appendSimpleOptionalConfigRule[String](path) { (value, opts) =>
opts.copy(mainDb = Some(DbOptions(url = Some(value))))
}
()
}
}

class ReaderConfigRuleAndCliOption extends ReaderWithConfigRule(requireConfig = true) {
override def fillReader(): Unit = {
super.fillReader()
cliParser.opt[String]("some")
.action { (value, opts) => opts.copy(mainDb = Some(DbOptions(url = Some(value)))) }
()
}
}
}
Loading

0 comments on commit 456559c

Please sign in to comment.