Frage

I'd like to create a DSL with syntax like:

Graph.make {
    foo {
        bar()
        definedMethod1() // isn't missing!
    }
    baz()
}

Where when the handler for this tree encounters the outermost closure, it creates an instance of some class, which has some defined methods and also its own handler for missing methods.

I figured this would be easy enough with some structure like:

public class Graph {
    def static make(Closure c){
        Graph g = new Graph()
        c.delegate = g
        c()
    }

    def methodMissing(String name, args){
        println "outer " + name
        ObjImpl obj = new ObjImpl(type: name)
        if(args.length > 0 && args[0] instanceof Closure){
            Closure closure = args[0]
            closure.delegate = obj
            closure()
        }
    }

    class ObjImpl {
        String type
        def methodMissing(String name, args){
            println "inner " + name
        }
        def definedMethod1(){ 
                println "exec'd known method"
        }
    }
}

But the methodMissing handler interprets the entire closure inside Graph rather than delegating the inner closure to ObjImpl, yielding output:

outer foo
outer bar
exec'd known method
outer baz

How do I scope the missing method call for the inner closure to the inner object that I create?

War es hilfreich?

Lösung 3

There are at least two problems with this approach:

  1. Defining ObjImpl within the same context as Graph means that any missingMethod call will hit Graph first
  2. Delegation appears to happen locally unless a resolveStrategy is set, e.g.:

    closure.resolveStrategy = Closure.DELEGATE_FIRST
    

Andere Tipps

The easy answer is to set the inner closure's resolveStrategy to "delegate first", but doing that when the delegate defines a methodMissing to intercept all method calls has the effect of making it impossible to define a method outside the closure and call it from inside, e.g.

def calculateSomething() {
  return "something I calculated"
}

Graph.make {
  foo {
    bar(calculateSomething())
    definedMethod1()
  }
}

To allow for this sort of pattern it's better to leave all the closures as the default "owner first" resolve strategy, but have the outer methodMissing be aware of when there is an inner closure in progress and hand back down to that:

public class Graph {
    def static make(Closure c){
        Graph g = new Graph()
        c.delegate = g
        c()
    }

    private ObjImpl currentObj = null

    def methodMissing(String name, args){
        if(currentObj) {
            // if we are currently processing an inner ObjImpl closure,
            // hand off to that
            return currentObj.invokeMethod(name, args)
        }
        println "outer " + name
        if(args.length > 0 && args[0] instanceof Closure){
            currentObj = new ObjImpl(type: name)
            try {
                Closure closure = args[0]
                closure()
            } finally {
                currentObj = null
            }
        }
    }

    class ObjImpl {
        String type
        def methodMissing(String name, args){
            println "inner " + name
        }
        def definedMethod1(){ 
                println "exec'd known method"
        }
    }
}

With this approach, given the above DSL example, the calculateSomething() call will pass up the chain of owners and reach the method defined in the calling script. The bar(...) and definedMethod1() calls will go up the chain of owners and get a MissingMethodException from the outermost scope, then try the delegate of the outermost closure, ending up in Graph.methodMissing. That will then see that there is a currentObj and pass the method call back down to that, which in turn will end up in ObjImpl.definedMethod1 or ObjImpl.methodMissing as appropriate.

If your DSL can be nested more than two levels deep then you'll need to keep a stack of "current objects" rather than a single reference, but the principle is exactly the same.

An alternative approach might be to make use of groovy.util.BuilderSupport, which is designed for tree building DSLs like yours:

class Graph {
  List children
  void addChild(ObjImpl child) { ... }

  static Graph make(Closure c) {
    return new GraphBuilder().build(c)
  }
}

class ObjImpl {
  List children
  void addChild(ObjImpl child) { ... }
  String name

  void definedMethod1() { ... }
}

class GraphBuilder extends BuilderSupport {

  // the various forms of node builder expression, all of which
  // can optionally take a closure (which BuilderSupport handles
  // for us).

  // foo()
  public createNode(name) { doCreate(name, [:], null) }

  // foo("someValue")
  public createNode(name, value) { doCreate(name, [:], value) }

  // foo(colour:'red', shape:'circle' [, "someValue"])
  public createNode(name, Map attrs, value = null) {
    doCreate(name, attrs, value)
  }

  private doCreate(name, attrs, value) {
    if(!current) {
      // root is a Graph
      return new Graph()
    } else {
      // all other levels are ObjImpl, but you could change this
      // if you need to, conditioning on current.getClass()
      def = new ObjImpl(type:name)
      current.addChild(newObj)
      // possibly do something with attrs ...
      return newObj
    }
  }

  /**
   * By default BuilderSupport treats all method calls as node
   * builder calls.  Here we change this so that if the current node
   * has a "real" (i.e. not methodMissing) method that matches
   * then we call that instead of building a node.
   */
  public Object invokeMethod(String name, Object args) {
    if(current?.respondsTo(name, args)) {
      return current.invokeMethod(name, args)
    } else {
      return super.invokeMethod(name, args)
    }
  }
}

The way BuilderSupport works, the builder itself is the closure delegate at all levels of the DSL tree. It calls all its closures with the default "owner first" resolve strategy, which means that you can define a method outside the DSL and call it from inside, e.g.

def calculateSomething() {
  return "something I calculated"
}

Graph.make {
  foo {
    bar(calculateSomething())
    definedMethod1()
  }
}

but at the same time any calls to methods defined by ObjImpl will be routed to the current object (the foo node in this example).

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top