DSL Scope Limiting with two different DSLs

Hey guys. In KorGE we have two different DSLs, one for view building and other for vector building.

Something like this:

stage {
container { // views DSL
  circle(Colors.RED, 0, 0) // views DSL
  graphics { // views DSL
    fill(Colors.RED) { // vector DSL
        circle(0.0, 0.0, radius = 16.0)
    }
  }
}
}

We have a Container.circle function and a VectorBuilder.circle function. I have tried several approaches to limit the scope of those functions like so inside the fill {} block we only have access to vector building functions. The only approach that seemed to work was to use the same DSL annotation for all the lambdas from both DSLs. And that worked, but prevented other use cases where we accessed a property available at the top-most block.

Is it possible to limit only a block? In this case fill { } to just have access to its this scope? so while building the vectors only have access to make a circle vector but don’t have access to create a circle view

I have created a video showing this issue:

have a look at the @DslMarker annotation!
https://kotlinlang.org/docs/reference/type-safe-builders.html#scope-control-dslmarker-since-11

Yeah, that is the one I used. If you check the video you can see the problem I have. Maybe I’m missing something. Let me reproduce the issue with a small complete sample:


@Target(AnnotationTarget.TYPE)
@DslMarker
annotation class MyDsl

fun MyView.container(block: @MyDsl MyView.() -> Unit) = block(MyView())
open class MyView {
	fun circle(a: Int) = Unit
}

fun MyView.stage(block: @MyDsl MyStageView.() -> Unit) = block(MyStageView())
class MyStageView : MyView() {
	val views: String = "hello"
}

fun MyView.vector(block: @MyDsl MyVector1.() -> Unit) = block(MyVector1())
class MyVector1 {
	fun circle(b: String) = Unit
}

fun test() {
	MyView().stage {
		container {
			container {
				vector {
					circle(1) // No autocompletion and Disallowed. Nice!
					circle("a")
				}
				// I want to have access to the views property!
				// No autocompletion for views
				views.toUpperCase() // 'val views: String' shouldnt be called in this context by implicit receiver, it will become an error soon. Use the explicit one if necessary
			}
		}
	}
}

Have you annotated MyView, MyStageView and MyVector1? Not sure it’s a solution but you may try.

Yeah, same results. What I want is to have two different DSLs and one of the DSLs hide the other.
I have the feeling that this is not possible right now, and in our case this would improve usability a lot.
Any of the Kotlin team members can help with this? Should I submit a feature request?

I’m not sure if this works or not, but I think that instead of views.method() you can do this@container.views.method()

Yep, it’s possible according to Kotlin’s documentation on the Dsl Marker Annotation:

Note that it’s still possible to call the members of the outer receiver, but to do that you have to specify this receiver explicitly:

html {
    head {
        this@html.head { } // possible
    }
    // ...
}

Yeah, I know that you can access with this@ in the inner method and in any method.
There are three levels with three injected objects and two different DSLs.

Stage(dsl1), View(dsl1), Vector(dsl2)

Inside View(dsl1) block I want to access Stage(dsl1) members without this@. While inside Vector(dsl2) I want to only have access to its dsl2 methods and have to access outside with this@.

1 Like

I think I figured it out. This code should work in the exact way that you specified:

@Target(AnnotationTarget.TYPE)
@DslMarker
annotation class ShouldNotAccessUpperContainer
@Target(AnnotationTarget.TYPE)
@DslMarker
annotation class ShouldNotAccessUpperStage

fun MyView.container(block: @ShouldNotAccessUpperContainer MyView.() -> Unit) = block(MyView())
open class MyView {
	fun circle(a: Int) = Unit
}

fun MyView.stage(block: @ShouldNotAccessUpperStage MyStageView.() -> Unit) = block(MyStageView())
class MyStageView : MyView() {
	val views: String = "hello"
}

fun MyView.vector(block: @ShouldNotAccessUpperStage @ShouldNotAccessUpperContainer MyVector1.() -> Unit) = block(MyVector1())
class MyVector1 {
	fun circle(b: String) = Unit
}

fun main() {
	MyView().stage {
		container {
			container {
				vector {
					circle(1) // Disallowed because it can't access any higher Container
					circle("a")
                    println(views.toUpperCase()) // Also Disallowed but this time because it cannot access any higher stage
				}
				println(views.toUpperCase()) // Allowed and has auto completion becuase a container can access any higher scope that isn't a container
			}
		}
	}
}

It is kind of counter intuitive, but basically you need to reverse the way that you mark the DSLs. Basically you need to make a separate annotation for every different type of function in Dsl1. By type here I mean things like stage and container, so for example if you have MyView.normalStage and MyView.amazingCoolerStage, those would probably be the same type and therefore should both be annotated with something like @ShouldNotAccessUpperStage (or if you have a super class that they both inherit from then just mark that one). In other words, make an annotation for every group of classes that have clashing fields. And then for every Dsl2 function, you gotta add all the Dsl1 annotations to it. If you have a Dsl3, then you also gotta make an annotation for every type of function in Dsl2 and then mark all the functions in Dsl3 with the Dsl2 and Dsl1 annotations. My recommendation would be to have a superclass or “super interface” for every dsl (named something like Dsl2) that has the annotations from the previous Dsls on it and that every class in Dsl2 inherits from.

I also made a Kotlin Playground version of this if you want to play around with it. I think there are definitely some improvements that can be made using classes or interfaces that can make this code easier to read.

Awesome! This is exactly what I wanted! :slight_smile: thanks you so much! Maybe at some point we can just mark a DSL annotation to disallow the other ones somehow

1 Like