Closures in annotations

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
14 messages Options
12
Reply | Threaded
Open this post in threaded view
|

Closures in annotations

Remko Popma
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

Remko Popma
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

Remko Popma
I’m probably overlooking something simple but I’m not seeing it yet. 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()

        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //	at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}



On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

RE: Closures in annotations

Milles, Eric (TR Technology)

Is there an instance in the core language/runtime where a closure literal is used as part of an annotation but not for an AST transformation?  I'm not sure what the compiler does when it encounters a closure expression as the value for an annotation attribute (of type Class).  But the AST transforms that do this -- ASTTest and AutoImplement come to mind -- take the closure code and use it to generate methods.  I think GContracts is this way as well.

 

After reading your example more closely, I see that a single-valued annotation attribute works as expected but the multi-value case fails.

 

From: Remko Popma <[hidden email]>
Sent: Monday, November 16, 2020 7:02 PM
To: Groovy_Developers <[hidden email]>
Subject: Re: Closures in annotations

 

I’m probably overlooking something simple but I’m not seeing it yet. 

 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }
 
    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()
 
        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //     at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //     at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //     at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}

 

 

 

On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:

PS 

 

The ITypeConverter interface definition is here:

 

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:

Hi all,

 

I have a question about passing closures to annotations in Groovy.

To illustrate, consider the @Option annotation in the picocli library.

Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

 

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

 

I am working on a change to picocli that would allow users to specify closures for these and other attributes.

User code could look like this:

 

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

 

I think this would be a nice addition and would make picocli more "groovy".

 

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.

When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

 

Is this a known limitation of Groovy?

Is there a way to work around this?

 

I can provide an example project to reproduce this if that is helpful.

 

Kind regards,

Remko

 

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

paulk_asert
Administrator
In reply to this post by Remko Popma
The Closure to Class conversion doesn't currently support arrays. If you change  converter() to take just a single convert, your example works for me.

Supporting arrays might be an interesting enhancement. I'll take a look at what would be involved.

Cheers, Paul.


On Tue, Nov 17, 2020 at 11:02 AM Remko Popma <[hidden email]> wrote:
I’m probably overlooking something simple but I’m not seeing it yet. 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()

        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //	at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}



On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

paulk_asert
Administrator
The following runs fine after adding in array support:

import java.lang.annotation.*
import org.codehaus.groovy.runtime.InvokerHelper

class ClosureTest {
    static class Demo {
        @Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.getDeclaredField("digest").getAnnotation(Option)
        Class comp = annotation.completionCandidates()
        assert comp != null
        assert Closure.isAssignableFrom(comp)
        assert ["A", "B", "C"] == InvokerHelper.invokeConstructorOf(comp, [null, null] as Object[])()

        Class[] conv = annotation.converter()
        assert conv != null
        assert conv.length == 1
        assert Closure.isAssignableFrom(conv[0])
        assert 'SHA-1' == InvokerHelper.invokeConstructorOf(conv[0], [null, null] as Object[])('SHA-1').algorithm
    }
}

interface ITypeConverter<K> {
    K convert(String value) throws Exception
}

class NoCompletionCandidates {}

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.FIELD])
@interface Option {
    Class<? extends ITypeConverter<?>>[] converter() default []
    Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates
    String names()
}

Probably worth adding. Did you want to create a Jira?

Cheers, Paul.


On Tue, Nov 17, 2020 at 12:32 PM Paul King <[hidden email]> wrote:
The Closure to Class conversion doesn't currently support arrays. If you change  converter() to take just a single convert, your example works for me.

Supporting arrays might be an interesting enhancement. I'll take a look at what would be involved.

Cheers, Paul.


On Tue, Nov 17, 2020 at 11:02 AM Remko Popma <[hidden email]> wrote:
I’m probably overlooking something simple but I’m not seeing it yet. 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()

        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //	at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}



On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

Remko Popma
Eric and Paul,
Thank you both for your responses!

Paul,
Thank you for your quick turnaround on supporting array annotations! 

I will create a Jira ticket when I get to my PC. 

Remko

On Nov 17, 2020, at 12:24, Paul King <[hidden email]> wrote:


The following runs fine after adding in array support:

import java.lang.annotation.*
import org.codehaus.groovy.runtime.InvokerHelper

class ClosureTest {
    static class Demo {
        @Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.getDeclaredField("digest").getAnnotation(Option)
        Class comp = annotation.completionCandidates()
        assert comp != null
        assert Closure.isAssignableFrom(comp)
        assert ["A", "B", "C"] == InvokerHelper.invokeConstructorOf(comp, [null, null] as Object[])()

        Class[] conv = annotation.converter()
        assert conv != null
        assert conv.length == 1
        assert Closure.isAssignableFrom(conv[0])
        assert 'SHA-1' == InvokerHelper.invokeConstructorOf(conv[0], [null, null] as Object[])('SHA-1').algorithm
    }
}

interface ITypeConverter<K> {
    K convert(String value) throws Exception
}

