Skip to content

Commit

Permalink
iss #24: universal config: append usage, extract result types
Browse files Browse the repository at this point in the history
  • Loading branch information
maizy committed Jan 10, 2017
1 parent 5d93d75 commit 17b360a
Show file tree
Hide file tree
Showing 9 changed files with 230 additions and 58 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@ object WebAppLauncher extends App {
val config = WebAppConfigReader
config.fillReader()
val eitherAppConfig = config.readAppConfig(args.toIndexedSeq)
println(eitherAppConfig)
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("")
)
case Right(opts) =>
println(s"Success: $opts")
}

// TODO: launch jetty app (merge with JettyLauncher)
}
1 change: 1 addition & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ val scalacOpts = Seq(
"-encoding", "UTF-8",
"-deprecation",
"-unchecked",
"-feature",
"-explaintypes",
"-Xfatal-warnings",
"-Xlint:_",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,8 @@ import java.nio.file.Path
case class Ambient7Options(
universalConfigPath: Option[Path] = None,
influxDb: Option[InfluxDbOptions] = None,
mainDb: Option[DbOptions] = None
mainDb: Option[DbOptions] = None,

// TODO: fix this little hack
showHelp: Boolean = false
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ package ru.maizy.ambient7.core.config
* Copyright (c) Nikita Kovaliov, maizy.ru, 2016
* See LICENSE.txt for details.
*/

trait InfluxDbConfigReader extends UniversalConfigReader {

private def influxDbOpts(opts: Ambient7Options)(fill: InfluxDbOptions => InfluxDbOptions): Ambient7Options = {
opts.copy(influxDb = Some(fill(opts.influxDb.getOrElse(InfluxDbOptions()))))
}

private def appendInfluxDbOptsCheck(check: InfluxDbOptions => Either[IndexedSeq[String], Unit]): Unit =
private def appendInfluxDbOptsCheck(check: InfluxDbOptions => Either[ParsingError, Unit]): Unit =
appendCheck { appOpts =>
appOpts.influxDb match {
case Some(influxDbOptions) => check(influxDbOptions)
case _ => Left(IndexedSeq("InfluxDB opts not defined"))
case _ => Left(ParsingError.withMessage("InfluxDB opts not defined"))
}
}

Expand All @@ -38,23 +39,23 @@ trait InfluxDbConfigReader extends UniversalConfigReader {
.required()

appendInfluxDbOptsCheck({
opts => Either.cond(opts.database.isDefined, (), IndexedSeq("influxdb-database is required"))
opts => Either.cond(opts.database.isDefined, (), ParsingError.withMessage("influxdb-database is required"))
})


cliParser.opt[String]("influxdb-user")
.action { (value, opts) => influxDbOpts(opts)(_.copy(user = Some(value))) }

appendInfluxDbOptsCheck({
opts => Either.cond(opts.user.isDefined, (), IndexedSeq("influxdb-user is required"))
opts => Either.cond(opts.user.isDefined, (), ParsingError.withMessage("influxdb-user is required"))
})


cliParser.opt[String]("influxdb-password")
.action { (value, opts) => influxDbOpts(opts)(_.copy(password = Some(value))) }

appendInfluxDbOptsCheck({
opts => Either.cond(opts.password.isDefined, (), IndexedSeq("influxdb-password is required"))
opts => Either.cond(opts.password.isDefined, (), ParsingError.withMessage("influxdb-password is required"))
})


Expand All @@ -68,7 +69,11 @@ trait InfluxDbConfigReader extends UniversalConfigReader {
.text("By default --influxdb-user")

appendInfluxDbOptsCheck({
opts => Either.cond(opts.readonlyUser.isDefined, (), IndexedSeq("influxdb-readonly-user is required"))
opts => Either.cond(
opts.readonlyUser.isDefined,
(),
ParsingError.withMessage("influxdb-readonly-user is required")
)
})


Expand All @@ -77,7 +82,11 @@ trait InfluxDbConfigReader extends UniversalConfigReader {
.text("By default --influxdb-password")

appendInfluxDbOptsCheck({
opts => Either.cond(opts.readonlyPassword.isDefined, (), IndexedSeq("influxdb-readonly-password is required"))
opts => Either.cond(
opts.readonlyPassword.isDefined,
(),
ParsingError.withMessage("influxdb-readonly-password is required")
)
})

// TODO: uni config rules
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package ru.maizy.ambient7.core.config
*/
trait MainDbConfigReader extends UniversalConfigReader {

import UniversalConfigReader._

private def mainDbOpts(opts: Ambient7Options)(fill: DbOptions => DbOptions): Ambient7Options = {
opts.copy(mainDb = Some(fill(opts.mainDb.getOrElse(DbOptions()))))
}
Expand All @@ -14,7 +16,7 @@ trait MainDbConfigReader extends UniversalConfigReader {
appendCheck { appOpts =>
appOpts.mainDb match {
case Some(dbOpts) => check(dbOpts)
case _ => Left(IndexedSeq("DB opts not defined"))
case _ => Left(ParsingError.withMessage("DB opts not defined"))
}
}

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

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


cliParser.opt[String]("db-user")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package ru.maizy.ambient7.core.config

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

case class ParsingError(messages: IndexedSeq[String] = IndexedSeq.empty, usage: Option[String] = None) {
def merge(other: ParsingError): ParsingError = {
val mergedUsage = (usage, other.usage) match {
case (Some(text), Some(otherText)) => Some(text + "\n" + otherText)
case (None, otherUsage@Some(_)) => otherUsage
case (thisUsage@Some(_), None) => thisUsage
case _ => None
}
copy(messages ++ other.messages, mergedUsage)
}

def appendUsage(additionalUsage: String): ParsingError =
copy(usage = Some(Seq(usage.getOrElse(""), additionalUsage).mkString("\n")))
}

object ParsingError {
def merge(a: ParsingError, b: ParsingError): ParsingError = a merge b

def withMessage(message: String): ParsingError =
ParsingError(IndexedSeq(message))

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

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,74 @@ import java.nio.file.Files
import scala.annotation.tailrec
import scopt.OptionParser

trait UniversalConfigReader {

type CheckResult = Either[IndexedSeq[String], Unit]
object UniversalConfigReader {
type CheckResult = Either[ParsingError, Unit]
type Check = Ambient7Options => CheckResult
type ParseResult = Either[IndexedSeq[String], Ambient7Options]
type ParseResult = Either[ParsingError, Ambient7Options]
type Postprocessor = Ambient7Options => ParseResult
}

trait UniversalConfigReader {
import UniversalConfigReader._

private var checks_ = List[Check]()
private var postprocessors_ = List[Postprocessor]()

private var isConfigEnabled_ = false
// private var isConfigRequired_ = false

private val isCliOptionsEnabled_ = true

protected val cliParser = new OptionParser[Ambient7Options](appName) {
help("help")
class SilentOptionParser[T](programmName: String) extends OptionParser[T](programmName) {
private var savedErrors: IndexedSeq[String] = IndexedSeq.empty
private var savedWarning: IndexedSeq[String] = IndexedSeq.empty

override def showUsage(): Unit = {}
override def showUsageAsError(): Unit = {}

override def reportError(msg: String): Unit = {
savedErrors = savedErrors :+ msg
()
}

override def reportWarning(msg: String): Unit = {
savedWarning = savedWarning :+ msg
()
}

def appendUsageToParserError(parser: ParsingError): ParsingError = {
val pattern = """(?ims)usage:\s+(.*)""".r
usage match {
case pattern(content) => parser.appendUsage(content)
case _ => parser.appendUsage(usage)
}
}

}

protected val cliParser = new SilentOptionParser[Ambient7Options](appName) {
opt[Unit]("help")
.abbr("h")
.action((_, opts) => opts.copy(showHelp = true))

override def showUsageOnError: Boolean = true
}

protected val configPathCliParser = new OptionParser[Ambient7Options](appName) {
protected val configPathCliParser = new SilentOptionParser[Ambient7Options](appName) {
override val errorOnUnknownArgument = false

override def reportError(msg: String): Unit = {}

override def reportWarning(msg: String): Unit = {}

opt[Unit]("help")
.abbr("h")
.action((_, opts) => opts.copy(showHelp = true))
}

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

def addConfigOption(parser: OptionParser[Ambient7Options], required: Boolean = false): Unit = {
val opt = parser.opt[File]("config")
Expand All @@ -50,7 +88,7 @@ trait UniversalConfigReader {
if (required) {
opt.required()
appendCheck { opts =>
Either.cond(opts.universalConfigPath.isDefined, (), IndexedSeq("config path is required"))
Either.cond(opts.universalConfigPath.isDefined, (), ParsingError.withMessage("config path is required"))
}
}

Expand Down Expand Up @@ -78,26 +116,38 @@ trait UniversalConfigReader {
protected def appendPostprocessor(postprocessor: Postprocessor): Unit =
postprocessors_ = postprocessor :: postprocessors_

private def processHelpOption(result: ParseResult): ParseResult = {
result match {
// show usage if --help option exists
case Right(opts) if opts.showHelp => Left(ParsingError())
case _ => result
}
}

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

// FIXME: implements

Right(optsWithConfigPath)
} else {
val message = s"unable to read config from $universalConfigPath"
configLogger.error(message)
Left(IndexedSeq(message))
}
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)
} else {
val message = s"unable to read config from $universalConfigPath"
configLogger.error(message)
Left(ParsingError.withMessage(message))
}

case _ =>
configLogger.info("universal config path not defined")
Right(opts)
case _ =>
configLogger.info("universal config path not defined")
Right(opts)
}
}
}

Expand All @@ -111,7 +161,7 @@ trait UniversalConfigReader {
private def checkConfig(opts: Ambient7Options): ParseResult =
{
@tailrec
def untilError(xs: List[Check]): Option[IndexedSeq[String]] = {
def untilError(xs: List[Check]): Option[ParsingError] = {
xs match {
case Nil => None
case check :: tail =>
Expand All @@ -136,7 +186,13 @@ trait UniversalConfigReader {
if (isCliOptionsEnabled) {
eitherAppConfig = eitherAppConfig.right.flatMap(opts => parseCliOptions(args, opts))
}
eitherAppConfig.right.flatMap(opts => checkConfig(opts))
eitherAppConfig = processHelpOption(eitherAppConfig)
eitherAppConfig = eitherAppConfig.right.flatMap(opts => checkConfig(opts))
eitherAppConfig match {
case config@Right(_) => config
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
@@ -1,11 +1,12 @@
package ru.maizy.ambient7.core

import com.typesafe.scalalogging.{ LazyLogging, Logger }

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

import com.typesafe.scalalogging.{ LazyLogging, Logger }

package object config extends LazyLogging {
val configLogger: Logger = logger
}
Loading

0 comments on commit 17b360a

Please sign in to comment.