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 20, 2017
1 parent b4d177d commit 412a0a6
Show file tree
Hide file tree
Showing 13 changed files with 335 additions and 59 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 All @@ -158,6 +201,15 @@ trait UniversalConfigReader {
Right(cliParser.parse(args, opts).getOrElse(opts))
}

private def applyPostprocessors(result: ParseResult): ParseResult = {
result.right.flatMap { opts =>
postprocessors.foldLeft[ParseResult](Right(opts)) {
case (error@Left(_), _) => error
case (res@Right(_), processor) => res.right.flatMap(processor)
}
}
}

private def checkConfig(opts: Ambient7Options): ParseResult =
{
@tailrec
Expand Down Expand Up @@ -187,9 +239,10 @@ trait UniversalConfigReader {
eitherAppConfig = eitherAppConfig.right.flatMap(opts => parseCliOptions(args, opts))
}
eitherAppConfig = processHelpOption(eitherAppConfig)
eitherAppConfig = applyPostprocessors(eitherAppConfig)
eitherAppConfig = eitherAppConfig.right.flatMap(opts => checkConfig(opts))
eitherAppConfig match {
case config@Right(_) => config
case success@Right(_) => success
case Left(parsingError) if isCliOptionsEnabled => Left(cliParser.appendUsageToParserError(parsingError))
case error@Left(_) => error
}
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
}
}
Loading

0 comments on commit 412a0a6

Please sign in to comment.