class NoCompletionCandidates {}

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.FIELD])
@interface Option {
    Class<? extends ITypeConverter<?>>[] converter() default []
    Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates
    String names()
}

Probably worth adding. Did you want to create a Jira?

Cheers, Paul.


On Tue, Nov 17, 2020 at 12:32 PM Paul King <[hidden email]> wrote:
The Closure to Class conversion doesn't currently support arrays. If you change  converter() to take just a single convert, your example works for me.

Supporting arrays might be an interesting enhancement. I'll take a look at what would be involved.

Cheers, Paul.


On Tue, Nov 17, 2020 at 11:02 AM Remko Popma <[hidden email]> wrote:
I’m probably overlooking something simple but I’m not seeing it yet. 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()

        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //	at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}



On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

paulk_asert
Administrator

On Tue, Nov 17, 2020 at 2:11 PM Remko Popma <[hidden email]> wrote:
Eric and Paul,
Thank you both for your responses!

Paul,
Thank you for your quick turnaround on supporting array annotations! 

I will create a Jira ticket when I get to my PC. 

Remko

On Nov 17, 2020, at 12:24, Paul King <[hidden email]> wrote:


The following runs fine after adding in array support:

import java.lang.annotation.*
import org.codehaus.groovy.runtime.InvokerHelper

class ClosureTest {
    static class Demo {
        @Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.getDeclaredField("digest").getAnnotation(Option)
        Class comp = annotation.completionCandidates()
        assert comp != null
        assert Closure.isAssignableFrom(comp)
        assert ["A", "B", "C"] == InvokerHelper.invokeConstructorOf(comp, [null, null] as Object[])()

        Class[] conv = annotation.converter()
        assert conv != null
        assert conv.length == 1
        assert Closure.isAssignableFrom(conv[0])
        assert 'SHA-1' == InvokerHelper.invokeConstructorOf(conv[0], [null, null] as Object[])('SHA-1').algorithm
    }
}

interface ITypeConverter<K> {
    K convert(String value) throws Exception
}

class NoCompletionCandidates {}

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.FIELD])
@interface Option {
    Class<? extends ITypeConverter<?>>[] converter() default []
    Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates
    String names()
}

Probably worth adding. Did you want to create a Jira?

Cheers, Paul.


On Tue, Nov 17, 2020 at 12:32 PM Paul King <[hidden email]> wrote:
The Closure to Class conversion doesn't currently support arrays. If you change  converter() to take just a single convert, your example works for me.

Supporting arrays might be an interesting enhancement. I'll take a look at what would be involved.

Cheers, Paul.


On Tue, Nov 17, 2020 at 11:02 AM Remko Popma <[hidden email]> wrote:
I’m probably overlooking something simple but I’m not seeing it yet. 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()

        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //	at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}



On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

paulk_asert
Administrator

On Tue, Nov 17, 2020 at 5:45 PM Paul King <[hidden email]> wrote:

On Tue, Nov 17, 2020 at 2:11 PM Remko Popma <[hidden email]> wrote:
Eric and Paul,
Thank you both for your responses!

Paul,
Thank you for your quick turnaround on supporting array annotations! 

I will create a Jira ticket when I get to my PC. 

Remko

On Nov 17, 2020, at 12:24, Paul King <[hidden email]> wrote:


The following runs fine after adding in array support:

import java.lang.annotation.*
import org.codehaus.groovy.runtime.InvokerHelper

class ClosureTest {
    static class Demo {
        @Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.getDeclaredField("digest").getAnnotation(Option)
        Class comp = annotation.completionCandidates()
        assert comp != null
        assert Closure.isAssignableFrom(comp)
        assert ["A", "B", "C"] == InvokerHelper.invokeConstructorOf(comp, [null, null] as Object[])()

        Class[] conv = annotation.converter()
        assert conv != null
        assert conv.length == 1
        assert Closure.isAssignableFrom(conv[0])
        assert 'SHA-1' == InvokerHelper.invokeConstructorOf(conv[0], [null, null] as Object[])('SHA-1').algorithm
    }
}

interface ITypeConverter<K> {
    K convert(String value) throws Exception
}

class NoCompletionCandidates {}

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.FIELD])
@interface Option {
    Class<? extends ITypeConverter<?>>[] converter() default []
    Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates
    String names()
}

Probably worth adding. Did you want to create a Jira?

Cheers, Paul.


On Tue, Nov 17, 2020 at 12:32 PM Paul King <[hidden email]> wrote:
The Closure to Class conversion doesn't currently support arrays. If you change  converter() to take just a single convert, your example works for me.

Supporting arrays might be an interesting enhancement. I'll take a look at what would be involved.

Cheers, Paul.


On Tue, Nov 17, 2020 at 11:02 AM Remko Popma <[hidden email]> wrote:
I’m probably overlooking something simple but I’m not seeing it yet. 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()

        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //	at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}



On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

Reply | Threaded
Open this post in threaded view
|

Re: Closures in annotations

