Permalink
Browse files

More work on Commands.

  • Loading branch information...
1 parent dc0458c commit 0389762b04fd4029b2aa94363d2d3ce0044ff7c1 Dean Wampler committed Dec 8, 2008
View
@@ -0,0 +1 @@
+build
@@ -0,0 +1,143 @@
+package sake.command
+
+import org.specs._
+
+object CommandSpec extends Specification {
+
+ import sake.command._
+ import sake.util._
+
+ def checkOption[A,B](opts: Map[A,B], key: A, expected: B) = opts.get(key) match {
+ case Some(expected) =>
+ case x => fail("for key: "+key+", "+x+" != Some("+expected+")")
+ }
+
+ "Constructing a Command with just a name" should {
+ "set the name" in {
+ (new Command("command")).name must be_==("command")
+ }
+ "have no default options" in {
+ (new Command("command")).defaultOptions match {
+ case None =>
+ case Some(opts) => fail()
+ }
+ }
+ }
+
+ "Constructing a Command with a name and default options map" should {
+ "set the name" in {
+ (new Command("command", Map[Symbol,String]('x -> "x", 'y -> "y"))).name must be_==("command")
+ }
+ "set the default options" in {
+ (new Command("command", Map[Symbol,String]('x -> "x", 'y -> "y"))).defaultOptions match {
+ case None => fail ()
+ case Some(opts) => {
+ opts.size must be_==(2)
+ checkOption(opts, 'x, "x")
+ checkOption(opts, 'y, "y")
+ }
+ }
+ }
+ }
+
+ "Constructing a Command with a name and default options pairs" should {
+ "set the default options" in {
+ (new Command("command", 'x -> "x", 'y -> "y")).defaultOptions match {
+ case None => fail ()
+ case Some(opts) => {
+ opts.size must be_==(2)
+ checkOption(opts, 'x, "x")
+ checkOption(opts, 'y, "y")
+ }
+ }
+ }
+ }
+
+ "Invoking a Command" should {
+ "merge the passed-in options with the default options, overriding the later" in {
+ val c = new Command("command", Map('x -> "x", 'y -> "y")) {
+ override def optionsPostFilter(opts: Map[Symbol,String]) = {
+ checkOption(opts, 'x, "x2")
+ checkOption(opts, 'y, "y")
+ opts
+ }
+ }
+ c('x -> "x2")
+ }
+
+ "throw a BuildError if the passed-in options are missing required options" in {
+ val c = new Command("command", Map('x -> "x")) {
+ override val requiredOptions = List('y)
+ }
+ c('x -> "x2") must throwA[BuildError]
+ }
+
+ "invoke the action of the command" in {
+ var invoked = false
+ val c = new Command("command", Map('x -> "x")) {
+ override def action(result: Result, options: Map[Symbol, String]) = {
+ invoked = true
+ result
+ }
+ }
+ c()
+ invoked must be_==(true)
+ }
+
+ "invoke a user-specified 'and' block after the action of the command" in {
+ var invoked = List[Int]()
+ val c = new Command("command", Map('x -> "x")) {
+ override def action(result: Result, options: Map[Symbol, String]) = {
+ invoked ::= 1
+ result
+ }
+ }
+ c() and { result =>
+ invoked ::= 2
+ result
+ }
+ invoked match {
+ case 2 :: List(1) =>
+ case _ => fail(invoked.toString())
+ }
+ }
+ }
+
+ "Subclasses" should {
+ "be able to filter the specified options" in {
+ var invoked = false
+ val c = new Command[Symbol,String]("command") {
+ override def optionsPostFilter(opts: Map[Symbol,String]) = {
+ invoked = true
+ opts
+ }
+ }
+ c('x -> "x2")
+ invoked must be_==(true)
+ }
+
+ "be able to define the command's action'" in {
+ var invoked = false
+ val c = new Command[Symbol,String]("command") {
+ override def action(result: Result, options: Map[Symbol, String]) = {
+ invoked = true
+ result
+ }
+ }
+ c('x -> "x2")
+ invoked must be_==(true)
+ }
+
+ "be able to filter the result to be returned by 'action'" in {
+ var invoked = false
+ val c = new Command[Symbol,String]("command") {
+ override def postFilterResult(result: Result) = {
+ invoked = true
+ result
+ }
+ }
+ c('x -> "x2")
+ invoked must be_==(true)
+ }
+ }
+}
@@ -0,0 +1,50 @@
+package sake.command
+
+import org.specs._
+
+object ResultSpec extends Specification {
+
+ import sake.command._
+ import sake.util._
+
+ "A successful result" should {
+ "be true" in {
+ (new Passed()).success must be_==(true)
+ }
+ }
+
+ "A failed result" should {
+ "be false" in {
+ (new Failed()).success must be_==(false)
+ }
+ }
+
+ "A result" should {
+ "have an optional result" in {
+ (new Passed()).result must be_==(None)
+ (new Passed(Some("success"))).result match {
+ case Some("success") =>
+ case _ => fail()
+ }
+ (new Passed(Some(List("success")))).result match {
+ case Some(List("success")) =>
+ case _ => fail()
+ }
+ }
+ "have an optional message" in {
+ (new Passed()).message must be_==(None)
+ (new Passed(None, Some("success"))).message match {
+ case Some("success") =>
+ case _ => fail()
+ }
+ }
+ "accept an optional closure for post-processing of the event" in {
+ val r1 = new Passed()
+ val r2 = r1 and { result =>
+ new Failed()
+ }
+ r2.success must be_==(false)
+ (r1 eq r2) must be_==(false)
+ }
+ }
+}
@@ -2,51 +2,58 @@ package sake.command
import sake.util._
-case class Result[R](success: Boolean, result: Option[R], message: String)
-
-class Command[R](val name: String, val defaultOptions: Map[Any, Any]) {
- def apply(options: Map[Any, Any]) = {
+class Command[A,B](val name: String, val defaultOptions: Option[Map[A,B]]) {
+
+ def this(name:String, defaultOpts: Map[A,B]) = this(name, Some(defaultOpts))
+ def this(name:String, defaultOpt0: (A,B), defaultOpts: (A,B)*) =
+ this(name, Map[A,B](defaultOpt0)++defaultOpts)
+ def this(name:String) = this(name, None)
+
+ def apply(options: (A,B)*) = {
+ val opts = defaultOptions match {
+ case Some(map) => map ++ options
+ case None => Map[A,B]() ++ options
+ }
postFilterResult(
action(
- new Result[R](true, None, ""),
- filterOptions(defaultOptions ++ options)))
+ new Passed(),
+ filterOptions(opts)))
}
-
+
/**
* Override as needed in subclasses, The user specified options will be checked to confirm
* that any required options are present.
*/
- val requiredOptions: List[String] = Nil
+ val requiredOptions: List[A] = Nil
- /**
- * Override as needed in subclasses, _e.g.,_ to handle option "aliases" or
- * to very required options are present.
- */
- def filterOptions(options: Map[Any, Any]) = {
+ private def filterOptions(options: Map[A,B]) = {
val missingOptions = for {
key <- requiredOptions
if (! options.contains(key))
} yield key
if (missingOptions.length > 0)
throw new BuildError(name+" requires option(s) "+missingOptions)
- postFilterOptions(options)
+ optionsPostFilter(options)
}
/**
* Override as needed in subclasses, _e.g.,_ to handle option "aliases" or
* to verify the _values_ for certain _keys_ are valid.
*/
- def postFilterOptions(options: Map[Any, Any]) = options
+ protected def optionsPostFilter(options: Map[A,B]) = options
/**
* Override as needed in subclasses. This is the method that does the normal work
* of the command. The override can discard the passed in result or return it, if
- * the action was successful.
+ * the action does nothing.
*/
- def action[R](successfulResult: Result[R], options: Map[Any, Any]) = successfulResult
+ protected def action(result: Result, options: Map[A,B]) = result
/**
* Override as needed in subclasses. Perhaps to recover from an error.
+ * Users can post-process the result using the "command(...) and { result => ... }"
+ * idiom.
*/
- def postFilterResult[R](result: Result[R]) = result
-}
+ protected def postFilterResult(result: Result) = result
+}
+
@@ -0,0 +1,27 @@
+package sake.command
+
+import sake.util._
+
+abstract class Result {
+ val success: Boolean
+
+ /**
+ * Allow the user to specify an optional closure after a command that is effectively a
+ * post-processing hook. The Result of the command's execution is passed as the one argument.
+ */
+ def and(postAction: (Result) => Result) = postAction(this)
+}
+
+case class Passed[R](result: Option[R], message: Option[String]) extends Result {
+ override val success = true
+
+ def this(result: Option[R]) = this(result, None)
+ def this() = this(None)
+}
+
+case class Failed[R](result: Option[R], message: Option[String]) extends Result {
+ override val success = false
+
+ def this(result: Option[R]) = this(result, None)
+ def this() = this(None)
+}
@@ -1,9 +1,13 @@
package sake.command.builtin
-
-class ScalaCommand(name: String, defaultOptions: Map[Any, Any])
- extends Command[String](name, defaultOptions) {
+/*
+private class ScalaCommand(name: String, defaultOptions: Map[Symbol, Any])
+ extends Command(name, defaultOptions) {
+
+ override val requiredOptions = List('command)
}
-
object ScalaCommand {
- val scala = new ScalaCommand("scala", Map('command -> ""))
+ val scala = new Command("scala") {
+ override val requiredOptions = List[Any]('command)
+ }
}
+*/

0 comments on commit 0389762

Please sign in to comment.