-
Notifications
You must be signed in to change notification settings - Fork 4
Codegen
Building (RESTful) APIs in Java Spring Boot requires many of the same components: Controllers, Services, Mappers, Repository interfaces and/or data-access objects (DAOs), data-transfer-objects (DTO), and entity models. The business logic to implement CRUD operations for each domain is similar as well. Thus, many of these components can be abstracted with parameterized dependencies so that implementing them at the concrete level can be done with minimal lines of code. This abstraction is provided by the components submodule.
Although the components submodule greatly improves a developer’s ability to quickly implement Spring Boot components with full CRUD functionality, the developer experience requires implementing and/or extending the interfaces and abstract classes that have been parameterized. This task can be tedious and reduce developer productivity.
To improve the developer experience further by reducing the amount of code necessary to implement CRUD functionality for a domain’s Spring Boot components, the codegen project uses code generation technologies to auto implement the Controller, Service, Mapper, Repository interfaces and/or DAOs and DTOs based on a domain's Entity model. It is the Entity model that typically dictates differences in the business logic, after all.
There are concerns about generating APIs based on introspecting database schemas (ruslantalpa r/java on Reddit), which is related to how this project introspects Entity models to generate code for Spring Boot components. Indeed, this coupling is an anti-pattern to the DTO pattern and could expose sensitive attributes.
These concerns are addressed with plans to support generating DTO objects based on the Entity model in ways that can exclude sensitive properties. There will also be support for providing DTO objects to the code generator thereby giving full granular control to the developer. Please see Next-Steps for more information.
There are many Spring Boot component code generators for various IDE platforms, such as here and here for IntelliJ, to name a few.
Although plugins like the ones mentioned above are useful to quickly spin-up end-to-end components for a domain, these plugins are also against DRY software design principles in how they generate identical code each time they are run. As a consequence, to obtain sufficient coverage, tests will need to be written for each domain that these plugins are executed in. This requirement slows the developer experience, and is an anti-thesis of the springharvest project at large.
Complete the Setup guide.
Ingest the dev.springharvest.codegen dependency with the desired version into your preferred build tool.
Maven
<dependency>
<groupId>dev.springharvest</groupId>
<artifactId>codegen</artifactId>
<version>${version}</version>
</dependency>Gradle
compileOnly "dev.springharvest:codegen:${version}"
annotationProcessor "dev.springharvest:codegen:${version}"
testAnnotationProcessor "dev.springharvest:codegen:${version}"
compileOnly files("$buildDir/generated/sources/annotationProcessor/java/main")
annotationProcessor files("$buildDir/generated/sources/annotationProcessor/java/main")Annotate a domain's Entity model with the @Harvest annotation and set the applicable values for domainContextPath, domainNameSingular, domainNamePlural, and parentDomainName.
Example: AuthorEntity using @Harvest
package dev.springharvest.library.codegen.domains.authors.author.models.entities;
import dev.springharvest.codegen.annotations.Harvest;
import dev.springharvest.shared.domains.embeddables.traces.traceable.models.entities.AbstractTraceableEntity;
import jakarta.persistence.AttributeOverride;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
@Slf4j
@Data
@SuperBuilder
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
@Entity
@Table(name = "authors")
@AttributeOverride(name = "id", column = @Column(name = "id"))
@Harvest(domainContextPath = "/library", domainNameSingular = "Author", domainNamePlural = "Authors", parentDomainName = "Authors")
public class AuthorEntity extends AbstractTraceableEntity<UUID> {
@NotBlank
@Column(name = "name")
protected String name;
@Override
public boolean isEmpty() {
return super.isEmpty() && StringUtils.isBlank(name);
}
}See Development documentation for more information.
./gradlew clean build
Example: codegen
./gradlew :projects:core:components:codegen clean buildExample: books-catalogue-codegen
./gradlew :projects:examples:books-catalogue-codegen clean buildAll library-based submodules are non-executable, including the codegen submodule.
To execute the example books-catalogue-codegen submodule, however, run the following gradle command:
./gradlew :projects:examples:books-catalogue-codegen bootRun -Pspring.profiles.active=postgres,liquibase
As discussed in the Motivation section of this document, components have been abstracted to make implementing domain-driven APIs with Spring Boot easy with minimal amounts of new code. These abstracted components are contained in the submodule here.
In the codegen submodule of this project, we take the level of abstraction even further by automatically generating the "minimal new code" that is required to concretely implement each domain. This generation is achieved by leveraging bespoke processing and processing, similar to other frameworks like SwaggerUI.
Below is the methodology for creating a new annotation to satisfy this goal.
The @Target annotation is a meta-annotation to scope which types of objects the annotation will be used for. The table here illustrates different flavours of ElementType that defines these scopes.
Because we will be creating an annotation to be used on class with an @Entity annotation, we have used the ElementType.TYPE.
Annotations have multiple uses, such as validation at runtime, code generation at compile-time, etc. The following article here describes the @Retention annotation.
@Retentionis also a meta-annotation that comes with some retention policies. These retention policies determine at which point an annotation is discarded. There are three types of retention policies: SOURCE, CLASS, and RUNTIME.
RetentionPolicy.SOURCE: The annotations annotated using the SOURCE retention policy are discarded at runtime.
RetentionPolicy.CLASS: The annotations annotated using the CLASS retention policy are recorded in the .class file but are discarded during runtime. CLASS is the default retention policy in Java.
RetentionPolicy.RUNTIME: The annotations annotated using the RUNTIME retention policy are retained during runtime and can be accessed in our program during runtime.
Since our use case requires generating code at compile time and otherwise is not needed at runtime, we configure the @Retention meta-annotation to ingest the RetentionPolicy.CLASS value.
The @Harvest annotation below (pasted from here) is how the user provides metadata that the system will use to generate Spring Boot components at compile time.
Example: @interface @Harvest
package dev.springharvest.codegen.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Harvest {
String domainContextPath() default "";
String domainNameSingular() default "";
String domainNamePlural() default "";
String parentDomainName() default "";
}The table below describes how each property is used for constructing the generated files.
| Property Name | Description |
|---|---|
| domainContextPath | Defines the root directory that the generated components will be published in. |
| domainNameSingular | The singular form of the domain name. This value is used to generate directories and packages, component names, etc. |
| domainNamePlural | The plural form of the domain name. This value is used just like domainNameSingular, but is used instead of the singular form when the plural case is more appropriate grammatically. Appropriate cases that are used in code generation are when there are lists, collections, sets, etc., of a particular entity type. |
| parentDomainName | Used when an entity has an ownership association with another entity. This optional property ensures that the generated components follow the parent-child package structure this project uses. This property may not be relevant for some use cases and can be ignored. |
HarvestBO below (pasted from here) is a business object (BO) model used to transmit metadata that is leveraged from the @Harvest annotation amongst the different factories we will use to generate each component layer of the Spring Boot api.
Example: HarvestBO
package dev.springharvest.codegen.models;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = false)
public class HarvestBO {
private final String rootPackageName;
private final String domainNameSingular;
private final String domainNamePlural;
private final String parentDomainName;
private final String domainContextPath;
private final String crudRepositoryPackageName;
private final TypeName dtoTypeName;
private final String dtoPackageName;
private final String dtoName;
private final TypeName entityTypeName;
private final String entityName;
private final String entityPackageName;
private final String mapperPackageName;
private final String crudRepositoryName;
public HarvestBO(String rootPackageName, String domainNameSingular, String domainNamePlural, String parentDomainName, String domainContextPath) {
if (rootPackageName == null) {
throw new IllegalArgumentException("Root package name cannot be null");
}
if (domainNameSingular == null) {
throw new IllegalArgumentException("Domain name singular cannot be null");
}
if (domainNamePlural == null) {
throw new IllegalArgumentException("Domain name plural cannot be null");
}
if (parentDomainName == null) {
throw new IllegalArgumentException("Parent domain name cannot be null");
}
if (domainContextPath == null) {
throw new IllegalArgumentException("Domain context path cannot be null");
}
this.rootPackageName = rootPackageName;
this.domainNameSingular = domainNameSingular;
this.domainNamePlural = domainNamePlural;
this.parentDomainName = parentDomainName;
this.domainContextPath = domainContextPath;
// DTO
this.dtoPackageName = getPackageName("models.dtos");
this.dtoName = String.format("%sDTO", domainNameSingular);
this.dtoTypeName = ClassName.get(getDtoPackageName(), getDtoName());
// Entity
this.entityName = String.format("%sEntity", domainNameSingular);
this.entityPackageName = getPackageName("models.entities");
this.entityTypeName = ClassName.get(getEntityPackageName(), getEntityName());
// Mapper
this.mapperPackageName = getPackageName("mappers");
// Crud Repository
this.crudRepositoryPackageName = getPackageName("persistence");
this.crudRepositoryName = String.format("I%sCrudRepository_", domainNameSingular);
}
private String getPackageName(String subPackage) {
return String.format("%s.%s.%s.%s", rootPackageName, domainNamePlural.toLowerCase(), domainNameSingular.toLowerCase(), subPackage);
}
}The @Harvest annotation processing adapts a pattern similar to the hub-and-spoke design where the HarvestProcessor (hub) uses one-to-many factories (spokes) to generate each layer of the Spring Boot API bottom-up, starting with the Repository layer:
Repository->Mappers->Services->Constants->Controllers
The following pseudocode illustrates how the @Harvest annotation is processed, and can be compared with the actual implementation here:
process(Set annotations, RoundEnvironment roundEnv):
Print a note that annotations are being processed
For each element annotated with @Harvest in the round environment:
If the element is not a class:
Call the error method to print an error message
Return false to exit processing
Extract the TypeMirror of the annotated class
Extract the properties from the @Harvest annotation
Extract the package name where the annotated class resides
Re-build the root package name using based on the extracted @Harvest property values
Generate the CRUD repository code using the CrudRepositoryGenerator
Generate the Mapper code using the MapperGenerator
Generate the CRUD service code using the CrudServiceGenerator
Generate the Controller Constants code using the ControllerConstantsGenerator
Generate the CRUD controller code using the CrudControllerGenerator
Return true to indicate successful processingThe code generators that create each Spring Boot component from the HarvestProcessor adopt a factory design pattern where the generate method handles the writing of each file based on the respective component it is specialized for. These generators use the Java Poet framework to make generating Java files easier to maintain.
The below pseudocode example demonstrates the logic for generating a Repository interface based on the domain that the @Harvest annotation is used, and can be compared with the actual implementation here.
generate(HarvestBO harvestBO, ProcessingEnvironment processingEnv):
If class is instantiated:
Throw an UnsupportedOperationException with a private constructor message
Create an interface specification named harvestBO.getCrudRepositoryName():
Set the modifier to public
Add a superinterface of ParameterizedTypeName:
Use ICrudRepository from "dev.springharvest.crud.domains.base.persistence"
Parameterize with harvestBO's entity package and entity name, and "java.util.UUID"
Add an @Repository annotation from "org.springframework.stereotype"
Create a Java file with the package name from harvestBO for the CRUD repository and the interface specification
Use JavaPoetUtils to write the created Java file using the processing environmentIllustrated below, the generated IAuthorCrudRepository_ interface (left) compares 1:1 to what a developer would manually implement (right, and also shown here):
| Generated with @Harvest | Developer Implemented |
|---|---|
package dev.springharvest.library.domains.authors.persistence;
import dev.springharvest.crud.domains.base.persistence.ICrudRepository;
import dev.springharvest.library.domains.authors.models.entities.AuthorEntity;
import java.util.UUID;
import org.springframework.stereotype.Repository;
@Repository
public interface IAuthorCrudRepository extends ICrudRepository<AuthorEntity, UUID> {
} |
package dev.springharvest.library.domains.authors.persistence;
import dev.springharvest.crud.domains.base.persistence.ICrudRepository;
import dev.springharvest.library.domains.authors.models.entities.AuthorEntity;
import java.util.UUID;
import org.springframework.stereotype.Repository;
@Repository
public interface IAuthorCrudRepository extends ICrudRepository<AuthorEntity, UUID> {
} |
For brevity, the other generators will not be explained in this document, but they follow similar logic to the pseudocode above. Their respective source code can be referenced here, however.
When this codegen submodule is used as a dependency in an external project, like the books-catalogue-codegen example does here, we can write integration tests as we do with any other Spring Boot project. The AuthorCrudIT test here, for example, executes integration tests for all CRUD operations within the Author domain. It extends the AbstractCrudIT in the components-testing submodule to implement these tests with minimal new code and with sufficient coverage. As such, since these tests can execute without error, it thereby asserts that the @Harvest annotation used in this project is performing as expected.
Because this type of testing is transitive (i.e. testing that the codegen dependency works as expected in an external project instead of in the codegen project itself), Jacoco code coverage does not contribute these results towards coverage on the codegen submodule. This testing does quality assure against regression, however.
Because the @Harvest uses @Retention(RetentionPolicy.SOURCE), @Harvest is processed at compile time and JUnit tests are executed at runtime, a different approach to automated testing needs to be taken.
The HarvestProcessorTest here in the codegen submodule, is an example of how compile-time testing can be done in a way that yields Jacoco test coverage. It leverages Truth instead of JUnit to make assertions, and Google's compile-testing framework to handle manipulate compile time environments.
For example blow is the pseudocode for the testHarvestProcessor test here.
testHarvvestProcessor():
Define a test method named testHarvestProcessor in the HarvestProcessorTest class.
Create a JavaFileObject for UserEntity class with the following content:
- Package: test.users.user.models.entities
- Imports: java.util.UUID, dev.springharvest.shared.domains.base.models.entities.BaseEntity, dev.springharvest.codegen.annotations.Harvest
- Class: UserEntity extending BaseEntity<UUID>
- Annotations: @Harvest with domainNameSingular, domainNamePlural, parentDomainName, and domainContextPath attributes
Create a JavaFileObject for UserDTO class with the following content:
- Package: test.users.user.models.dtos
- Imports: java.util.UUID, dev.springharvest.shared.domains.base.models.dtos.BaseDTO
- Class: UserDTO extending BaseDTO<UUID>
Use Google Truth assert_() to:
- Check the compilation of the provided Java source files.
- Process the source files using HarvestProcessor.
- Verify that the compilation completes without any errors.
When we create the JavaFileObjects for the Entity and DTO from a String, we can then manually pass these inputs into the HarvestProcessor, which is the entry point for test coverage. Using Google's testing-compile library, we can assert whether the processing passes or fails as expected.
With a single happy-path input, we are able to achieve sufficient code coverage, as shown here.
When booting the example application here, the user can navigate to the SwaggerUI page at http://localhost:8080/api/docs/ui, which uses the following OpenAPI specification at http://localhost:8080/api/docs, all from a single annotation.
OpenApi Specification Example
Try pasting the above json into the online SwaggerUI Editor here to see the OpenApi specification in a more user-friendly format.
{
"openapi":"3.0.1",
"info":{
"title":"OpenAPI definition",
"version":"v0"
},
"servers":[
{
"url":"http://localhost:8080/api",
"description":"Generated server url"
}
],
"paths":{
"/library/authors/create":{
"post":{
"tags":[
"Authors"
],
"summary":"Creates an entity.",
"description":"Use this API to create a new entity.",
"operationId":"create",
"requestBody":{
"description":"The DTO used to create the entity.",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
},
"required":true
},
"responses":{
"200":{
"description":"The entity with their generated primary key id.",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
}
}
}
}
},
"/library/authors/create/all":{
"post":{
"tags":[
"Authors"
],
"summary":"Creates entities.",
"description":"Use this API to create a new entity.",
"operationId":"createAll",
"requestBody":{
"description":"A list of DTOs used to create the entities.",
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
}
},
"required":true
},
"responses":{
"200":{
"description":"The entities created with their generated primary key ids.",
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
}
}
}
}
}
},
"/library/authors/update":{
"patch":{
"tags":[
"Authors"
],
"summary":"Updates an entity.",
"description":"Use this API to partially update a new entity.",
"operationId":"update",
"parameters":[
{
"name":"id",
"in":"query",
"required":true,
"schema":{
"type":"string",
"format":"uuid"
}
}
],
"requestBody":{
"description":"The DTO used to update its entity.\nOnly the attributes that are changed need to be passed into the dto.\nThe id in the DTO is ignored.\n",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
},
"required":true
},
"responses":{
"200":{
"description":"The updated entity.",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
}
}
},
"deprecated":true
}
},
"/library/authors/update/all":{
"patch":{
"tags":[
"Authors"
],
"summary":"Updates an entity.",
"description":"Use this API to partially update a new entity.",
"operationId":"update_1",
"requestBody":{
"description":"The DTO used to update its entity.\nOnly the attributes that are changed need to be passed into the dto.\nThe id in the DTO is immutable.\n",
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
}
},
"required":true
},
"responses":{
"200":{
"description":"The entity with the given primary key id.",
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
}
}
}
}
}
},
"/library/authors/find":{
"get":{
"tags":[
"Authors"
],
"summary":"Retrieves the entity.",
"description":"Use this API to retrieve an entity by their primary key id.",
"operationId":"findById",
"parameters":[
{
"name":"id",
"in":"query",
"description":"The primary key id of the entity to be retrieved.",
"required":true,
"schema":{
"type":"string",
"format":"uuid"
}
}
],
"responses":{
"200":{
"description":"The entity with the given primary key id",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/AuthorDTO"
}
}
}
}
}
}
},
"/library/authors/find/all":{
"get":{
"tags":[
"Authors"
],
"summary":"Retrieves a list of entities.",
"description":"Use this API to retrieve an entity by their primary key id. Leaving all parameters empty will return all entities.",
"operationId":"findAll",
"parameters":[
{
"name":"pageNumber",
"in":"query",
"description":"The page number of the pageable entities that are returned.",
"required":false,
"schema":{
"type":"integer",
"format":"int32"
}
},
{
"name":"pageSize",
"in":"query",
"description":"The maximum number of entities to return on any single page.",
"required":false,
"schema":{
"type":"integer",
"format":"int32"
}
},
{
"name":"sorts",
"in":"query",
"description":"The attributes to sort by, ascending or descending.",
"required":false,
"schema":{
"type":"array",
"items":{
"type":"string"
}
},
"example":"id-asc"
}
],
"responses":{
"200":{
"description":"The retrieved entities.",
"content":{
"application/json":{
"schema":{
"$ref":"#/components/schemas/PageAuthorDTO"
}
}
}
}
}
}
},
"/library/authors/count":{
"get":{
"tags":[
"Authors"
],
"summary":"Counts the entities.",
"description":"Use this API to retrieve an number of entities in its domain.",
"operationId":"count",
"responses":{
"200":{
"description":"The number of entities in its domain.",
"content":{
"application/json":{
"schema":{
"type":"integer",
"format":"int64"
}
}
}
}
}
}
},
"/library/authors/delete":{
"delete":{
"tags":[
"Authors"
],
"summary":"Delete an entity by its id.",
"description":"Use this API to delete an entity via its id.",
"operationId":"deleteById",
"parameters":[
{
"name":"id",
"in":"query",
"description":"The id used to delete the entity by.",
"required":true,
"schema":{
"type":"string",
"format":"uuid"
}
}
],
"responses":{
"204":{
"description":"The entity was deleted by its id successfully. A redirect is not required."
}
}
}
},
"/library/authors/delete/all":{
"delete":{
"tags":[
"Authors"
],
"summary":"Delete all entities by their ids.",
"description":"Use this API to delete any entity in this domain that matches the passed ids.",
"operationId":"deleteAllById",
"requestBody":{
"description":"The ids used to delete the entities by.",
"content":{
"application/json":{
"schema":{
"type":"array",
"items":{
"type":"string",
"format":"uuid"
}
}
}
},
"required":true
},
"responses":{
"204":{
"description":"The entity was deleted by its id successfully. A redirect is not required."
}
}
}
}
},
"components":{
"schemas":{
"AbstractTraceUsersDTOUUID":{
"type":"object",
"properties":{
"createdBy":{
"type":"string",
"format":"uuid"
},
"updatedBy":{
"type":"string",
"format":"uuid"
}
}
},
"AuthorDTO":{
"type":"object",
"properties":{
"id":{
"type":"string",
"description":"The id of the author.",
"format":"uuid",
"example":"00000000-0000-0000-0000-000000000001"
},
"traceData":{
"$ref":"#/components/schemas/TraceDataDTOUUID"
},
"name":{
"type":"string",
"description":"The name of the author.",
"example":"Dr. Seuss"
}
},
"description":"A book's author."
},
"TraceDataDTOUUID":{
"type":"object",
"properties":{
"traceDates":{
"$ref":"#/components/schemas/TraceDatesDTO"
},
"traceUsers":{
"$ref":"#/components/schemas/AbstractTraceUsersDTOUUID"
}
}
},
"TraceDatesDTO":{
"type":"object",
"properties":{
"dateCreated":{
"type":"string",
"format":"date-time"
},
"dateUpdated":{
"type":"string",
"format":"date-time"
}
}
},
"PageAuthorDTO":{
"type":"object",
"properties":{
"totalElements":{
"type":"integer",
"format":"int64"
},
"totalPages":{
"type":"integer",
"format":"int32"
},
"size":{
"type":"integer",
"format":"int32"
},
"content":{
"type":"array",
"items":{
"$ref":"#/components/schemas/AuthorDTO"
}
},
"number":{
"type":"integer",
"format":"int32"
},
"sort":{
"type":"array",
"items":{
"$ref":"#/components/schemas/SortObject"
}
},
"numberOfElements":{
"type":"integer",
"format":"int32"
},
"pageable":{
"$ref":"#/components/schemas/PageableObject"
},
"first":{
"type":"boolean"
},
"last":{
"type":"boolean"
},
"empty":{
"type":"boolean"
}
}
},
"PageableObject":{
"type":"object",
"properties":{
"offset":{
"type":"integer",
"format":"int64"
},
"sort":{
"type":"array",
"items":{
"$ref":"#/components/schemas/SortObject"
}
},
"paged":{
"type":"boolean"
},
"unpaged":{
"type":"boolean"
},
"pageNumber":{
"type":"integer",
"format":"int32"
},
"pageSize":{
"type":"integer",
"format":"int32"
}
}
},
"SortObject":{
"type":"object",
"properties":{
"direction":{
"type":"string"
},
"nullHandling":{
"type":"string"
},
"ascending":{
"type":"boolean"
},
"property":{
"type":"string"
},
"ignoreCase":{
"type":"boolean"
}
}
}
}
}
}The books-catalogue-codegen example can be referenced to view how the annotation is used.
Please note that since this example is a nested submodule for the project repository, its build.gradle file consumes adjacent submodules in the project as opposed to consuming the dependencies from GitHub. As such, the build.gradle configuration shown in this example may need changes to be used in your own project. Please reference the Setup guide for more information.
This codegen project is in alpha and currently under active development to provide additional and more robust features.
As such, the following features below are not yet supported but are planned for future development.
For more information about these and other planned features, please visit the backlog under the Codegen Project linked to this GitHub repository.
By design, DTOs and Entities must follow the following naming convention (grammar), respectively:
{Domain}DTO,
{Domain}Entity,
where the Domain could be Book, Author, {Your desired domain}.
Data-transfer-objects (DTO) use Swagger annotations, such as @Schema, to generate OpenAPI specifications. An example of these annotations can be found here.
The metadata used within these @Schema annotations are difficult to generate because they are domain specific and cannot be inferred.
Multiple solutions are being considered and can be tracked here.
Currently, only UUID typed primary keys are supported.
Support for more primary key types can be tracked here.
Criteria search support is not yet available, but planned for development.
This feature can be tracked here.
GraphQL support is coming to the Spring Harvest project and may be implemented using annotation processing and code generation technologies. Investigation is on-going.