Developing Quarkus functions

Quarkus function template structure

When you create a Quarkus function using the kn func CLI, the project directory looks like a typical maven project, with the exception of an additional func.yaml configuration file. Both http and event trigger functions have the same template structure:

Template structure
.
├── func.yaml (1)
├── mvnw
├── mvnw.cmd
├── pom.xml (2)
├── README.md
└── src
    ├── main
    │   ├── java
    │   │   └── functions
    │   │       ├── Function.java (3)
    │   │       ├── Input.java
    │   │       └── Output.java
    │   └── resources
    │       └── application.properties
    └── test
        └── java
            └── functions (4)
                ├── FunctionTest.java
                └── NativeFunctionIT.java
1 The func.yaml configuration file determines the image name and registry.
2 The Project Object Model (POM) file contains project configuration, such as its dependencies.
3 The project must contain a Java method annotated with @Funq. You can place it in the Function.java class.
4 Test cases for the function are created automatically.

Dependencies

You can add additional dependencies you need using the Project Object Model (POM) file:

Example additional dependencies
$ cat pom.xml
...
  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.assertj</groupId>
      <artifactId>assertj-core</artifactId>
      <version>3.8.0</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
...

The dependencies will be downloaded during the first compilation.

Invoking a function

When using the kn func CLI to create a function project you can generate a project that responds to CloudEvents, or one that responds to simple HTTP requests. CloudEvents in Knative are transported over HTTP as a POST request, so both function types will listen and respond to incoming HTTP events.

When an incoming request is received, functions are invoked with an instance of a permitted type of your choice. What the instance will contain depends on the invocation method:

Table 1. Function invocation options
Invocation method What the instance will contain Example of data

HTTP POST request

JSON object in the body of the request

{ "customerId": "0123456", "productId": "6543210" }

HTTP GET request

Data in the query string

?customerId=0123456&productId=6543210

CloudEvent

JSON object in the data property

{ "customerId": "0123456", "productId": "6543210" }

This is the signature of an example function that receives and processes purchase data, such as the customerId and productId data in the table:

Example
public class Functions {
    @Funq
    public void processPurchase(Purchase purchase) {
        // process the purchase
    }
}

The corresponding Purchase JavaBean class that contains the purchase data might look like this:

Example
public class Purchase {
    private long customerId;
    private long productId;
    // getters and setters
}

Return values

Functions can return an instance of:

  • any type from the permitted types

  • the Uni<T> type, where the <T> type parameter can be of any type from the permitted types

The Uni<T> type might be useful when the function calls asynchronous APIs.

The returned object is serialized in the same format as the received object:

  • If the function received an HTTP request, then the returned object is sent in the body of an HTTP response.

  • If the function received CloudEvent in binary encoding, then the returned object is sent in the data property of a binary-encoded CloudEvent.

This is an example function for getting a list of purchases:

Example
public class Functions {
    @Funq
    public List<Purchase> getPurchasesByName(String name);
}
  • Invoking this function through an HTTP request produces an HTTP response, the body of which contains a list of purchases.

  • Invoking this function through an incoming CloudEvent produces a CloudEvent response with a list of purchases in the data property.

Permitted types

The input and output types of a function can be:

  • void

  • String

  • byte[]

  • Primitive types and their wrappers (for example, int and Integer)

  • A JavaBean, if its attributes are of types listed here

  • A Map, List, or Array of types listed here

  • The special CloudEvents<T> type, where the <T> type parameter is of type listed here

Example
public class Functions {
    public List<Integer> getIds();
    public Purchase[] getPurchasesByName(String name);
    public String getNameById(int id);
    public Map<String,Integer> getNameIdMapping();
    public void processImage(byte[] img);
}

CloudEvent attributes

In many cases, we only need to work with the data property of a CloudEvent. However, sometimes we also need to read or write the attributes of a CloudEvent, such as type or subject.

For this purpose, Funqy offers the CloudEvent<T> generic interface and the CloudEventBuilder builder. The <T> type parameter must be one of the permitted types.

In this example, CloudEventBuilder is used to return success or failure of processing the purchase.

public class Functions {

    private boolean _processPurchase(Purchase purchase) {
        // do stuff
    }

    public CloudEvent<Void> processPurchase(CloudEvent<Purchase> purchaseEvent) {
        System.out.println("subject is: ", purchaseEvent.subject());

        if (!_processPurchase(purchaseEvent.data())) {
            return CloudEventBuilder.create()
                    .type("purchase.error")
                    .build();
        }
        return CloudEventBuilder.create()
                .type("purchase.success")
                .build();
    }
}

Invocation examples

This code defines the withBeans, withCloudEvent, and withBinary functions:

Example
import io.quarkus.funqy.Funq;
import io.quarkus.funqy.knative.events.CloudEvent;

public class Input {
    private String message;

    // getters and setters
}

public class Output {
    private String message;

    // getters and setters
}

public class Functions {
    @Funq
    public Output withBeans(Input in) {
        // function body
    }

    @Funq
    public CloudEvent<Output> withCloudEvent(CloudEvent<Input> in) {
        // function body
    }

    @Funq
    void withBinary(byte[] in) {
        // function body
    }
}
  • The withBeans function of the Functions class can be invoked by:

    • An HTTP POST request with JSON body:

      $ curl "http://localhost:8080/" -X POST \
          -H "Content-Type: application/json" \
          -d '{"message": "Hello there."}'
    • An HTTP GET request with query parameters:

      $ curl "http://localhost:8080?message=Hello%20there." -X GET
    • A CloudEvent in binary encoding:

      curl "http://localhost:8080/" -X POST \
        -H "Content-Type: application/json" \
        -H "Ce-SpecVersion: 1.0" \
        -H "Ce-Type: my-type" \
        -H "Ce-Source: cURL" \
        -H "Ce-Id: 42" \
        -d '{"message": "Hello there."}'
    • A CloudEvent in JSON encoding:

      $ curl http://localhost:8080/ \
          -H "Content-Type: application/cloudevents+json" \
          -d '{ "data": {"message":"Hello there."},
                "datacontenttype": "application/json",
                "id": "42",
                "source": "curl",
                "type": "my-type",
                "specversion": "1.0"}'
  • The withCloudEvent function of the Functions class can be invoked with CloudEvent similarly to the withBeans function. However, unlike withBeans, withCloudEvent cannot be invoked with a plain HTTP request.

  • The withBinary function of the Functions class can be invoked by:

    • CloudEvent in binary encoding:

      curl "http://localhost:8080/" -X POST \
        -H "Content-Type: application/octet-stream" \
        -H "Ce-SpecVersion: 1.0"\
        -H "Ce-Type: my-type" \
        -H "Ce-Source: cURL" \
        -H "Ce-Id: 42" \
        --data-binary '@img.jpg'
    • CloudEvent in JSON encoding:

      curl http://localhost:8080/ \
        -H "Content-Type: application/cloudevents+json" \
        -d "{ \"data_base64\": \"$(base64 img.jpg)\",
              \"datacontenttype\": \"application/octet-stream\",
              \"id\": \"42\",
              \"source\": \"curl\",
              \"type\": \"my-type\",
              \"specversion\": \"1.0\"}"

Testing a Quarkus function locally

Quarkus functions can be tested locally on your computer. The Maven project contains tests that you can run, similar to any other Maven project:

$ mvn test