Hello, Tapestry community!
I've been working on something I really wanted Tapestry to have but
haven't
had a chance to implement myself until some time ago: proper REST
support.
And not do it by integrating with some other framework like Jersey or
CXF
or by being a JAX-RS implementation, but by doing it in a very Tapestry
way: event handler methods, TypeCoercer for type conversions,
everything
being customizable by contributions to Tapestry-IoC services, return
value
of event handler methods handled by ComponentEventResultProcessor
implementations, etc.
This is a work in progress, done in the "rest" branch, so I decided to
publish a snapshot (5.8.0-SNAPSHOT) so you can use it yourself and tell
me
what you feel about it. All feedback is welcome!
Major points:
- New page-level events are triggered after the "activate" event is
triggered (i.e. onActivate()) so you can write REST endpoints as
regular,
live-class-reloaded event handler methods.
- One for each supported HTTP method: GET, POST, DELETE, PUT,
HEAD,
PATCH.
- Event name is onHttp[method name]. For example, onHttpPut.
- New constants in EventConstants for each supported HTTP method.
- Activation context for REST event handler methods exactly in
the
same way as in onActivate().
- Just like onActivate(), these new events are not triggered in
components.
- New service in tapestry-http (which is included by tapestry-core),
RestSupport, with utility methods and <T> Optional<T>
getRequestBodyAs(Class<T> type), which returns the request body
converted
to a given type.
- getRequestAsBody(Class) uses HttpRequestBodyConverter.
- Another new service in tapestry-http, HttpRequestBodyConverter,
which
uses an ordered configuration of itself, which provides conversions
of
request body to given types.
- Only one method, <T> T convert(HttpServletRequest request,
Class<T>
type);
- tapestry-http contributes one implementation, which is added as
the
last one in the ordered configuration, which uses TypeCoercer.
- Out of the box, it supports conversions to String, primitive
types,
JSONArray, JSONObject plus any other type that has a String ->
type
conversion available in TypeCoercer, directly or not.
- New annotation for page event handler method (onActivate(),
onHttp*)
parameters: @RequestBody, which receives the request body and
converts it
to the parameter type using RestSupport.getRequestAsBody().
- Think of it as @RequestParameter but for the request body instead
of a
query parameter.
- Example below.
- New annotation for page event handler method (onActivate(),
onHttp*)
parameters: @StaticActivationContextValue("something"), for defining
that
method will only be called if that page activation context value
matches a
given string.
- Example below.
- Parameters with that annotation still get their values set as
usual.
- This was built mostly for REST support, but it can useful for
non-REST situations too when a give activation context has a
number of
possible values and there's some logic tied to it.
- Automatic generation of Swagger/OpenAPI 3.0 REST API definition
files.
- OpenApiDescriptionGenerator service, which is an ordered
configuration of itself.
- An internal implementation generates a base JSON object
definition
that can be customized by contributing more OpenApiDescription
implementations.
- Titles, names, summaries, etc can be provided by configuration
symbols or message files (app.properties).
- Messages files are live class reloaded too, so all changes
done
to the REST endpoint event handler methods and the
corresponding messages
are live reloaded.
- Formats for message keys and described below.
- Parameter descriptions not implemented yet.
- It will support query parameters and path parameters.
- [Not implemented yet] Embedded Swagger/OpenAPI viewer (i.e. the
right pane of https://editor.swagger.io) hooked directly to the
definition file generated by OpenApiDescriptionGenerator
- It's going to be a separate subproject/JAR to avoid bloating
projects not using the viewer
@StaticActivationContextValue example:
final private String COMPLETED = "completed";
final private String CLOSED = "closed";
// Only called if first page activation context value is
"completed"
@OnEvent(EventConstants.ACTIVATE)
void completed(@StaticActivationContextValue(COMPLETED) String
state,
int id) {
...
}
// Only called if first page activation context value is "closed"
@OnEvent(EventConstants.ACTIVATE)
void closed(@StaticActivationContextValue(CLOSED) String state, int
id)
{
...
}
REST endpoint event handler method example:
Object onHttpPatch(
@StaticActivationContextValue("subpath") String subpath,
String parameter,
@RequestBody String body)
{
...
}
@OnEvent(value = EventConstants.HTTP_DELETE)
Object delete(@StaticActivationContextValue("subpath") String
subpath,
String parameter)
{
return createResponse(EventConstants.HTTP_DELETE, null,
parameter);
}
Happy coding!
--
Thiago