Skip to content

Commit

Permalink
Add Claper
Browse files Browse the repository at this point in the history
  • Loading branch information
mattroberts297 committed Apr 13, 2017
1 parent e5432b3 commit d46e767
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 5 deletions.
34 changes: 32 additions & 2 deletions README.md
Expand Up @@ -5,8 +5,38 @@

A Command Line Argument Parser without the boiler plate.

## Getting Started

Add the library as a dependency in your project's `build.sbt` file:

```scala
scalaVersion := "2.12.1"

libraryDependencies ++= Seq(
"io.mattroberts" %% "claper" % "0.2.0"
)
```

Then use it to parse command line arguments:

```scala
import io.mattroberts.Claper
case class Args(alpha: String, beta: Int, charlie: Boolean)
val args = List("--alpha", "alpha", "--beta", "1", "--charlie")
val parsed = Claper[Args].parse(args)
println(parsed) // Right(Args("alpha", 1, true))
```

## Usage

For this style: `my-app --alpha a --beta 1 --charlie true`, see [LabelledParserSpec](src/test/scala/io/mattroberts/LabelledParserSpec.scala).
See [ClaperSpec](src/test/scala/io/mattroberts/ClaperSpec.scala) for full usage.

## Features

- Support for case classes (products)
- Support for default values
- Support for Linux style arguments

## Future Features

