Protocol
The protocol provides support for mapping a request to a method declared on a handler. It also features role-based access control through an AuthorizationHandler.
Javadoc can be found here.
The dynamic invocation when using annotations uses Java reflection and is pretty fast. Previous
versions of chili-core used reflectasm but as of J11 Java reflection is 10% faster, or 50% faster
if setAccessible(true) is called.
If the programmatic API is used instead, reflection will not be used at all.
Registering a protocol
There are two ways of creating a protocol mapping, lets create the protocol instance first.
private Protocol<Request> protocol = new Protocol<>();
Then we need to set up some mappings and the default role for the API.
Annotations
Annotations can be used to simplify the mapping.
Setting the default Role required for the api to USER and mapping a method to a route called list.
@Roles(USER)
public class MyHandler implements CoreHandler {
@Api
public void list(Request request) {
request.write(someList);
}
}
Programmatically
Programmatic registration is more dynamic and a tiny bit faster at runtime.
protocol.setRoles(Role.USER)
.use("ROUTE", this::list)
.use("ROUTE", this::listAsAdmin, Role.ADMIN);
Authentication
Authentication is required by default for all routes as Role.USER.
Strategy
The @Authenticator annotation can be applied to a function with the signature
Function<Request, Future<RoleType>>, which maps a request into a role. If the user is not
authorized, then the RoleType of PUBLIC is used. It’s also possible to define custom role types.
@Authenticator
public Future<RoleType> authenticator(Request request) {
return tokenFactory.verify(request.token())
.map(verified -> (RoleType) Role.USER)
.otherwise(Role.PUBLIC);
}
For the authenticator to be registered in the protocol the handler needs to be annotated and the handler
passed to the Protocol constructor or registered with the Protocol.annotated(handler) method.
If annotations are not being used, the authenticator can be set with the following,
protocol.authenticator(this::authenticator);
Levels
When resolving the access level for a route, the max level of all the current principals roles is used by the default authorizer. This does not allow for granular permission schemes but it’s very simple.
Custom roles can be defined, they need to implement the RoleType interface and be added to
the RoleMap.
Example of creating a custom role
public SuperRootRole implements RoleType {
public String getName() {
return "super root role";
}
public int getLevel() {
return Integer.MAX_VALUE;
}
}
Registering the role in the RoleMap,
RoleMap.put("super_root", new SuperRootRole());
In order to provide more advanced authorization models, the authorization handler can be replaced in the protocol.
This is done by calling
protocol.setAuthorizationHandler(customHandler);
The interface to implement, AuthorizationHandler.
Processing
Processing is the invocation of a mapped method, it’s initiated from the CoreHandler::handle method.
public void handle(Request request) {
protocol.process(request);
}
This handles authorization and error handling internally within the protocol.
Typed routes (experimental)
A route can take its input as a parameter and return its result, instead of reading the request and writing the response.
record GetAccount(String id) {
GetAccount {
if (id == null || id.isBlank()) {
throw new IllegalArgumentException("id is required");
}
}
}
record AccountView(String id, String name, int level) {}
@Api
public Future<AccountView> get(GetAccount input) {
return accounts.get(input.id()).map(AccountView::of);
}
A method is a typed route when its first parameter is not a request. The protocol
- deserializes the input from the data of the request. A record that validates itself in its constructor is answered with
BADand the message of theIllegalArgumentException, a value of the wrong type withBADandinvalid input: .... The data of the request also contains the route, the target and the token, the input ignores what it doesn’t declare, - writes the result when the returned
Futuresucceeds, or the returned value.void,Future<Void>and null are answered withACCEPTED, - answers failures as it does for any route: an exception that is thrown, or that fails the future, with the status of the
exception if it’s an exception of the framework (
ValueMissingExceptionisMISSING), otherwiseERROR, - documents the route with the class of the input, see below.
The request can be taken as a second parameter, for the token or the connection: create(CreateAccount input, Request request).
When the protocol is given a wrapper of the request, that is what the method receives. Roles work as for other routes,
@Api(RoleMap.ADMIN). Routes that read the request can be mixed with typed routes in the same handler.
What it doesn’t do (yet): a typed route answers with ACCEPTED when it succeeds, use the request to answer otherwise; the
input is an object, not a list or a single value; unknown properties of the input are ignored; the type of the result is not
part of the documentation. Reading the input costs about the same as Serializer.unpack in a route that reads the request
(about 150 ns for a small record), the dispatch about 60 ns more. See TypedRoute and the sample in TypedRouteTest.
Documenting the API
Programmatically documenting
protocol
.model(DefaultModel.class)
.description("this is the API")
.use("wowza", this::wowza)
.description("does the wowza.")
.model(Wowza.class);
Alternatively, with annotations
@Description("this is the API")
@DataModel(Wowza.class)
public class MyHandler implements CoreHandler {
@Api
@Description("does the wowza.")
@DataModel(Wowza.class)
public void wowza(Request request) {
request.accept();
}
}
The annotation based setup requires that either the handler is passed to the protocol constructor or that
protocol.annotated(handler) is called.
When a protocol is documented the protocol will automatically add a route for retrieving a the specification in a custom
format generated by the Serializer.describe(class) method.
Note that it is possible to mix the annotated and programmatic setup on the same protocol instance. It is also possible to register multiple classes onto the same protocol.
- It’s currently not possible to generate any OpenAPI definitions from an API. (wink wink new PR?)
Example
The current format looks like this, from the highscore sample
description: "A simple highscore API."
target: "api"
routes:
update:
description: "Updates the highscore entries with the given entry."
model:
score: "java.lang.Integer"
player: "java.lang.String"
roles:
- "PUBLIC"
list:
description: "Returns a list of the current highscore selection."
roles:
- "PUBLIC"
It resembles OpenAPI 3, which means that adding support for it should be easy. Consider implementing codegen from API descriptions.
The protocol description API can also be used programmatically
class Main {
public static void main(String[] args){
ProtocolDescription<TestPayload> protocol = new ProtocolDescription<>();
protocol.setDescription("a testing api")
.setTarget("master")
.setTemplate(Authentication.class);
Route<TestPayload> route = new Route<TestPayload>("info")
.setDescription("retrieves the info")
.setRoles(Role.ADMIN)
.setTemplate(TestPayload.class);
protocol.addRoute(route);
Serializer.yaml(protocol);
}
private class Authentication {
public String token;
}
private class TestPayload {
public String details;
}
}
outputs;
description: "a testing api"
target: "master"
model:
token: "java.lang.String"
routes:
info:
description: "retrieves the info"
model:
details: "java.lang.String"
roles:
- "ADMIN"
Deserializing a protocol description
ProtocolDescription<Request> description = Serializer.unyaml(description, ProtocolDescription.class);
The protocol description can also be retrieved directly from any Protocol.
Protocol<Request> protocol = new Protocol<>();
ProtocolDescription description = protocol.getDescription();
When using #getDescription the generation of API metadata is not cached as it is when the /api/document route is invoked during request processing. The Protocol instance internally keeps track of when the protocol definition changes and flushes the cache. This means that changes to the protocol during runtime will always be reflected in the documentation.