org.jooby.spec.RouteSpec Maven / Gradle / Ivy
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.jooby.spec;
import java.io.Serializable;
import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
/**
* spec
*
*
* The spec module allows you to export your API/microservices outside an application.
*
*
*
* The goal of this module is to define a common way to write APIs and provide you API tools like
* live doc and testing for FREE. By FREE we mean:
*
*
*
* You aren't force to learn any other tool or annotated your code special annotations. All you
* have to do is: **write your application** following a few/minor suggestions.
*
*
*
* This module process, collect and compile routes from your application. It
* extracts HTTP method/pattern, parameter, responses, types and doc.
*
*
*
* You will find here the basis and the necessary documentation to build and expose rich APIs for
* free, but keep in mind this module isn't intended for direct usage. It is the basis for tools
* like Swagger or RAML.
*
*
* api def
*
*
* The goal of this module is to define a common way to write APIs and provide you API tools like
* live doc and testing for FREE. By FREE we mean:
*
*
*
* You aren't force to learn any other tool or annotated your code special annotations. All you
* have to do is: **write your application** following a few/minor suggestions.
*
*
* Let's review how to build rich APIs using the spec
module via script
or
* mvc
programming model:
*
* script API
*
* {@code
* {
* /{@literal *}{@literal *}
* {@literal *} Everything about your Pets.
* {@literal *}/
* use("/api/pets")
* /{@literal *}{@literal *}
* * List pets ordered by name.
* *
* * @param start Start offset, useful for paging. Default is 0
.
* * @param max Max page size, useful for paging. Default is 200
.
* * @return Pets ordered by name.
* {@literal *}/
* .get(req {@literal ->} {
* int start = req.param("start").intValue(0);
* int max = req.param("max").intValue(200);
* DB db = req.require(DB.class);
* List<Pet> pets = db.findAll(Pet.class, start, max);
* return pets;
* })
* /{@literal *}{@literal *}
* * Find pet by ID
* *
* * @param id Pet ID.
* * @return Returns 200
with a single pet or 404
* {@literal *}/
* .get("/:id",req {@literal ->} {
* int id=req.param("id").intValue();
* DB db=req.require(DB.class);
* Pet pet = db.find(Pet.class,id);
* return pet;
* })
* /{@literal *}{@literal *}
* * Add a new pet to the store.
* *
* * @param body Pet object that needs to be added to the store.
* * @return Returns a saved pet.
* {@literal *}/
* .post(req {@literal ->} {
* Pet pet=req.body().to(Pet.class);
* DB db=req.require(DB.class);
* db.save(pet);
* return pet;
* })
* /{@literal *}{@literal *}
* * Update an existing pet.
* *
* * @param body Pet object that needs to be updated.
* * @return Returns a saved pet.
* {@literal *}/
* .put(req {@literal ->} {
* Pet pet=req.body().to(Pet.class);
* DB db=req.require(DB.class);db.save(pet);
* return pet;
* })
* /{@literal *}{@literal *}
* * Deletes a pet by ID.
* *
* * @param id Pet ID.
* * @return A 204
* {@literal *}/
* .delete("/:id",req {@literal ->} {
* int id=req.param("id").intValue();
* DB db=req.require(DB.class);
* db.delete(Pet.class,id);
* return Results.noContent();
* })
* .produces("json")
* .consumes("json");}
* }
*
* MVC API
*
*
* {@code
* /{@literal *}{@literal *}
* * Everything about your Pets.
* {@literal *}/
* @Path("/api/pets")
* @Consumes("json")
* @Produces("json")
* public class Pets {
*
* private DB db;
*
* @Inject
* public Pets(final DB db) {
* this.db = db;
* }
*
* /{@literal *}{@literal *}
* * List pets ordered by name.
* *
* * @param start Start offset, useful for paging. Default is 0
.
* * @param max Max page size, useful for paging. Default is 200
.
* * @return Pets ordered by name.
* {@literal *}/
* @GET
* public List<Pet> list(final Optional start, final Optional max) {
* List<Pet> pets = db.findAll(Pet.class, start.orElse(0), max.orElse(200));
* return pets;
* }
*
* /{@literal *}{@literal *}
* * Find pet by ID.
* *
* * @param id Pet ID.
* * @return Returns a single pet
* {@literal *}/
* @Path("/:id")
* @GET
* public Pet get(final int id) {
* Pet pet = db.find(Pet.class, id);
* return pet;
* }
*
* /{@literal *}{@literal *}
* * Add a new pet to the store.
* *
* * @param pet Pet object that needs to be added to the store.
* * @return Returns a saved pet.
* {@literal *}/
* @POST
* public Pet post(@Body final Pet pet) {
* db.save(pet);
* return pet;
* }
*
* /{@literal *}{@literal *}
* * Update an existing pet.
* *
* * @param body Pet object that needs to be updated.
* * @return Returns a saved pet.
* {@literal *}/
* @PUT
* public Pet put(@Body final Pet pet) {
* db.save(pet);
* return pet;
* }
*
* /{@literal *}{@literal *}
* * Deletes a pet by ID.
* *
* * @param id Pet ID.
* {@literal *}/
* @DELETE
* public void delete(final int id) {
* db.delete(Pet.class, id);
* }
* }
* }
*
*
*
* Previous examples are feature identical, but they were written in very different way. Still
* they produces an output likes:
*
*
*
* {@code
* GET /api/pets
* summary: Everything about your Pets.
* doc: List pets ordered by name.
* consumes: [application/json]
* produces: [application/json]
* params:
* start:
* paramType: QUERY
* type: int
* value: 0
* doc: Start offset, useful for paging. Default is 0
.
* max:
* paramType: QUERY
* type: int
* value: 200
* doc: Max page size, useful for paging. Default is 200
.
* response:
* type: java.util.List
* doc: Pets ordered by name.
* GET /api/pets/:id
* summary: Everything about your Pets.
* doc: Find pet by ID
* consumes: [application/json]
* produces: [application/json]
* params:
* id:
* paramType: PATH
* type: int
* doc: Pet ID.
* response:
* type: apps.model.Pet
* doc: Returns 200
with a single pet or 404
* POST /api/pets
* summary: Everything about your Pets.
* doc: Add a new pet to the store.
* consumes: [application/json]
* produces: [application/json]
* params:
* body:
* paramType: BODY
* type: apps.model.Pet
* doc: Pet object that needs to be added to the store.
* response:
* type: apps.model.Pet
* doc: Returns a saved pet.
* PUT /api/pets
* summary: Everything about your Pets.
* doc: Update an existing pet.
* consumes: [application/json]
* produces: [application/json]
* params:
* body:
* paramType: BODY
* type: apps.model.Pet
* doc: Pet object that needs to be updated.
* response:
* type: apps.model.Pet
* doc: Returns a saved pet.
* DELETE /api/pets/:id
* summary: Everything about your Pets.
* doc: Deletes a pet by ID.
* consumes: [application/json]
* produces: [application/json]
* params:
* id:
* paramType: PATH
* type: int
* doc: Pet ID.
* response:
* type: void
* doc: A 204
* }
*
*
* NOTE: We use text
for simplicity and easy read, but keep in mind the output
* is compiled in binary format.
*
*
*
* how it works?
*
*
* The spec module scan and parse the source code: *.java
and produces a list of
* {@link RouteSpec}.
*
*
* There is a jooby:spec
maven plugin for that collects and compiles {@link RouteSpec}
* at build time, useful for production environments where the source code isn't available.
*
*
* Why do we parse the source code?
*
*
* It is required for getting information from script routes
. We don't need that for
* mvc routes
because all the information is available via Reflection
and
* {@link Method}.
*
*
*
* But also, you can write clean and useful JavaDoc in your source code that later are added to the
* API information.
*
*
* Why don't parse byte-code with ASM?
*
* Good question, the main reason is that we lost generic type information and we aren't able to
* tell if the route response is for example a list of pets.
*
*
* script rules
*
* Have a look at the previous examples again? Do you see anything special? No, right?
*
*
* Well there are some minor things you need to keep in mind for getting or collecting route
* metadata from script
routes:
*
*
* params
*
* Params need to be in one sentence/statement, like:
*
*
*
*
* req {@literal ->} {
* int id = req.param("id").intValue();
* }
*
*
* not like:
*
*
* req {@literal ->} {
* Mutant p = req.param("id");
* int id = p.intValue();
* }
*
*
* response type (a.k.a return type)
*
*
* There should be ONLY one return statement and return type needs to be declared
* as variable, like:
*
*
*
* req {@literal ->} {
* ...
* Pet pet = db.find(id); // variable pet
* ...
* return pet;
* }
*
*
* not like:
*
*
* req {@literal ->} {
* ...
* return db.find(id); // we aren't able to tell what type returns db.find
* }
*
*
* or
*
*
* req {@literal ->} {
* ...
* if (...) {
* return ...;
* } else {
* return ...;
* }
* }
*
*
*
* There is a workaround if these rules doesn't make sense to you and/or the algorithm fails to
* resolve the correct type. Please checkout next section.
*
*
* API doc
*
* If you take a few minutes and write good quality doc the prize will be huge!
*
*
* The tool takes the doc and export it as part of your API!!
*
*
* Here is an example on how to document script routes:
*
*
*
* /{@literal *}{@literal *}
* * Everything about your Pets.
* {@literal *}/
* use("/api/pets")
* /{@literal *}{@literal *}
* * List pets ordered by name.
* *
* * @param start Start offset, useful for paging. Default is 0
.
* * @param max Max page size, useful for paging. Default is 200
.
* * @return Pets ordered by name.
* {@literal *}/
* .get(req {@literal ->} {
* int start = req.param("start").intValue(0);
* int max = req.param("max").intValue(200);
* DB db = req.require(DB.class);
* List<Pet> pets = db.findAll(Pet.class, start, max);
* return pets;
* });
*
*
* The spec for /api/pets
will have the following doc:
*
*
* params:
* start:
* paramType: QUERY
* type: int
* value: 0
* doc: Start offset, useful for paging. Default is 0
.
* max:
* paramType: QUERY
* type: int
* value: 200
* doc: Max page size, useful for paging. Default is 200
.
* response:
* type: java.util.List<apps.model.Pet>
* doc: Pets ordered by name.
*
*
* response
*
*
* With JavaDoc, you can control the default type returned by the route and/or the status codes.
* For example:
*
*
*
*
* /{@literal *}{@literal *}
* * Find pet by ID.
* *
* * @param id Pet ID.
* * @return Returns a {@link Pet} with 200
status or 404
* {@literal *}/
* get(req {@literal ->} {
* DB db = req.require(DB.class);
* return db.find(Pet.class, id);
* });
*
*
*
* Here you tell the tool that this route produces a Pet
, response looks like:
*
*
*
* response:
* type: apps.model.Pet
* statusCodes:
* 200: Success
* 404: Not Found
* doc: Returns 200
with a single pet or 404
*
*
*
* You can override the default message of the status code with:
*
*
*
* @return Returns a {@link Pet}
with 200 = Success
status or 404 = Missing
*
*
*
* Finally, you can specify the response type via JavaDoc type references:
* {@link Pet}
. This is useful when the tool isn't able to detect the type for
* you and/or you aren't able to follow the return rules described before.
*
*
* @author edgar
* @see RouteProcessor
* @since 0.15.0
*/
public interface RouteSpec extends Serializable {
/**
* @return Top level doc (a.k.a summary).
*/
Optional summary();
/**
* @return Route name.
*/
Optional name();
/**
* @return Route method.
*/
String method();
/**
* @return Route pattern.
*/
String pattern();
/**
* @return Route doc.
*/
Optional doc();
/**
* @return List all the types this route can consumes, defaults is: {@code * / *}.
*/
List consumes();
/**
* @return List all the types this route can produces, defaults is: {@code * / *}.
*/
List produces();
/**
* @return List of params or empty list.
*/
List params();
/**
* @return Route response.
*/
RouteResponse response();
}