kotlingroovygradle-plugingradle-kotlin-dslextension-function

How to call Kotlin extension function from Groovy code?


I am writing a Gradle plugin in Kotlin language using kotlin-dsl plugin:

plugins {
    `kotlin-dsl`
}

I have created a plugin class and an extension:

class MyPlugin : Plugin<Project> {

    private lateinit var extension: MyPluginExtension

    override fun apply(project: Project) {
        extension = project.extensions.create("myPluginExtension")
        ...
    }

}

There is an infix function foo inside my extension class:

open class MyPluginExtension {

    infix fun String.foo(other: String) {
        ...
    }

}

Here is how plugin users are supposed to use my plugin (using Kotlin DSL):

plugins {
    id("my-plugin")
}

myPluginExtension {
    "first" foo "second"
}

I want to have the same syntax for plugin users that use Groovy language:

myPluginExtension {
    'first' foo 'second'   // cannot call Kotlin infix function using Groovy
    'first'.foo('second')  // this is not working too
    foo('first', 'second') // ok, but not desired syntax for me
}

I don't know Groovy much, but I've heard it has extension methods. How to make my plugin looks cool for Groovy lovers?

If it is not possible to make a function call directly, I believe there is a way to create a wrapper extension method in Groovy language that will call the Kotlin extension function.

I've found a similar question Kotlin function parameter with receiver, called from Groovy, but it is not exactly what I need.


Solution

  • i'm not familiar with kotlin, so, i'll be referencing java as a baseline

    here is an extension method for String class in groovy world:

    String.metaClass.foo = { x-> " foo( first:${delegate}, second:${x} ) "}
    
    
    "aaa".foo "bbb"    //returns: foo( first:aaa, second:bbb )
    

    in java you can use approximately this code to register extension method for some class:

    class MethodFoo extends groovy.lang.Closure{
        public MethodFoo(Object owner){
            super(owner);
        }
        Object doCall(String second){
            return "foo( first:" + getDelegate() + ", second:" + second + " )";
        }
    }
    
    //-------------------------------
    var mc = GroovySystem.getMetaClassRegistry().getMetaClass(String.class);
    Class []argTypes = { String.class };
    String methodName = "foo"
    if( !mc.hasMetaMethod(methodName, argTypes) ){
        //instead of null it could be a reference to a gradle project if you want to access it inside a method with getOwner()
        mc.registerInstanceMethod(methodName, new MethodFoo(null));
    }
    
    //---- after this point in groovy you can make this call:
    "aaa".foo "bbb"    //should return: foo( first:aaa, second:bbb )
    
    

    PS: it's still possible to achieve syntax "aaa" foo "bbb"

    groovy will translate this line to approximately following code:

    plugin."aaa"( plugin.getProperty("foo") ).getProperty("bbb")
    
    // where plugin."aaa" -> invoke method with name `"aaa"`
    

    as soon as "aaa" is dynamic - you can implement GroovyObect invokeMethod in your plugin that will be invoked on any unknown method call

    this approach is quite tricky but doable. i'm lazy to implement it in java. here is a groovy code that you can run in groovy console:

    class MethodFoo{
        String first
        Object getProperty(String name){
            println( "foo( first: $first, second: $name )" )
        }
    }
    
    class Plugin{
        MethodFoo getFoo(){
            return new MethodFoo()
        }
        
        Object invokeMethod(String name, Object args){
            if(args.length==1 && args[0] instanceof MethodFoo){
                args[0].first = name
                return args[0]
            }
            throw new Exception("method not found: $name")
        }
    }
    
    
    new Plugin().with{
        "aaa" foo "bbb"    //prints: foo( first: aaa, second: bbb )
    }