Tuesday, February 26, 2013

A Simple Scala Internal DSL

My earlier posts have talked about why I'm using Scala to create a DSL for documenting user interface procedures.  I also introduced Sikuli and posted a simple Java program for capturing on-screen images and then later using the Sikuli vision library to find them on the screen and click them.

In this post, I will model a very simple Internal DSL in Scala that can execute this script:
Click the "OK".button;

As you can see, as is the case with internal DSLs, I couldn't quite get the syntax the way that I wanted, but it's pretty close.  A lesson learned is that you can use internal DSLs in situations where you aren't too picky about the syntax.  In my next post I will share an external DSL that has the exact syntax that I want (and some other benefits also).

Here's how to read the code (at the bottom of this post):

  • Imports
  • I create a Scala object (MehtodRunner) that extends App (basic Scala stuff)
  • I create the Sikuli screen object (main object for executing Sikuli commands)
  • I create the "Click" object (the first word in my syntax example above)
  • I define a "the" method (second word in my syntax) which takes in a single Component as a parameter.
    • When called, it creates an image filename and checks to see if the file exists or not.
    • If it exists, it asks Sikuli to find a matching image on the screen and click it.
    • If it does not exist, it asks Sikuli to capture the image and store it for the next time.  See my last post for a more in depth explanation of how this works.
  • The next section converts the "OK".button syntax into a Component object.  It's basic Scala Internal DSL stuff.  For more information on this topic, take a look at Designing Internal DSLs in Scala by Debasish Ghosh.
  • There is a section that does the work of capturing screenshots in Sikuli (called from "the" method above).  Again, see my last post for an explanation.
  • Finally, I embed the Click the "OK".button; script inline.  This is one of the benefits (and drawbacks) of an internal DSL.



----

import org.sikuli.script.ScreenHighlighter
import org.sikuli.script.Screen
import org.sikuli.script.Location
import org.sikuli.script.CapturePrompt
import org.sikuli.script.Observer
import org.sikuli.script.Subject
import java.io.File
import org.sikuli.script.Pattern
import javax.imageio.ImageIO

object MethodRunner extends App {

  // =============================
  // Sikuli screen object
  var screen = new Screen()

  // =============================
  // Script Actions  
  object Click {
    def the(in: Component) {
      println("Click the " + compDesc(in))

      var filename = safeFilename(in);
      println(filename)
      var file = new File(filename);

      if (file.exists()) {
        screen.click(new Pattern(filename));
      } else {
        captureComponent = in
        cp.prompt("I don't know what the '" + compDesc(in) +
          "' looks like.  Please select it.");
      }
    }
  }

  // =============================
  // this supports the syntax "OK".button
  implicit def toComponentBuilder(in: String) =
    new ComponentBuilder(in)
  class ComponentBuilder(name: String) {
    def button(): Component = {
      return new button(name);
    }
  }

  abstract class Component(val name: String)
  class button(name: String) extends Component(name)
  def compDesc(in: Component) = in.name + "." +
    in.getClass().getSimpleName()

  // =============================
  // capture and store screenshots for Sikuli 
  var cp = new CapturePrompt(screen)
  cp.addObserver(CaptureObserver)
  var captureComponent: Component = null

  var USER_HOME = System.getProperty("user.home");
  def safeFilename(comp: Component) = USER_HOME +
    "/Method/img/cache/" + comp.name.replaceAll("\\W+", "_") +
    "_" + comp.getClass().getSimpleName() + ".png";

  object CaptureObserver extends Observer {
    def update(s: Subject) {
      var img = cp.getSelection()
      ImageIO.write(img.getImage(), "png", 
          new File(safeFilename(captureComponent)));
      cp.close()
    }
  }

  // =============================
  // The Script

  Click the "OK".button;

}

No comments: