Quarkus RestClient @BeanParam equivalent

87 Views Asked by At

I have a case where I have a search endpoint with many possible parameters. On the server side, I have a @BeanParam keeping the method signature sane.

I would like similar functionality, to use a pojo to specify the possible params and not have to include them all in the method signature. Is there a way to do this?

    @GET
    Set<Extension> getByFilter(@RestQuery Map<String, String> filter);

seems close, but I would rather specify an actual pojo rather than deal with making a map.

1

There are 1 best solutions below

1
zforgo On BEST ANSWER

@BeanParam is definitely works on client side.

Configuring OpenapiTools to generate parameters as @BeanParam

A sample endpoint which allows a custom headper parameter X-Custom-Header and three query params like: filter_name, filter_age, filter_active is documented like:

---
openapi: 3.0.3
info:
  title: so-77975157-quarkus-client-beanparam API
  version: 1.0-SNAPSHOT
paths:
  /api:
    get:
      tags:
      - Example Resource
      parameters:
      - name: filter_active
        in: query
        schema:
          type: boolean
      - name: filter_age
        in: query
        schema:
          format: int32
          type: integer
      - name: filter_name
        in: query
        schema:
          type: string
      - name: X-Custom-Header
        in: header
        schema:
          type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                uniqueItems: true
                type: array
                items:
                  type: string

By default Openapi Generator will create a method and that's signature will contain every parameter one by one.

@GET
@Produces(MediaType.APPLICATION_JSON)
public Set<String> filter(@HeaderParam("X-Custom-Header") String headerParam,
                          @QueryParam("filter_name") String nameFilter,
                          @QueryParam("filter_age") Integer ageFilter,
                          @QueryParam("filter_active") Boolean activeFilter);

Using the following plugin configuration will generate interfaces using one parameter object annotated by @BeanParam

<plugin>
    <groupId>org.openapitools</groupId>
    <artifactId>openapi-generator-maven-plugin</artifactId>
    <version>7.3.0</version>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
            <configuration>
                <cleanupOutput>true</cleanupOutput>
                <verbose>true</verbose>
                <inputSpec>${project.basedir}/src/main/resources/api.yaml</inputSpec>
                <additionalProperties>disableMultipart=true</additionalProperties>
                <generateApiTests>false</generateApiTests>
                <generateModelTests>false</generateModelTests>

                <generatorName>java</generatorName>
                <library>microprofile</library>
                <configOptions>
                    <interfaceOnly>true</interfaceOnly>
                    <useJakartaEe>true</useJakartaEe>
                    <configKey>myclient</configKey>
                    <serializationLibrary>jackson</serializationLibrary>
                    <useSingleRequestParameter>true</useSingleRequestParameter>
                    <microprofileRestClientVersion>3.0</microprofileRestClientVersion>
                    <useRuntimeException>true</useRuntimeException>
                </configOptions>
            </configuration>
        </execution>
    </executions>
</plugin>

Note #1 the magic is useSingleRequestParameter configuration option.

Note #2 there are some hidden trap in the built-in template. E.g. an unvanted Apache CXF import definition which may cause compile errors. So, <additionalProperties>disableMultipart=true</additionalProperties> property is important to set.

Now, the generated method signature will be:

@GET
@Produces({ "application/json" })
Set<String> apiGet(@BeanParam ApiGetRequest request) throws ApiException, ProcessingException;

and the ApiGetRequest (sorry for that name, I didn't use operationId and/or tags in the sample)

public class ApiGetRequest {

    private @QueryParam("filter_active") Boolean filterActive;
    private @QueryParam("filter_age") Integer filterAge;
    private @QueryParam("filter_name") String filterName;
    private @HeaderParam("X-Custom-Header")  String xCustomHeader;

    private ApiGetRequest() {
    }

    public static ApiGetRequest newInstance() {
        return new ApiGetRequest();
    }

    /**
     * Set filterActive
     * @param filterActive  (optional)
     * @return ApiGetRequest
     */
    public ApiGetRequest filterActive(Boolean filterActive) {
        this.filterActive = filterActive;
        return this;
    }
    /**
     * Set filterAge
     * @param filterAge  (optional)
     * @return ApiGetRequest
     */
    public ApiGetRequest filterAge(Integer filterAge) {
        this.filterAge = filterAge;
        return this;
    }
    /**
     * Set filterName
     * @param filterName  (optional)
     * @return ApiGetRequest
     */
    public ApiGetRequest filterName(String filterName) {
        this.filterName = filterName;
        return this;
    }
    /**
     * Set xCustomHeader
     * @param xCustomHeader  (optional)
     * @return ApiGetRequest
     */
    public ApiGetRequest xCustomHeader(String xCustomHeader) {
        this.xCustomHeader = xCustomHeader;
        return this;
    }
}

On the server side the registered client will be available.

@Path("/foobar")
public class ServerSideResource {

    @RestClient
    ExampleResourceApi clientApi;

    @GET
    public Set<String> callSampleClient() {
        return clientApi.apiGet(ApiGetRequest.newInstance()
                .filterActive(true)
                .filterName("zforgo")
        );
    }

}

However, Openapi Generator can generate functions with a single argument it has lack of utilization capabilities (like inheritance, reuse common parts, etc.)

Fortunately JakartaEE standard supports that, but it has to be created manually.

Creating more complex API manually

@RegisterRestClient(configKey = "person-api")
public interface PersonClient {

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    FilterResult<PersonDTO> search(@HeaderParam("X-Custom-Header") String headerParam,
                                   @BeanParam PersonFilter personFilter,
                                   @BeanParam PagingAndSorting pagingAndSorting);


}

As you can see, the result type is generic and paging and sorting capabilities are utilized to a separated argument.

PagingAndSorting:

public class PagingAndSorting {
    public static final String param_PageIndex = "page_index";
    public static final String param_PageSize = "page_size";
    public static final String param_SortCriteria = "sort_criteria";
    public static final String param_SortDirection = "sort_direction";

    private static final String directionAscending = "Ascending";
    public static final String pageUnlimited = "-1";

    @QueryParam(param_SortCriteria)
    public Optional<String> sortingCriteria;

    @QueryParam(param_SortDirection)
    @DefaultValue(directionAscending)
    public Sort.Direction sortDirection;

    @QueryParam(param_PageIndex)
    public int pageIndex;

    @QueryParam(param_PageSize)
    @DefaultValue(pageUnlimited)
    public int pageSize;

    // getters, setters, builder ...
}

PersonFilter:

public class PersonFilter {

    @QueryParam("filter_name")
    private String nameFilter;

    @QueryParam("filter_age")
    private Integer ageFilter;

    @QueryParam("filter_active")
    private Boolean activeFilter;

    // getters, setters, builder ...
}

FilterResult:

public class FilterResult<T> {
    private final Pagination pagination;
    private final List<T> items;

    public FilterResult(List<T> items, Pagination pagination) {
        this.items = items;
        this.pagination = pagination;
    }

    @JsonCreator
    public FilterResult(List<T> items, long totalCount, Integer pageCount, Integer pageIndex) {
        this(items, new Pagination(totalCount, pageCount, pageIndex));
    }

    public List<T> getItems() {
        return items;
    }

    public Pagination getPagination() {
        return pagination;
    }

    public FilterResult<T> withPageSize(Integer pageSize) {
        pagination.pageSize = pageSize;
        return this;
    }
}

Pagination:

public class Pagination {

    protected long totalCount;
    protected Integer pageCount;
    protected Integer pageIndex;

    protected Integer pageSize;

    public Pagination(long totalCount, Integer pageCount, Integer pageIndex) {
        this.totalCount = totalCount;
        this.pageCount = pageCount;
        this.pageIndex = pageIndex;
    }

    public long getTotalCount() {
        return totalCount;
    }

    public Integer getPageCount() {
        return pageCount;
    }

    public Integer getPageIndex() {
        return pageIndex;
    }
}

Finally the server side code sample:

@Path("/check")
public class ServerSideResource {

    @RestClient
    PersonClient personApi;

    @GET
    @Path("/person-search")
    public FilterResult<PersonDTO> searchPerson() {
        return personApi.search("FooBar",
                // create and fill PersonFilter
                personFilter()
                        .filterActive(true)
                        .filterName("zforgo"),
                // create and fill PagingAndSorting
                pagingAndSorting()
                        .pageSize(20)
                        .sortCriteria("name")
                        .sortDirection(Sort.Direction.Ascending)
        );
    }
}