From 0ff4cb5ff8140334d7b358d1437c0e0210a1d273 Mon Sep 17 00:00:00 2001 From: Nikita Kovaliov Date: Mon, 16 Jan 2017 09:02:05 +0300 Subject: [PATCH] iss #24: config parse rules & helpers --- .../ambient7/webapp/WebAppLauncher.scala | 16 +++-- build.sbt | 1 + config/{application.conf => ambient7.conf} | 0 .../core/config/MainDbConfigReader.scala | 13 ++++ .../ambient7/core/config/ParsingError.scala | 2 +- .../core/config/UniversalConfigReader.scala | 67 +++++++++++++++---- .../core/config/helper/ConfigRuleOps.scala | 23 +++++++ 7 files changed, 104 insertions(+), 18 deletions(-) rename config/{application.conf => ambient7.conf} (100%) create mode 100644 core/src/main/scala/ru/maizy/ambient7/core/config/helper/ConfigRuleOps.scala diff --git a/ambient7-webapp/src/main/scala/ru/maizy/ambient7/webapp/WebAppLauncher.scala b/ambient7-webapp/src/main/scala/ru/maizy/ambient7/webapp/WebAppLauncher.scala index b364687..d5545cf 100644 --- a/ambient7-webapp/src/main/scala/ru/maizy/ambient7/webapp/WebAppLauncher.scala +++ b/ambient7-webapp/src/main/scala/ru/maizy/ambient7/webapp/WebAppLauncher.scala @@ -4,6 +4,7 @@ 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 @@ -11,11 +12,16 @@ object WebAppLauncher extends App { 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") } diff --git a/build.sbt b/build.sbt index 969098b..40d34f9 100644 --- a/build.sbt +++ b/build.sbt @@ -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" ) ) diff --git a/config/application.conf b/config/ambient7.conf similarity index 100% rename from config/application.conf rename to config/ambient7.conf diff --git a/core/src/main/scala/ru/maizy/ambient7/core/config/MainDbConfigReader.scala b/core/src/main/scala/ru/maizy/ambient7/core/config/MainDbConfigReader.scala index 96a5e05..cb6221b 100644 --- a/core/src/main/scala/ru/maizy/ambient7/core/config/MainDbConfigReader.scala +++ b/core/src/main/scala/ru/maizy/ambient7/core/config/MainDbConfigReader.scala @@ -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._ @@ -30,6 +31,10 @@ trait MainDbConfigReader extends UniversalConfigReader { .action { (value, opts) => mainDbOpts(opts)(_.copy(url = Some(value))) } .text(s"URL for connecting to h2 database") + appendOptionalSimpleConfigRule[String]("db.url") { (value, opts) => + mainDbOpts(opts)(_.copy(url = Some(value))) + } + appendDbOptsCheck{ dbOpts => Either.cond(dbOpts.url.isDefined, (), ParsingError.withMessage("db-url is required")) } @@ -37,11 +42,19 @@ trait MainDbConfigReader extends UniversalConfigReader { .action { (value, opts) => mainDbOpts(opts)(_.copy(user = value)) } .text("database user") + appendOptionalSimpleConfigRule[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") + appendOptionalSimpleConfigRule[String]("db.password") { (value, opts) => + mainDbOpts(opts)(_.copy(password = value)) + } + () } } diff --git a/core/src/main/scala/ru/maizy/ambient7/core/config/ParsingError.scala b/core/src/main/scala/ru/maizy/ambient7/core/config/ParsingError.scala index efd7f95..1afcb29 100644 --- a/core/src/main/scala/ru/maizy/ambient7/core/config/ParsingError.scala +++ b/core/src/main/scala/ru/maizy/ambient7/core/config/ParsingError.scala @@ -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) } diff --git a/core/src/main/scala/ru/maizy/ambient7/core/config/UniversalConfigReader.scala b/core/src/main/scala/ru/maizy/ambient7/core/config/UniversalConfigReader.scala index e178274..a5bb124 100644 --- a/core/src/main/scala/ru/maizy/ambient7/core/config/UniversalConfigReader.scala +++ b/core/src/main/scala/ru/maizy/ambient7/core/config/UniversalConfigReader.scala @@ -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 { @@ -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 @@ -77,7 +83,7 @@ trait UniversalConfigReader { protected def fillConfigOptions(requireUniversalConfig: Boolean = false): Unit = { isConfigEnabled_ = true - // isConfigRequired_ = requireUniversalConfig + isConfigRequired_ = requireUniversalConfig def addConfigOption(parser: OptionParser[Ambient7Options], required: Boolean = false): Unit = { val opt = parser.opt[File]("config") @@ -101,6 +107,8 @@ trait UniversalConfigReader { def isConfigEnabled: Boolean = isConfigEnabled_ + def isConfigRequired: Boolean = isConfigRequired_ + def isCliOptionsEnabled: Boolean = isCliOptionsEnabled_ def appName: String @@ -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 appendOptionalSimpleConfigRule[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 @@ -124,6 +156,13 @@ 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) @@ -131,19 +170,23 @@ trait UniversalConfigReader { 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) diff --git a/core/src/main/scala/ru/maizy/ambient7/core/config/helper/ConfigRuleOps.scala b/core/src/main/scala/ru/maizy/ambient7/core/config/helper/ConfigRuleOps.scala new file mode 100644 index 0000000..d7e3756 --- /dev/null +++ b/core/src/main/scala/ru/maizy/ambient7/core/config/helper/ConfigRuleOps.scala @@ -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)) + } + } + } +}