@Nested task inputs

I am trying to grok the usage of @Nested for task inputs, but seem to be failing. Specifically having trouble understanding how @Nested and nested property conventions work.

At a high-level, I have a DSL extension and part of its state will be used as a nested-input for a task.

class MyPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        ...
        MyDslExtension dsl = project.getExtensions().create( "abc", MyDslExtension.class, project );
        project.getTasks().create( "generateDescriptor", GenerateDescriptor.class, dsl );
    }
}

class abstract MyDslExtension implements ExtensionAware {
    private final Descriptor descriptor;

    @Inject
    public MyDslExtension(Project project) {
        descriptor = project.getObjects().newInstance( Descriptor.class, project );
    }

    @Nested
    public Descriptor getDescriptor() { 
        return descriptor;
    }
}

class abstract Descriptor implements ExtensionAware {
    private final Property<String> description;
    private final MetadataDescriptor metadataDescriptor;
    ...
    @Inject
    public Descriptor(Project project) {
        description = project.getObjects().property(String.class);
        description.convention( "stuff" );

        metadataDescriptor = project.getObjects().newInstance( MetadataDescriptor.class, project );
    }

    @Input
    Property<String> getDescription() { ... }

    @Nested
    MetadataDescriptor getMetadataDescriptor() { ... }
}

class abstract MetadataDescriptor implements ExtensionAware {
    private final ListProperty<String> keywords;
    ...

    @Inject
    public MetadataDescriptor(Project project) {
        keywords = project.getObjects().listProperty(String.class);
    }

    ...
}

abstract class GenerateDescriptor extends DefaultTask {
    private MyDslExtension dsl;

    @Inject
    public GenerateDescriptor(MyDslExtension dsl) {
        this.dsl = dsl;
    }

    @Nested
    public Descriptor getDescriptorDetails() {
        return dsl.getDescriptor();
    }
}

Roughly, the plugin adds MyDslExtension as a project DSL extension and a GenerateDescriptor task, which uses the Descriptor portion of the MyDslExtension for its input.

The problem seems to be with property conventions not being honored if the property is part of a Nested structure.

Some problems were found with the configuration of task ':generateDescriptor' (type 'GenerateDescriptor').
  - In plugin 'xyz' type 'GenerateDescriptor' property 'descriptorDetails.description' doesn't have a configured value.
    
    Reason: This property isn't marked as optional and no value has been configured.
    
    Possible solutions:
      1. Assign a value to 'descriptorDetails.description'.
      2. Mark property 'descriptorDetails.description' as optional.

  - <rinse, repeat for each property with a convention>

Why don’t these task conventions take affect?

Thanks!

So is my only option to simply make the Descriptor Serializable and rely on @Input instead?

This seems utterly broken (I played with it more all weekend) and I just cannot see how this is supposed to work.

Hi Steve,

this is very strange, since I think it should work. What happens if you call .get() on the properties? Does the convention work there?

Cheers,
Stefan

Ok, I was able to get this to work, though so far only if I create a new set up descriptor/metadata classes for the task itself -

class GenerateDescriptor {
  ...
  private final Descriptor descriptor;

  @Inject
  public GenerateDescriptor(MyDslExtension config) {
    descriptor = new Descriptor( getProject(), config );
    ...
  }

  @Nested
  public Provider<Descriptor> getDescriptor() { 
    return descriptor; 
  }

  static class Descriptor {
    Descriptor(Project project, MyDslExtension config) {
      name = project.getObjects().property( String.class );
      name.set( config.getDescriptor().getNameProperty() );

      ...
    }
  }
}

No idea why this one works while directly using the DSL extension objects did not.

Ahhh… I think I see. I think it has to do with “multi-form access” for properties. Which I think I have a general misunderstanding about and is maybe better as a new post. I really have this:

class MyDslExtension {
    private final Property<String> name;
    ...

    @Inject
    MyDslExtension(Project project) {
    }

    @Input
    public Property<String> getNameProperty() {
        return name;
    }

    @Internal
    public String getName() {
        return name.getOrNull();
    }

    public void setName(String name) {
        this.name.set( name );
    }

    public void name(String name) {
        setName( name );
    }

    ...
}

Initially I was missing the @Internal for the “simple getters” which I guess Gradle did not like.

I do those extra forms of access for Groovy scripts. Is there a general guide for how to design properties for use in both Kotlin and Groovy scripts? Like I said, if this part is better as a separate discussion let me know and I’ll create one.

Thanks!

Generally you shouldn’t need to add anything but a getter for the Property itself. There’s also specifically guidelines against that in the documentation about lazy configuration.

  • Avoid simplifying calls like obj.getProperty().get() and obj.getProperty().set(T) in your code by introducing additional getters and setters.

For the Groovy DSL, you can still do things like descriptor.name = 'MyDescriptorName' with only the following getter.

@Input
public Property<String> getName() {
    return name;
}

There’s also more info about it scattered around the notes on particular topics, but I’m not aware of a section that states everything that it does for theses cases. For example, when talking about tasks specifically:

Note that Gradle Groovy DSL generates setter methods for each Property -typed property in a task implementation. These setter methods allow you to configure the property using the assignment ( = ) operator as a convenience.

In Kotlin DSL, the set() method on the property needs to be used instead of = .

Thanks as always @jjustinic! That makes sense.

I’ll go and read that whole Lazy Configuration page. I have a feeling it will help answer some of the other questions I have.