I like to give step by step solutions.
If you’re just interested in the end-result, I collapsed the steps.
step by step
initial given
Want to link to a class at compiletime? Use a generic.
Therefor, my field looks like:
class Field<MODEL, TYPE>
basic implementation
The most basic implementation requires the class to add it’s type to the function
fun <MODEL, TYPE> field() = Field<MODEL, TYPE>()
object AModel {
val value1 = field<AModel, String>()
val value2 = field<AModel, Int>()
}
object BModel{
val value3 = field<BModel, String>()
}
now you can create a dsl-class which accepts fields where the first generic is of a specific type:
class ScopeDsl<M>{
fun addField(field : Field<M, *>) : Unit = TODO()
}
//and function
fun <MODEL> scope(
scope : MODEL,
script : ScopeDsl<MODEL>.()->Unit
) = Unit
we can use it and see it does what it should:
scope(AModel){
addField(AModel.value1) // allowed
addField(BModel.value2) // not allowed
}
helper functions
We can let the function create the field itself:
object AModel {
fun <T> field() = Field<Amodel, T>()
val value1 = field<String>()
}
This needs to be added to every method and so contains loads of duplication.
move helper-function to interface
We can move the function to an interface.
This interface needs to know where to add the function to.
Therefor, the interface needs a generic.
interface Model<M>{
fun <T> field() = Field<M, T>()
}
object AModel : Model<AModel>{
val value1 = field<String>()
}
common props
Don’t know enough, but I think it already works.
You just need to pass the generic of the class.
abstract class Common<M> : Model<M>{
val common = field<String>()
}
object AModel : Common<AModel>(){
val value1 = field<String>()
}
val aScope = scope(AModel) {
addField(AModel.value1)
addField(AModel.common)
}
do note, the field-method is not final and overwriting it will lead to problems.
You have two options:
- ignore the warnings and the issue
- make the interface an abstract class where the method can be final.
This will also mean that no other classes can be extended (models are not allowed anyways)
relationship
The relationShip uses the same principles:
class Relation<FROM, TO>
interface Model<M>{
fun <T> relationShop(to : TO) = Relation<M, TO>()
}
class ScopeDsl<M>{
fun <TO> addEntity(
relation : Relation<M, TO>,
script : ScopeDSL<TO>.() -> Unit
) : Unit = TODO()
}
DSLmarker
val aScope = scope(AModel) {
val a = this
addEntity(AModel.relation) {
val b = this
addField(AModel.value1) // compiles
}
}
The code above does compile to:
a.addField(Amodel.value1)
instread of
b.addField(AModel.value1)
This is because b cannot accept fields from AModel.
To stop it from compiling use DSLMarker:
@DSLMarker
annotation class ScopeDSLMarker
@ScopeDSLMarker
class ScopeDSL<M>{...}
Now the code above doesn’t compile anymore.
all together
@DSLMarker
annotation class ScopeDSLMarker
class Field<M, Type>
class Relation<FROM , TO>
interface Model<M>{
fun <Type> field() = Field<M, Type>()
fun <TO> relationship(to: TO) = Relation<M, TO>()
}
@ScopeDSLMarker
class ScopeDSL<M>{
fun <TO> addEntity(
relation : Relation<M, TO>,
script : ScopeDSL<TO>.() -> Unit
) : Unit = TODO()
fun addField(field : Field<M, *>) : Unit = TODO()
}
fun <M> scope(
model : M,
script : ScopeDSL<M>.()->Unit
) = Unit
val aScope = scope(AModel) {
addField(AModel.value1)
addField(AModel.common)
addEntity(AModel.relation) {
addField(BModel.value3)
}
}
abstract class Common<M> : Model<M>{
val common = field<String>()
}
object AModel : Common<AModel>(){
val value1 = field<String>()
val value2 = field<Int>()
val relation = relationship(to = BModel)
}
object BModel : Model<BModel>{
val value3 = field<String>()
}
val aScope = scope(AModel) {
addField(AModel.value1)
addField(AModel.common)
addEntity(AModel.relation) {
addField(BModel.value3)
}
}
Bonus - restrict generic
The generic passed to Model
can be restricted to a generic that extends Model
.
interface Model<M : Model>
The same counts for all the common-classes.
(This is the same way as generics works)
The bonus-restriction makes the code look way more ugly, so you should see look if you want it
class Field<M : Model<M>, Type>
class Relation<FROM : Model<FROM>, TO : Model<TO>>
interface Model<M : Model<M>>{
fun <Type> field() = Field<M, Type>()
fun <TO : Model<TO>> relationship(to: TO) = Relation<M, TO>()
}
@DSLMarker
annotation class ScopeDSLMarker
@ScopeDSLMarker
class ScopeDSL<M : Model<M>>{
fun <TO : Model<TO>> addEntity(
relation : Relation<M, TO>,
script : ScopeDSL<TO>.() -> Unit
) : Unit = TODO()
fun addField(field : Field<M, *>) : Unit = TODO()
}
fun <M : Model<M>> scope(
model : M,
script : ScopeDSL<M>.()->Unit
) = Unit
abstract class Common<M : Common<M>> : Model<M>{
val common = field<String>()
}
object AModel : Common<AModel>(){
val value1 = field<String>()
val value2 = field<Int>()
val relation = relationship(to = BModel)
}
object BModel : Model<BModel>{
val value3 = field<String>()
}