For this style: `my-app a 1 true`, see [ParserSpec](src/test/scala/io/mattroberts/ParserSpec.scala).
- In the future I might add coproduct support
2 changes: 1 addition & 1 deletion build.sbt
@@ -1,7 +1,7 @@
lazy val root = (project in file(".")).settings(
name := "claper",
organization := "io.mattroberts",
version := "0.1.0",
version := "0.2.0",
scalaVersion := "2.12.1",
scalacOptions := Seq("-deprecation", "-feature", "-unchecked"),
libraryDependencies ++= Seq(
Expand Down
129 changes: 129 additions & 0 deletions src/main/scala/io/mattroberts/Claper.scala
@@ -0,0 +1,129 @@
package io.mattroberts

import shapeless._
import shapeless.labelled._

final case class ClaperError(message: String) {
def msg: String = message
}

trait Claper[A] {
import Claper.Or
def parse(args: Seq[String]): ClaperError Or A
}

object Claper {
type Or[A, B] = Either[A, B]

def apply[A](
implicit
st: Lazy[Claper[A]]
): Claper[A] = st.value

def create[A](thunk: Seq[String] => ClaperError Or A): Claper[A] = {
new Claper[A] {
def parse(args: Seq[String]): ClaperError Or A = thunk(args)
}
}

implicit def genericParser[A, R <: HList, D <: HList](
implicit
defaults: Default.AsOptions.Aux[A, D],
generic: LabelledGeneric.Aux[A, R],
parser: Lazy[UnderlyingClaper[R, D]]
): Claper[A] = {
create { args => parser.value.parse(args, defaults()).map(generic.from) }
}
}

trait UnderlyingClaper[A, B] {
import Claper.Or

def parse(args: Seq[String], defaults: B): ClaperError Or A
}

object UnderlyingClaper {
import Claper.Or

def apply[A, B](
implicit
st: Lazy[UnderlyingClaper[A, B]]
): UnderlyingClaper[A, B] = st.value

def create[A, B](thunk: (Seq[String], B) => ClaperError Or A): UnderlyingClaper[A, B] = {
new UnderlyingClaper[A, B] {
def parse(args: Seq[String], defaults: B): ClaperError Or A = thunk(args, defaults)
}
}

implicit val hnilParser: UnderlyingClaper[HNil, HNil] = {
create { (_, _) => Right(HNil) }
}

implicit def hlistParser[K <: Symbol, H, T <: HList, TD <: HList](
implicit
witness: Witness.Aux[K],
hParser: Lazy[UnderlyingClaper[FieldType[K, H], Option[H]]],
tParser: UnderlyingClaper[T, TD]
): UnderlyingClaper[FieldType[K, H] :: T, Option[H] :: TD] = {
create { (args, defaults) =>
val hv = hParser.value.parse(args, defaults.head)
val tv = tParser.parse(args, defaults.tail)
for {
h <- hv
t <- tv
} yield h :: t
}
}

implicit def stringParser[K <: Symbol](
implicit
witness: Witness.Aux[K]
): UnderlyingClaper[FieldType[K, String], Option[String]] = {
val name = witness.value.name
create { (args, defaultArg) =>
val providedArg = getArgFor(args, name)
providedArg.map(Right(_)).getOrElse(
defaultArg.map(Right(_)).getOrElse(
Left(ClaperError(s"Missing argument $name"))
)
).map(field[K](_))
}
}

implicit def intParser[K <: Symbol](
implicit
witness: Witness.Aux[K]
): UnderlyingClaper[FieldType[K, Int], Option[Int]] = {
val name = witness.value.name
create { (args, defaultArg) =>
val providedArg = getArgFor(args, name).map(_.toInt)
providedArg.map(Right(_)).getOrElse(
defaultArg.map(Right(_)).getOrElse(
Left(ClaperError(s"Missing argument $name"))
)
).map(field[K](_))
}
}

implicit def booleanParser[K <: Symbol](
implicit
witness: Witness.Aux[K]
): UnderlyingClaper[FieldType[K, Boolean], Option[Boolean]] = {
val name = witness.value.name
create { (args, default) =>
val arg = args.find(a => a == s"--$name").isDefined
Right(field[K](arg))
}
}

private def getArgFor(args: Seq[String], name: String): Option[String] = {
val indexOfName = args.indexOf(s"--$name")
val indexAfterName = indexOfName + 1
if (indexOfName > -1 && args.isDefinedAt(indexAfterName)) {
Some(args(indexAfterName))
} else {
None
}
}
}
33 changes: 33 additions & 0 deletions src/test/scala/io/mattroberts/ClaperSpec.scala
@@ -0,0 +1,33 @@
package io.mattroberts

import org.scalatest.{MustMatchers, FlatSpec}

class ClaperSpec extends FlatSpec with MustMatchers {
"Claper" must "parse all args" in {
case class Args(alpha: String, beta: Int, charlie: Boolean)
val args = List("--alpha", "alpha", "--beta", "1", "--charlie")
val parsed = Claper[Args].parse(args)
parsed must be (Right(Args("alpha", 1, true)))
}

it must "use defaults" in {
case class Args(alpha: String = "alpha", beta: Int = 1, charlie: Boolean)
val args = List.empty[String]
val parsed = Claper[Args].parse(args)
parsed must be (Right(Args("alpha", 1, false)))
}

it must "error on missing strings" in {
case class Args(alpha: String, beta: Int, charlie: Boolean)
val args = List("--beta", "1", "--charlie")
val parsed = Claper[Args].parse(args)
parsed must be (Left(ClaperError("Missing argument alpha")))
}

it must "error on missing ints" in {
case class Args(alpha: String, beta: Int, charlie: Boolean)
val args = List("--alpha", "alpha", "--charlie")
val parsed = Claper[Args].parse(args)
parsed must be (Left(ClaperError("Missing argument beta")))
}
}
2 changes: 0 additions & 2 deletions src/test/scala/io/mattroberts/DefaultedParserSpec.scala
Expand Up @@ -4,9 +4,7 @@ import org.scalatest.{MustMatchers, FlatSpec}

class DefaultedParserSpec extends FlatSpec with MustMatchers {
"A DefaultedParser" must "parse SimpleArguments" in {
import shapeless._
val args = List("--beta", "1", "--charlie")
val default = Default[SimpleArguments]
val parsed = DefaultedParser[SimpleArguments].parse(args)
parsed must be (SimpleArguments("alpha", 1, true))
}
Expand Down

0 comments on commit d46e767

Please sign in to comment.