Remko Popma
In reply to this post by paulk_asert
You beat me to it! Thanks!
I updated the description, please take a look. 


On Tue, Nov 17, 2020 at 16:45 Paul King <[hidden email]> wrote:

On Tue, Nov 17, 2020 at 2:11 PM Remko Popma <[hidden email]> wrote:
Eric and Paul,
Thank you both for your responses!

Paul,
Thank you for your quick turnaround on supporting array annotations! 

I will create a Jira ticket when I get to my PC. 

Remko

On Nov 17, 2020, at 12:24, Paul King <[hidden email]> wrote:


The following runs fine after adding in array support:

import java.lang.annotation.*
import org.codehaus.groovy.runtime.InvokerHelper

class ClosureTest {
    static class Demo {
        @Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.getDeclaredField("digest").getAnnotation(Option)
        Class comp = annotation.completionCandidates()
        assert comp != null
        assert Closure.isAssignableFrom(comp)
        assert ["A", "B", "C"] == InvokerHelper.invokeConstructorOf(comp, [null, null] as Object[])()

        Class[] conv = annotation.converter()
        assert conv != null
        assert conv.length == 1
        assert Closure.isAssignableFrom(conv[0])
        assert 'SHA-1' == InvokerHelper.invokeConstructorOf(conv[0], [null, null] as Object[])('SHA-1').algorithm
    }
}

interface ITypeConverter<K> {
    K convert(String value) throws Exception
}

class NoCompletionCandidates {}

@Retention(RetentionPolicy.RUNTIME)
@Target([ElementType.FIELD])
@interface Option {
    Class<? extends ITypeConverter<?>>[] converter() default []
    Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates
    String names()
}

Probably worth adding. Did you want to create a Jira?

Cheers, Paul.


On Tue, Nov 17, 2020 at 12:32 PM Paul King <[hidden email]> wrote:
The Closure to Class conversion doesn't currently support arrays. If you change  converter() to take just a single convert, your example works for me.

Supporting arrays might be an interesting enhancement. I'll take a look at what would be involved.

Cheers, Paul.


On Tue, Nov 17, 2020 at 11:02 AM Remko Popma <[hidden email]> wrote:
I’m probably overlooking something simple but I’m not seeing it yet. 

The below code demonstrates the issue when trying to pass a Groovy closure to the @Option(converter = ...)attribute:

class ClosureTest {
    static class Demo {
        @picocli.CommandLine.Option(names = "-x",
                completionCandidates = {["A", "B", "C"]},
                converter = [{ str -> java.security.MessageDigest.getInstance(str) }])
        java.security.MessageDigest digest
    }

    static void main(String[] args) {
        def annotation = Demo.class.getDeclaredField("digest").getAnnotation(picocli.CommandLine.Option)
        Class ok = annotation.completionCandidates()
        assert ok != null
        assert Closure.class.isAssignableFrom(ok)
        assert ["A", "B", "C"] == ((Closure) ok.getConstructor(Object, Object).newInstance(null, null)).call()

        Class[] bad = annotation.converter()
        assert bad != null
        assert bad.length == 1 // this assert fails:
        //Exception in thread "main" Assertion failed:
        //
        //assert bad.length == 1
        //       |   |      |
        //       []  0      false
        //
        //	at org.codehaus.groovy.runtime.InvokerHelper.assertFailed(InvokerHelper.java:434)
        //	at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.assertFailed(ScriptBytecodeAdapter.java:670)
        //	at closure.ClosureTest.main(ClosureTest.groovy:18)
    }
}



On Mon, Nov 16, 2020 at 21:16 Remko Popma <[hidden email]> wrote:
PS 

The ITypeConverter interface definition is here:

On Mon, Nov 16, 2020 at 21:08 Remko Popma <[hidden email]> wrote:
Hi all,

I have a question about passing closures to annotations in Groovy.
To illustrate, consider the @Option annotation in the picocli library.
Relevant attributes are `completionCandidates` and `converter`, defined in Java as follows:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER})
public @interface Option {
  Class<? extends ITypeConverter<?>>[] converter() default {};
  Class<? extends Iterable<String>> completionCandidates() default NoCompletionCandidates.class;
  ...
}

I am working on a change to picocli that would allow users to specify closures for these and other attributes.
User code could look like this:

@Option(names = '-s', completionCandidates = {["A", "B", "C"]})
@Field String s

@Option(names = '-a', converter = [{ str -> MessageDigest.getInstance(str) }] )
@Field MessageDigest algorithm

I think this would be a nice addition and would make picocli more "groovy".

I have a prototype implementation, but it appears that only the first example ( completionCandidates = {["A", "B", "C"]} ) works as expected.
When stepping through my prototype test in a debugger, it looks like the second example (the converter attribute) receives a zero-length array of classes when invoked from Groovy. I tried with Groovy 2.4.10 and 3.0.6.  

Is this a known limitation of Groovy?
Is there a way to work around this?

I can provide an example project to reproduce this if that is helpful.

Kind regards,
Remko

12