annotationProcessor("io.micronaut.sourcegen:micronaut-sourcegen-generator-java")
Compilation time source code generators
Micronaut SourceGen exposes a language-neutral API for source code generation.
Version: 1.5.3
1 Introduction
Micronaut SourceGen exposes a language-neutral API for performing source code generation. Since JavaPoet is no longer maintained this module includes a fork of the code with extensions to support Java Records and other modern Java constructs.
An additional API is provided in the io.micronaut.sourcegen.model
package that abstracts both JavaPoet and KotlinPoet such that the developer can build source generators in a language neutral manner.
Any processors built with the Micronaut SourceGen API work with:
-
Java
-
Kotlin via KAPT
-
Kotlin via KSP
Note that Groovy is not supported at this time of writing since it lacks APIs to add generated sources to the current compilation unit and perform multiple rounds of processing like Java and Kotlin do.
2 Release History
For this project, you can find a list of releases (with release notes) here:
3 Quick Start
To get started add Micronaut SourceGen to the annotation processor scope of your build configuration:
For Java projects add:
<annotationProcessorPaths>
<path>
<groupId>io.micronaut.sourcegen</groupId>
<artifactId>micronaut-sourcegen-generator-java</artifactId>
</path>
</annotationProcessorPaths>
For Kotlin projects using KSP add:
ksp("io.micronaut.sourcegen:micronaut-sourcegen-generator-kotlin")
<dependency>
<groupId>io.micronaut.sourcegen</groupId>
<artifactId>micronaut-sourcegen-generator-kotlin</artifactId>
<scope>ksp</scope>
</dependency>
or, for those using KAPT add:
kapt("io.micronaut.sourcegen:micronaut-sourcegen-generator-kotlin")
<dependency>
<groupId>io.micronaut.sourcegen</groupId>
<artifactId>micronaut-sourcegen-generator-kotlin</artifactId>
<scope>kapt</scope>
</dependency>
4 Writing a Source Generator
To write a source generator you have a few options. If you only wish to support Java you can use the JavaPoet API that is forked into the io.micronaut.sourcegen.javapoet
package. Since JavaPoet is no longer being developed and maintained we recommending using this fork since it is maintained as part of the project and includes support for Java 17+ constructors like Records.
Alternatively, you can also use the language neutral code generation API defined in the io.micronaut.sourcegen.model
package that works with both Kotlin and Java adding the ability to write code generators that work cross language.
To get started add dependency on the micronaut-core-processor
module:
implementation("io.micronaut:micronaut-core-processor")
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-core-processor</artifactId>
</dependency>
And the micronaut-sourcegen-model
module:
implementation("io.micronaut.sourcegen:micronaut-sourcegen-model")
<dependency>
<groupId>io.micronaut.sourcegen</groupId>
<artifactId>micronaut-sourcegen-model</artifactId>
</dependency>
The module you build will have to be placed on the annotation processor classpath of the target project. You CANNOT mix source generation code and application code is the same source tree. Typically you will have a separate project for your source generator a separate project for your application that uses the source generator. In addition the micronaut-core-processor module should NEVER be in the application classpath.
|
The following is an example of using the API:
import io.micronaut.core.annotation.Internal;
import io.micronaut.core.annotation.NonNull;
import io.micronaut.inject.ast.ClassElement;
import io.micronaut.inject.processing.ProcessingException;
import io.micronaut.inject.visitor.TypeElementVisitor;
import io.micronaut.inject.visitor.VisitorContext;
import io.micronaut.sourcegen.custom.example.GenerateInterface;
import io.micronaut.sourcegen.generator.SourceGenerator;
import io.micronaut.sourcegen.generator.SourceGenerators;
import io.micronaut.sourcegen.model.InterfaceDef;
import io.micronaut.sourcegen.model.MethodDef;
import io.micronaut.sourcegen.model.TypeDef;
import javax.lang.model.element.Modifier;
import java.io.IOException;
@Internal
public final class GenerateInterfaceBuilder implements TypeElementVisitor<GenerateInterface, Object> { // (1)
@Override
public @NonNull VisitorKind getVisitorKind() {
return VisitorKind.ISOLATING;
} // (2)
@Override
public void visitClass(ClassElement element, VisitorContext context) {
SourceGenerator sourceGenerator = SourceGenerators.findByLanguage(context.getLanguage()).orElse(null); // (3)
if (sourceGenerator == null) {
return;
}
String builderClassName = element.getPackageName() + ".MyInterface1";
InterfaceDef interfaceDef = InterfaceDef.builder(builderClassName) // (4)
.addModifiers(Modifier.PUBLIC)
.addMethod(MethodDef.builder("findLong")
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.returns(Long.class)
.build())
.addMethod(MethodDef.builder("saveString")
.addModifiers(Modifier.ABSTRACT, Modifier.PUBLIC)
.addParameter("myString", String.class)
.returns(TypeDef.VOID)
.build())
.build();
sourceGenerator.write(interfaceDef, context, element);
}
}
1 | A source generator should implement TypeElementVisitor . The first type argument is the type-level annotation you want to visit in source code the second argument is the member level (field, method, constructor) level annotation. You can specify Object to visit all. |
2 | The VisitorKind should typically be ISOLATING if you generate a single source file corresponding to a single originating source file (1-to-1 mapping). If you generate a source file that takes into account multiple other source files then change this to AGGREGATING |
3 | You should obtain an instance of SourceGenerator from the context, in general this should never be null but you never know if a new language is added in the the future. |
4 | You can use one of the *Def classes as a builder to build the source file. In this case we use InterfaceDef but there are others like RecordDef, ClassDef etc. |
5 | Once you have built the model write it to a source file! |
To see more examples take a look at some of the existing implementations like BuilderAnnotationVisitor that powers the @Builder annotation. |
5 Annotations
The module Micronaut SourceGen annotations ships with annotations which you can use in your projects:
To use them, you need the following dependency:
implementation("io.micronaut.sourcegen:micronaut-sourcegen-annotations")
<dependency>
<groupId>io.micronaut.sourcegen</groupId>
<artifactId>micronaut-sourcegen-annotations</artifactId>
</dependency>
Annotation | Description |
---|---|
Create a builder of the annotated type. |
|
Create an interface with copy style method and possible builder style methods for a record |
|
Generated a new class with a static implementation of the java.utils.Object’s toString() method for annotated Java Bean |
|
Generated a new class with a static implementation of the java.utils.Object’s equals() and hashCode() methods for annotated Java Bean |
5.1 Builder
If you annotate a Java Record with @Builder
, a PersonBuilder
class is generated at compilation-time.
import io.micronaut.sourcegen.annotations.Builder;
@Builder
public record Person(Long id, String name, byte[] bytes) {
}
You can use a builder pattern to create a Person
instance:
@Test
public void buildsPerson() {
var person = PersonBuilder.builder()
.id(123L)
.name("Cédric")
.bytes(new byte[]{1,2,3})
.build();
assertEquals("Cédric", person.name());
assertArrayEquals(new byte[]{1, 2, 3}, person.bytes());
assertEquals(123L, person.id());
}
The BuilderAnnotationVisitor is an example of how to use the Micronaut SourceGen API. |
5.2 Wither
If you annotate a Java Record with @Wither
, a YourRecordWither
interface is generated at compilation-time with all the methods having a default implementation (nothing needs to be implemented). The record class can add that interface using implements YourRecordWither
which will expand it with the default withProperty
copy style methods like YourRecord withPropertyName(PropertyValue)
, calling those methods will create a copy of the record with a modified property.
@Wither
public record Walrus (
@NonNull
String name,
int age,
byte[] chipInfo
) implements WalrusWither {
}
Example of different ways to use the copy methods:
@Test
public void test() throws Exception {
Walrus walrus = new Walrus("Abc", 123, new byte[]{56});
assertEquals(walrus.name(), "Abc");
assertEquals(walrus.age(), 123);
assertArrayEquals(walrus.chipInfo(), new byte[]{56});
Walrus finalWalrus = walrus;
// The name property is annotated with @NonNull the `withName(null)` method should fail
assertThrowsExactly(NullPointerException.class, () -> finalWalrus.withName(null));
walrus = walrus.withName("Xyz");
assertEquals(walrus.name(), "Xyz");
assertEquals(walrus.age(), 123);
assertArrayEquals(walrus.chipInfo(), new byte[]{56});
walrus = walrus.withAge(99);
assertEquals(walrus.name(), "Xyz");
assertEquals(walrus.age(), 99);
assertArrayEquals(walrus.chipInfo(), new byte[]{56});
walrus = walrus.withChipInfo(new byte[]{1, 2, 3});
assertEquals(walrus.name(), "Xyz");
assertEquals(walrus.age(), 99);
assertArrayEquals(walrus.chipInfo(), new byte[]{1, 2, 3});
}
If your record is annotated with Builder the wither interface will also include:
-
default YourRecordBuilder with() {…}
method that will return the record builder populated with the current values of the record -
default YourRecord with(Consumer<YourRecordBuilder> consumer) {…}
a method receiving a lambda that can modify the populated record builder with the current values of the record and producing a new instance of a record as a result
@Wither
@Builder
public record Walrus2(
String name,
int age,
byte[] chipInfo
) implements Walrus2Wither {
}
Example of different ways to use the copy and builder methods:
@Test
public void testWitherAndBuilder() throws Exception {
Walrus2 walrus = new Walrus2("Abc", 123, new byte[]{56});
assertEquals(walrus.name(), "Abc");
assertEquals(walrus.age(), 123);
assertArrayEquals(walrus.chipInfo(), new byte[]{56});
// The name property is NOT annotated with @NotNull so `withName(null)` method should NOT fail
walrus = walrus.withName(null);
assertNull(walrus.name());
assertEquals(walrus.age(), 123);
assertArrayEquals(walrus.chipInfo(), new byte[]{56});
walrus = walrus.withName("Xyz");
assertEquals(walrus.name(), "Xyz");
assertEquals(walrus.age(), 123);
assertArrayEquals(walrus.chipInfo(), new byte[]{56});
walrus = walrus.withAge(99);
assertEquals(walrus.name(), "Xyz");
assertEquals(walrus.age(), 99);
assertArrayEquals(walrus.chipInfo(), new byte[]{56});
walrus = walrus.withChipInfo(new byte[]{1, 2, 3});
assertEquals(walrus.name(), "Xyz");
assertEquals(walrus.age(), 99);
assertArrayEquals(walrus.chipInfo(), new byte[]{1, 2, 3});
walrus = walrus.with().build();
assertEquals(walrus.name(), "Xyz");
assertEquals(walrus.age(), 99);
assertArrayEquals(walrus.chipInfo(), new byte[]{1, 2, 3});
walrus = walrus.with().name("Foobar").build();
assertEquals(walrus.name(), "Foobar");
assertEquals(walrus.age(), 99);
assertArrayEquals(walrus.chipInfo(), new byte[]{1, 2, 3});
walrus = walrus.with().name("Abc").age(123).chipInfo(new byte[]{9, 8, 7}).build();
assertEquals(walrus.name(), "Abc");
assertEquals(walrus.age(), 123);
assertArrayEquals(walrus.chipInfo(), new byte[]{9, 8, 7});
walrus = walrus.with(builder -> builder.name("Denis"));
assertEquals(walrus.name(), "Denis");
assertEquals(walrus.age(), 123);
assertArrayEquals(walrus.chipInfo(), new byte[]{9, 8, 7});
walrus = walrus.with(builder -> builder.name("Kevin").age(1).chipInfo(new byte[]{123}));
assertEquals(walrus.name(), "Kevin");
assertEquals(walrus.age(), 1);
assertArrayEquals(walrus.chipInfo(), new byte[]{123});
}
5.3 Super Builder
To use a builder pattern with types that are using inheritance you annotate a Java bean with @SuperBuilder
:
@SuperBuilder
public abstract class Animal {
private String name;
private int age;
private String color;
// Getters / Setters
}
@SuperBuilder
public class Cat extends Animal {
private int meowLevel;
private String bread;
// Getters / Setters
@Vetoed // vetoed is currently required.
public static CatSuperBuilder builder() {
return new CatSuperBuilder();
}
}
@SuperBuilder
public class Dog extends Animal {
private int barkLevel;
private String bread;
private boolean big;
// Getters / Setters
}
For every class annotated with @SuperBuilder
there going to be two builders generated at compilation-time, an abstract one intended to be inherited and second one to constructor the bean.
In the previous example five classes are generated:
-
AbstractAnimalSuperBuilder
-
AbstractCatSuperBuilder
-
CatSuperBuilder
-
AbstractDogSuperBuilder
-
DogSuperBuilder
@SuperBuilder requires every super-class to be annotated with @SuperBuilder
|
You can use a builder to create an instance:
@Test
public void testCat() {
Cat cat = new CatSuperBuilder()
.name("MrPurr")
.age(2)
.bread("British")
.meowLevel(100)
.color("Red")
.build();
assertEquals(cat.getName(), "MrPurr");
assertEquals(cat.getAge(), 2);
assertEquals(cat.getBread(), "British");
assertEquals(cat.getMeowLevel(), 100);
assertEquals(cat.getColor(), "Red");
}
@Test
public void testDog() {
Dog dog = new DogSuperBuilder()
.name("MrDog")
.age(3)
.bread("JackR")
.barkLevel(20)
.color("Blue")
.big(true)
.build();
assertEquals(dog.getName(), "MrDog");
assertEquals(dog.getAge(), 3);
assertEquals(dog.getBread(), "JackR");
assertEquals(dog.getBarkLevel(), 20);
assertEquals(dog.getColor(), "Blue");
assertTrue(dog.isBig());
}
5.4 Singular
The @Singular
annotation is used together with @Builder
or @SuperBuilder
on a collection property.
The property annotated as singular will have the following:
-
A method with a singular name to add a single element to a collection
-
A method with a plural name and a collection parameter to include all the elements from that collection
-
A
clear+PropertyName
to clean all the items of the collection.
The annotation only following collection types:
-
java.lang.Iterable
-
java.util.Collection
-
java.util.List
-
java.util.Set
-
java.util.SortedSet
-
java.util.Map
-
java.util.SortedMap
The final collection is always immutable |
import io.micronaut.sourcegen.annotations.Singular;
import io.micronaut.sourcegen.annotations.SuperBuilder;
import java.util.List;
import java.util.Map;
@SuperBuilder
public record User(Long id,
String name,
@Singular List<String> roles,
@Singular("property") Map<String, Object> properties) {
}
@Test
public void testSimple() {
User user = new UserSuperBuilder()
.id(123L)
.name("Denis")
.role("READ")
.role("WRITE")
.property("key", "value")
.build();
assertEquals(123L, user.id());
assertEquals("Denis", user.name());
assertEquals(List.of("READ", "WRITE"), user.roles());
assertEquals(Map.of("key", "value"), user.properties());
}
@Test
public void testAddAll() {
User user = new UserSuperBuilder()
.id(123L)
.name("Denis")
.role("READ")
.role("WRITE")
.roles(List.of("ROLES_ADMIN", "ROLES_USER"))
.property("key", "value")
.properties(Map.of("key1", "value1", "key2", "value2"))
.build();
assertEquals(123L, user.id());
assertEquals("Denis", user.name());
assertEquals(List.of("READ", "WRITE", "ROLES_ADMIN", "ROLES_USER"), user.roles());
assertEquals(Map.of("key", "value", "key1", "value1", "key2", "value2"), user.properties());
}
@Test
public void testClear() {
User user = new UserSuperBuilder()
.id(123L)
.name("Denis")
.role("READ")
.role("WRITE")
.clearRoles()
.roles(List.of("ROLES_ADMIN", "ROLES_USER"))
.property("key", "value")
.clearProperties()
.properties(Map.of("key1", "value1", "key2", "value2"))
.build();
assertEquals(123L, user.id());
assertEquals("Denis", user.name());
assertEquals(List.of("ROLES_ADMIN", "ROLES_USER"), user.roles());
assertEquals(Map.of("key1", "value1", "key2", "value2"), user.properties());
}
5.5 ToString
If you annotate a Java Bean with @ToString
, a [Bean]Object
class is generated at compilation-time
with a static implementation of the java.utils.Object’s toString() method.
-
Properties that do not wish to be printed out should be annotated with @ToString.Exclude
-
All bean properties (with getters) will be written in the form of a Java Record in the generated toString() method with signature:
`public static String [BeanName]Object.toString(BeanName object)`
The user is expected to use the generated static method by overriding the toString() method themselves. |
import io.micronaut.sourcegen.annotations.EqualsAndHashCode;
import io.micronaut.sourcegen.annotations.ToString;
@ToString
@EqualsAndHashCode
public class Elephant {
@EqualsAndHashCode.Exclude
public String name;
public int age;
private boolean hasSibling;
@ToString.Exclude
private int[][] values;
public Elephant(String name, int age, boolean hasSibling, int value) {
this.name = name;
this.age = age;
this.hasSibling = hasSibling;
values = new int[3][3];
for(int i=0; i<3; i++) {
for(int j=0; j<3; j++) {
values[i][j] = value;
}
}
}
public Elephant(String name) {
this.name = name;
}
public String getName() {
return name;
}
public boolean isHasSibling() {
return hasSibling;
}
public int[][] getValues() {
return values;
}
@Override
public String toString() {
return ElephantObject.toString(this);
}
@Override
public boolean equals(Object o) {
return ElephantObject.equals(this, o);
}
@Override
public int hashCode() {
return ElephantObject.hashCode(this);
}
}
@Test
public void testToString() {
var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
assertNotNull(Person4Object.toString(person));
Assertions.assertTrue(person.toString().contains("Person4["));
assertEquals("Person4[id=123, title=MR, name=Cédric, bytes=[1, 2, 3]]", person.toString());
}
5.6 Equals and HashCode
If you annotate a Java Bean with @EqualsAndHashCode
, a [Bean]Object
class is generated at compilation-time
with a static implementation of the java.utils.Object’s equals() and hashCode() methods.
-
Properties that do not wish to be processed should be annotated with @EqualsAndHashCode.Exclude
-
All bean properties (with getters) will be used in the calculations of both methods with signature:
`public static int [BeanName]Object.hashCode(BeanName object)`
`public static boolean [BeanName]Object.equals(BeanName object, Object o)`
The user is expected to use the generated static method by overriding the equals() and hashCode() methods themselves. |
import io.micronaut.sourcegen.annotations.EqualsAndHashCode;
import io.micronaut.sourcegen.annotations.ToString;
import java.util.Arrays;
@ToString
@EqualsAndHashCode
public class Person4 {
public enum Title {
MRS,
MR,
MS
}
private long id;
private Title title;
private String name;
private byte[] bytes;
public Person4(long id, Title title, String name, byte[] bytes) {
this.id = id;
this.title = title;
this.name = name;
this.bytes = (bytes != null) ? Arrays.copyOf(bytes, bytes.length) : null;
}
public long getId() {
return id;
}
public Title getTitle() {
return title;
}
public String getName() {
return name;
}
public byte[] getBytes() {
return bytes;
}
@Override
public String toString() {
return Person4Object.toString(this);
}
@Override
public boolean equals(Object o) {
return Person4Object.equals(this, o);
}
@Override
public int hashCode() {
return Person4Object.hashCode(this);
}
}
@Test
public void testMultipleDimensionArrays() {
var elephant = new Elephant("Daisy", 5, false, 1);
var elephantDiff = new Elephant("Daisy", 5, false, 2);
var elephantSame = new Elephant("Dumbo", 5, false, 1);
assertNotEquals(elephant.hashCode(), elephantDiff.hashCode());
assertEquals(elephant.hashCode(), elephantSame.hashCode());
}
@Test
public void testEqualsWithExclude() {
var elephant = new Elephant("Daisy", 5, false, 1);
var elephantDiff = new Elephant("Daisy", 5, false, 2);
var elephantSame = new Elephant("Dumbo", 5, false, 1);
assertNotEquals(elephant, elephantDiff);
assertEquals(elephant, elephantSame);
}
@Test
public void testEqualsWithCorrectObjects() {
var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personSame = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personDiffPrimitive = new Person4(124L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personDiffEnum = new Person4(123L, Person4.Title.MRS,"Cédric", new byte[]{1,2,3});
var personDiffObject = new Person4(123L, Person4.Title.MR,"Cédric Jr.", new byte[]{1,2,3});
var personDiffArray = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,4});
assertNotNull(Person4Object.equals(person, personSame));
assertEquals(person, person);
assertEquals(person, personSame);
assertNotEquals(person, personDiffPrimitive);
assertNotEquals(person, personDiffEnum);
assertNotEquals(person, personDiffObject);
assertNotEquals(person, personDiffArray);
}
@Test
public void testEqualsWithNulls() {
var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personDoubleNull1 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
var personDoubleNull2 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
var personSingleNull = new Person4(124L, Person4.Title.MR,"Cédric", null);
assertNotEquals(null, person);
assertNotEquals(person, new Object());
assertEquals(personDoubleNull1, personDoubleNull2);
assertNotEquals(personSingleNull, person);
assertNotEquals(person, personSingleNull);
}
@Test
public void testHashCodeWithNulls() {
var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personDoubleNull1 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
var personDoubleNull2 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
var personSingleNull = new Person4(124L, Person4.Title.MR,"Cédric", null);
assertEquals(personDoubleNull1.hashCode(), personDoubleNull2.hashCode());
assertNotEquals(personSingleNull.hashCode(), person.hashCode());
assertNotEquals(person.hashCode(), personSingleNull.hashCode());
}
@Test
public void testHashCode() {
var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personSame = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personDiffPrimitive = new Person4(124L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
var personDiffEnum = new Person4(123L, Person4.Title.MRS,"Cédric", new byte[]{1,2,3});
var personDiffObject = new Person4(123L, Person4.Title.MR,"Cédric Jr.", new byte[]{1,2,3});
var personDiffArray = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,4});
assertNotNull(Person4Object.hashCode(person));
assertEquals(person.hashCode(), person.hashCode());
assertEquals(person.hashCode(), personSame.hashCode());
assertNotEquals(person.hashCode(), personDiffPrimitive.hashCode());
assertNotEquals(person.hashCode(), personDiffEnum.hashCode());
assertNotEquals(person.hashCode(), personDiffObject.hashCode());
assertNotEquals(person.hashCode(), personDiffArray.hashCode());
}
5.7 Delegate
If you annotate a Java interface with @Delegate
, a <type>Delegate
abstract class is generated at compilation-time.
import io.micronaut.sourcegen.annotations.Delegate;
import java.util.List;
import java.util.Set;
/**
* A worker interface that delegate will be generated for.
*/
@Delegate
public interface Worker<T> extends SimpleWorker {
String name();
boolean canComplete(List<T> tasks);
T currentTask();
List<String> competencies();
Set<?> complaints();
}
/**
* A simple worker interface.
*/
public interface SimpleWorker {
String name();
double tasksPerDay();
}
You can use a delegate pattern to change the behavior of a Worker delegatee:
/**
* A delegate changing tasks per day.
*/
public class OvertimeWorker<T> extends WorkerDelegate<T> {
OvertimeWorker(Worker<T> delegatee) {
super(delegatee);
}
@Override
public double tasksPerDay() {
return super.tasksPerDay() * 1.2;
}
}
The delegate will delegate the behavior to inner object except for changed methods:
OvertimeWorker<String> worker = new OvertimeWorker<>(new RobotWorker<String>(
"robot",
10,
List.of("does everything"),
"wash flowers",
Set.of()
));
assertEquals("robot", worker.name());
assertEquals(12, worker.tasksPerDay()); // increased because of delegate
assertEquals(List.of("does everything"), worker.competencies());
assertEquals("wash flowers", worker.currentTask());
6 Repository
You can find the source code of this project in this repository: