All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ossuminc.riddl.commands.RepeatCommand.scala Maven / Gradle / Ivy

/*
 * Copyright 2019 Ossum, Inc.
 *
 * SPDX-License-Identifier: Apache-2.0
 */

package com.ossuminc.riddl.commands

import com.ossuminc.riddl.commands.CommandOptions.optional
import com.ossuminc.riddl.language.CommonOptions
import com.ossuminc.riddl.language.Messages.Messages
import com.ossuminc.riddl.passes.PassesResult
import com.ossuminc.riddl.utils.{Interrupt,Logger}

import pureconfig.ConfigCursor
import pureconfig.ConfigReader
import pureconfig.ConfigReader.Result
import pureconfig.error.CannotParse
import scopt.OParser

import java.io.File
import java.nio.file.Path
import scala.concurrent.duration.Duration
import scala.concurrent.duration.DurationInt
import scala.concurrent.duration.FiniteDuration
import scala.util.Success

object RepeatCommand {

  final val cmdName = "repeat"
  val defaultMaxLoops: Int = 1024

  case class Options(
    inputFile: Option[Path] = None,
    targetCommand: String = "",
    refreshRate: FiniteDuration = 10.seconds,
    maxCycles: Int = defaultMaxLoops,
    interactive: Boolean = false)
      extends CommandOptions {
    def command: String = cmdName
  }
}

class RepeatCommand
    extends CommandPlugin[RepeatCommand.Options](RepeatCommand.cmdName) {
  import RepeatCommand.Options

  /** Provide an scopt OParser for the commands options type, OPT
    *
    * @return
    *   A pair: the OParser and the default values for OPT
    */
  override def getOptions: (OParser[Unit, Options], Options) = {
    import builder.*
    cmd(RepeatCommand.cmdName)
      .text("""This command supports the edit-build-check cycle. It doesn't end
              |until  has completed or EOF is reached on standard
              |input. During that time, the selected subcommands are repeated.
              |""".stripMargin).children(
        arg[File]("config-file").required()
          .action((f, c) => c.copy(inputFile = Some(f.toPath)))
          .text("The path to the configuration file that should be repeated"),
        arg[String]("target-command").required().action { (cmd, opt) =>
          opt.copy(targetCommand = cmd)
        }.text("The name of the command to select from the configuration file"),
        arg[FiniteDuration]("refresh-rate").optional().validate {
          case r if r.toMillis < 1000 =>
            Left(" is too fast, minimum is 1 seconds")
          case r if r.toDays > 1 =>
            Left(" is too slow, maximum is 1 day")
          case _ => Right(())
        }.action((r, c) => c.copy(refreshRate = r))
          .text("""Specifies the rate at which the  is checked
                  |for updates so the process to regenerate the hugo site is
                  |started""".stripMargin),
        arg[Int]("max-cycles").optional().validate {
          case x if x < 1           => Left(" can't be less than 1")
          case x if x > 1024 * 1024 => Left(" is too big")
          case _                    => Right(())
        }.action((m, c) => c.copy(maxCycles = m))
          .text("""Limit the number of check cycles that will be repeated."""),
        opt[Unit]('n', "interactive").optional()
          .action((_, c) => c.copy(interactive = true)).text(
            """This option causes the repeat command to read from the standard
              |input and when it reaches EOF (Ctrl-D is entered) then it cancels
              |the loop to exit.""".stripMargin
          )
      ) -> Options()
  }

  // target-command = "hugo"
  //  refresh-rate = 5.seconds
  //  max-cycles = 10
  //  interactive = true

  private class OptionsReader extends ConfigReader[Options] {
    override def from(cur: ConfigCursor): Result[Options] = for
      topCur <- cur.asObjectCursor
      topRes <- topCur.atKey(pluginName)
      objCur <- topRes.asObjectCursor
      inputPathRes <- objCur.atKey("input-file")
      inputPath <- inputPathRes.asString.flatMap { inputPath =>
        if inputPath.isEmpty then {
          ConfigReader.Result.fail[String](
            CannotParse("'input-path' requires a non-empty value", None)
          )
        } else Right(inputPath)
      }
      targetCommand <- optional(objCur, "target-command", "")(_.asString)
      refreshRate <- optional(objCur, "refresh-rate", "10s")(_.asString)
        .flatMap { rr =>
          val dur = Duration.create(rr)
          if dur.isFinite then { Right(dur.asInstanceOf[FiniteDuration]) }
          else {
            ConfigReader.Result.fail[FiniteDuration](CannotParse(
              s"'refresh-rate' must be a finite duration, not $rr",
              None
            ))
          }
        }
      maxCycles <- optional(objCur, "max-cycles", 100)(_.asInt)
      interactive <- optional(objCur, "interactive", true)(_.asBoolean)
    yield {
      RepeatCommand.Options(
        Some(Path.of(inputPath)),
        targetCommand,
        refreshRate,
        maxCycles,
        interactive
      )
    }
  }
  override def getConfigReader: ConfigReader[Options] = { new OptionsReader }


  /** Execute the command given the options. Error should be returned as
    * Left(messages) and not directly logged. The log is for verbose or debug
    * output
    *
    * @param options
    *   The command specific options
    * @param commonOptions
    *   The options common to all commands
   * @param log
   *    A logger for logging errors, warnings, and info
   * @return
   * Either a set of Messages on error or a Unit on success
   */
  override def run(
    options: Options,
    commonOptions: CommonOptions,
    log: Logger,
    outputDirOverride: Option[Path]
  ): Either[Messages, PassesResult] = {
    val maxCycles = options.maxCycles
    val refresh = options.refreshRate
    val sleepTime = refresh.toMillis
    val (shouldQuit, cancel) = Interrupt.allowCancel(options.interactive)

    def userHasCancelled: Boolean = shouldQuit.isCompleted &&
      shouldQuit.value == Option(Success(true))

    var shouldContinue = true
    var i: Int = 0
    while i < maxCycles && shouldContinue && !userHasCancelled do {
      val result = CommandPlugin.runFromConfig(
        options.inputFile,
        options.targetCommand,
        commonOptions,
        log,
        pluginName
      ).map { _ =>
        if !userHasCancelled then {
          cancel.map(_.apply())
          shouldContinue = false
        } else {
          i += 1
          if commonOptions.verbose then {
            println(s"Waiting for $refresh, cycle # $i of $maxCycles")
          }
          Thread.sleep(sleepTime)
        }
      }
      if result.isLeft then { shouldContinue = false }
    }
    Right(PassesResult())
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy