Reuse Eclipse Microprofile @Header definition in REST API declaration

132 views Asked by At

I'm struggling on how to reuse an @Header annotation within my API specification using Quarkus and Eclipse Microprofile. I have a lot of methods which return a paginated list of objects and I want to deliver the information about the

  • total amount of objects existing
  • current offset
  • current limit

as X-... Header, this currently looks like

@GET
@Path("/obj1")
@Operation(description = "get filtered list of obj1")
@APIResponse(
  responseCode = "200",
  headers = { @Header(name = "X-Length"),@Header(name = "X-Offset") ,@Header(name = "X-Limit")},
  content = @Content(
    mediaType = MediaType.APPLICATION_JSON,
    schema = @Schema(implementation = Obj1.class, type = SchemaType.ARRAY)
  )
)

Since the headers are always the same for every method returning a list, I don't want to copy & paste them on every method, since I'm also currently not sure about the naming.

Is it possible, and if so how, to declare this array once and reuse it like

paginationHeaders = { @Header(name = "X-Length"),@Header(name = "X-Offset") ,@Header(name = "X-Limit")}
...
@APIResponse(...
headers = paginationHeaders

Is there maybe another standardized way to return pagination information (but not in the response object) using Quarkus?

I tried searching via Google, browsing the Quarkus docs and defining a custom annotation.

1

There are 1 answers

0
zforgo On BEST ANSWER

At first I'd like to mention that passing pagination information to HTTP headers is not the best idea. For example firewalls or proxy servers can modify (even remove) HTTP headers. I'm going to show my preferred solution later.

Add pagination headers to response

However, reusing header constants is not fully supported neither OpenAPI nor Quarkus the MicroProfile OpenAPI Specification (and Smallrye OpenAPI implementation) supports to modify OpenAPI documentation programatically.

A REST resource

@Path("/hello")
public class ExampleResource {

    @Operation(
            operationId = "listExamples",
            summary = "List examples"
    )
    @APIResponse(
            headers = @Header(name = "X-Paginated"),
            responseCode = "200",
            description = "Successful operation"
    )
    @APIResponse(
            responseCode = "418",
            description = "In case of Armageddon"
    )

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public List<String> filter() {
        return List.of("Hello from RESTEasy Reactive");
    }

    @DELETE
    public void remove() {
        // Just to haven an other endpoint
    }
}

The listExamples operation has possible two responses, but the only one should contain pagination response headers. I've introduced a meta header X-Paginated to mark which response has to be modified.

By implementing a custom OpenAPI filter it is possible to change OpenAPI definition programatically.

The following filter will change each occurrence of X-Paginated header to X-Length, X-Offset and X-Limit triple.

public class PaginatedHeaderOASFilter implements OASFilter {

    @Override
    public APIResponse filterAPIResponse(APIResponse apiResponse) {
        return Optional.of(apiResponse)
                .filter(isPaginated)
                .map(mapResponse)
                .orElse(apiResponse);
    }

    private static final Predicate<APIResponse> isPaginated = response -> Optional
            .ofNullable(response.getHeaders())
            .map(map -> map.containsKey("X-Paginated"))
            .orElse(false);

    private static final UnaryOperator<APIResponse> mapResponse = response -> {
        response.removeHeader("X-Paginated");
        return response
                .addHeader("X-Length", OASFactory.createHeader()
                        .description("Description of X-Length")
                        .schema(OASFactory.createSchema().type(Schema.SchemaType.INTEGER))
                )
                .addHeader("X-Offset", OASFactory.createHeader().description("Description of X-Offset"))
                .addHeader("X-Limit", OASFactory.createHeader().description("Description of X-Limit"));
    };
}

Note: Don't forget to register custom OASFilter(s) in application configuration file e.g.:

mp.openapi.filter=io.github.zforgo.PaginatedHeaderOASFilter

Add pagination properties to response

As I started I prefer to pass pagination properties in response body. Luckily it can be reusable, because Smallrye supports generic response types.

Pagination holds the pagination properties

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;
    }

    // getters / setters / others
}

FilterResult contains the result list along with pagination

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));
    }

    // getters / setters / others
}

Now, an enpoint like

@GET
@Path("/orders")
@Produces(MediaType.APPLICATION_JSON)
public FilterResult<OrderDto> filterOrders() {
    return null;
}

will be generated in OpenAPI documentation as:

paths:
  /orders:
    get:
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/FilterResultOrderDto'
components:
  schemas:
    FilterResultOrderDto:
      type: object
      properties:
        pagination:
          $ref: '#/components/schemas/Pagination'
        items:
          type: array
          items:
            $ref: '#/components/schemas/OrderDto'
    OrderDto:
      type: object
      properties:
        billingAddress:
          type: string
    Pagination:
      type: object
      properties:
        totalCount:
          format: int64
          type: integer
        pageCount:
          format: int32
          type: integer
        pageIndex:
          format: int32
          type: integer
        pageSize:
          format: int32
          type: integer