Scope control for builder-like DSLs


#1

Can someone confirm that the @DslMarker solution is solely for those builders
developed with new classes, and that I have to find another solution when
working with existing classes. For example, the Ant builder:

val project = project("Example02", ".", "archive") {
    description = "Simple project to copy a folder"

    target("archive") {
        description = "A target to copy files from/to specific folders"

        val destination: File = File(this.project?.baseDir, "folder")
        val source: File = File(this.project?.baseDir, "src/example02")
        mkdir(destination)
        copy(destination) {
            fileset(source)
            echo("Unexpected")
        }
        echo("Archive complete")
    }

}

would be assembed with extension functions on the Ant classes:

fun project(name: String = "defaultProject", basedir: String = "basedir", def: String = "default", body: Project.() -> Unit = defaultBody): Project { ... }

fun Project.target(name: String = "target", body: Target.() -> Unit = defaultBody): Target { ... }

fun Target.mkdir(dir: File): Mkdir { ... }

fun Target.copy(todir: File, body: Copy.() -> Unit = defaultBody): Copy { ... }

fun Target.echo(message: String = "echo"): Echo { ... }

fun Task.fileset(dir: File): FileSet { ... }

DSL markers
#2

Scope control works by putting an annotation on the relevant classes. Annotations are present on those classes and you can’t add annotations to classes you don’t own (without bytecode magic). Of course as annotations it is fairly straightforward to add it to existing classes, although it does technically break the api (at source level), not the abi. The api breakage would however be in only those cases where the marker causes a failure because of “improper” use - in other words, most breakage would be very suspect code in the first place.


#3

@kenbarc You can place the DslMarker-annotated annotation not only on classes, but on types as well.
So if you provide the dsl scope functions, which take a function with receiver, you can annotate that receiver type with your dsl annotation:

@DslMarker
@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE)
annotation class TestDsl

fun build1(builder: (@TestDsl DslReceiver1).() -> Unit) {}
fun build2(builder: (@TestDsl DslReceiver2).() -> Unit) {}


fun dslUsage() = build1 {

    dslMethod1()  // ok here

    build2 {
        dslMethod2() // ok as this lambda has implicit receiver of type DslReceiver2
        dslMethod1() // error: 'fun DslReceiver1.dslMethod1(): Unit' cant be called in this context by implicit receiver.
    }
}


// here are some external DSL classes and their methods
class DslReceiver1
class DslReceiver2

fun DslReceiver1.dslMethod1() {}
fun DslReceiver2.dslMethod2() {}

#4

Exactly what I was looking for.
I was not aware that a function with a receiver could be annotated in this way

Thanks.


#5

Ahhh, thank you! I can’t tell you how much time I’ve spent trying to figure out how to get this behavior. They should add this example in the DslMarker documentation.