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

caseapp.core.CommandParser.scala Maven / Gradle / Ivy

package caseapp.core

import caseapp.CommandName
import caseapp.core.util.pascalCaseSplit
import caseapp.util.AnnotationOption

import shapeless.labelled.{ FieldType, field }
import shapeless.{ :+:, Inl, Inr, Coproduct, CNil, LabelledGeneric, Witness, Strict }

trait CommandParser[T] {
  def get(command: String): Option[Parser[T]]

  def withHelp: CommandParser[WithHelp[T]] =
    CommandParser.instance { c =>
      get(c).map(_.withHelp)
    }

  def apply[D: Parser](args: Seq[String]): Either[String, (D, Seq[String], Option[Either[String, (String, T, Seq[String])]])] =
    detailedParse(args).right.map {
      case (d, args, cmdOpt) =>
        (d, args, cmdOpt.map(_.right.map {
          case (cmd, t, rem, extra) =>
            (cmd, t, rem ++ extra)
        }))
    }

  def detailedParse[D: Parser](args: Seq[String]): Either[String, (D, Seq[String], Option[Either[String, (String, T, Seq[String], Seq[String])]])] = {
    val dp = Parser[D]

    def helper(
      current: dp.D,
      args: Seq[String]
    ): Either[String, (D, Seq[String], Seq[String])] =
      if (args.isEmpty)
        dp.get(current).right.map((_, Nil, args))
      else
        dp.step(args, current) match {
          case Right(None) =>
            args match {
              case "--" :: t =>
                dp.get(current).right.map((_, t, Nil))
              case opt :: t if opt startsWith "-" =>
                Left(s"Unrecognized argument: $opt")
              case rem =>
                dp.get(current).right.map((_, Nil, rem))
            }

          case Right(Some((newD, newArgs))) =>
            assert(newArgs != args)
            helper(newD, newArgs)

          case Left(msg) =>
            Left(msg)
        }

    helper(dp.init, args.toList).right.map { case (d, dArgs, rem) =>
      val cmdOpt = rem.toList match {
        case c :: rem0 =>
          get(c) match {
            case None =>
              Some(Left(s"Command not found: $c"))
            case Some(p) =>
              Some(p.detailedParse(rem0).right.map { case (t, trem, trem0) => (c, t, trem, trem0) })
          }
        case Nil =>
          None
      }

      (d, dArgs, cmdOpt)
    }
  }

  def map[U](f: T => U): CommandParser[U] =
    CommandParser.instance { c =>
      get(c).map(_.map(f))
    }
}

object CommandParser {
  def apply[T](implicit parser: CommandParser[T]): CommandParser[T] = parser

  def instance[T](f: String => Option[Parser[T]]): CommandParser[T] =
    new CommandParser[T] {
      def get(command: String) = f(command)
    }

  implicit val cnil: CommandParser[CNil] =
    instance(_ => None)

  implicit def ccons[K <: Symbol, H, T <: Coproduct]
   (implicit
     key: Witness.Aux[K],
     commandName: AnnotationOption[CommandName, H],
     parser: Strict[Parser[H]],
     tail: CommandParser[T]
   ): CommandParser[FieldType[K, H] :+: T] =
    instance {
      val name = commandName().map(_.commandName).getOrElse {
        pascalCaseSplit(key.value.name.toList.takeWhile(_ != '$'))
          .map(_.toLowerCase)
          .mkString("-")
      }

      val tail0 = tail.map(Inr(_): FieldType[K, H] :+: T)

      c =>
        if (c == name)
          Some(parser.value.map(h => Inl(field[K](h))))
        else
          tail0.get(c)
    }

  implicit def generic[S, C <: Coproduct]
   (implicit
     lgen: LabelledGeneric.Aux[S, C],
     underlying: Strict[CommandParser[C]]
   ): CommandParser[S] =
    underlying.value.map(lgen.from)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy