The RestApiDoc plugin allows to document your Grails Rest API. Thanks to some Annotations (@), you will be ready to build a full API report (with a playground to perform test request). The plugin is based on jsondoc.
This plugin allows you to document methods (description, HTTP path/verb, parameters, response type...) and resources (description, all fields,...). It does a lot of stuff for you (computing path/verb for the method, retrieving field type,...)
A sample report is available here. The github repo for this sample is here.
The project is hosted on Github and is available for use under the Apache 2.0 License. You can report bugs and discuss features on the GitHub issues page.
Just add the following line in your BuildConfig (plugins):
compile ":rest-api-doc:0.6"
The resources plugin must be in your BuildConfig! (runtime ":resources:1.2.1")
Grails version | Status | Comment |
---|---|---|
2.4 | ||
2.3 | ||
2.2 or lesser | UrlMapping rules extractor appears in 2.3. Two possibilities:
|
Run the command:
grails rest-api-doc
A restapidoc.json file will be create on the root of your Grails project (in outputFileGeneration).
http://.../restApiDoc/api service output the JSON file.
You can call this to view the Web report:
http://.../restApiDoc/?doc_url=http://.../restApiDoc/api#
Example for the sample app:
http://localhost:8080/RestApiDoc-example/restApiDoc/?doc_url=http://localhost:8080/RestApiDoc-example/restApiDoc/api#
In production, you have to put the json file (restapidoc.json by default) on the root of your app
(new File("restapidoc.json") from your app must work). You can change the location on the server with outputFileReading.
@RestApi(name = "book services", description = "Methods for managing books")
class BookController {
@RestApiMethod(description="Get a book")
@RestApiParams(params=[
@RestApiParam(name="id", type="long", paramType = RestApiParamType.PATH, description = "The book id")
])
def show() {
Book.read(params.id)
}
}
The result will be like this. You can test your method in the playground area.
@RestApiObject(name = "book", description = "A book available on the store")
class Book {
@RestApiObjectField(description = "The title of the book")
String title
@RestApiObjectField(description = "Year published for this book")
Integer year
@RestApiObjectField(description = "The book category")
String category
@RestApiObjectField(description = "The author of the book")
Author author
[...]
}
The result will be like this.
@RestApi(name = "book services", description = "Methods for managing books")
class BookController {
}
Parameter | Description | Comment |
---|---|---|
String name | The name of the service | |
String description | The service description |
@RestApiMethod(description="Get an author")
def show() {
}
@RestApiMethod(description="List all authors", listing = true)
def list() {
}
@RestApiMethod(description="Get stats data for an author", path="/custom_path/stats.{json}", verb = RestApiVerb.PUT) //path and verb are stupid...just for the example...
def stats() {
}
Declare on a method. Unfortunately, you cannot declare a Java Annotation on a Grails action (def show = {...}). Anyway, Grails methods are better than action/closure.
Parameter | Description | Comment |
---|---|---|
String description | The method description | |
String path | The HTTP path for this method | Optional. By now, we support 3 ways to define the path:
|
RestApiVerb verb | The HTTP verb for this method | Optional, similar to the "path" property.
|
String[] extensions | Extension for the path service |
Optional, by default the defaultFormat value ("json"). A service may have multiple extensions. The path (from UrlMappings, path attribute,...) must contain the string "{format}" (will be replaced by each extensions). For example: if path = "/user/{id}/picture.{format}" and extensions = ["png","jpg"], you will get two entries in the documentation.
The config value defaultFormatString ("{format}" by default) may be changed by something else. |
String[] produces | The output format | Optional, by default, the config value for defaultResponseType will be used (MediaType.APPLICATION_JSON_VALUE by default). You can change this with another MediaType. |
String[] consumes | The input format | Same as produces. |
boolean listing | The method will response a list (instead of an object) | Optional, by default false. If config property defaultParamsQueryMultiple is set, you can add default params for all listing methods. Usefull if you have, for example, max/offset for methods returning a list. |
A simple array of @RestApiParam
@RestApiMethod(description="Get an author")
@RestApiParams(params=[
@RestApiParam(name="id", type="long", paramType = RestApiParamType.PATH, description = "The author id")
])
def show() {
}
@RestApiMethod(description="List books written by the author", listing = true) //author/$id/book.json?max=$max
@RestApiParams(params=[
@RestApiParam(name="id", type="long", paramType = RestApiParamType.PATH, description = "The author id")
@RestApiParam(name="max", type="long", paramType = RestApiParamType.QUERY, description = "Limit for listing")
])
def listByAuthor() {
//params.id or params.max
}
Declare on a method
Parameter | Description | Comment |
---|---|---|
String name | The param name | |
String description | The param description | |
String type | The param type (String, Long, Book,...) | |
RestApiParamType paramType | The HTTP param type (PATH or QUERY) |
If you use Controller method with parameters, the type option may be ignored. The plugin will automatically retrieve the type. The @RestApiParam must be ordered like the method parameters.
@RestApiMethod(description="...)
@RestApiParams(params=[
@RestApiParam(name="lastname", paramType = RestApiParamType.PATH, description = "The lastname criteria"), // the plugin will get the type from the method parameter
])
def search(String lastname) {
//we don't use params.lastname,...
}
//a method in BookController will return Book by default...
def show() {
}
@RestApiResponseObject(objectIdentifier = "Long")
def getNumberOfBooks() {
}
@RestApiResponseObject(objectIdentifier = "[stats]") //see "Define Custom Domain" section
def stats() {
}
Declare on a method
Parameter | Description | Comment |
---|---|---|
String objectIdentifier | The method return type | Optional, by default the plugin will compute the name by looking to the controller name (BookController => Book). If you controller has a special name (LibraryController for Book domain), you can implements a “String currentDomainName()” method in your controller. It must return the domain name. If the method return a simple type (String, Integer,...), you can just set objectIdentifier with the good type. If the method return a complex type (which is not a domain), you can define a custom domain. |
Declare on a method
Parameter | Description | Comment |
---|---|---|
String name | The method input type | Same as objectIdentifier from @RestApiResponseObject |
An array of @RestApiError
Declare on a method
@RestApiErrors(apierrors=[
@RestApiError(code="404",description="The book was not found!")
])
def show() {
}
Parameter | Description | Comment |
---|---|---|
String code | The HTTP code | |
String description | The error description |
@RestApiObject(name = "author", description = "An author write books")
class Author {
Declare on a domain
Parameter | Description | Comment |
---|---|---|
String name | The name of the resource | |
String description | The resource description |
@RestApiObjectField(description = "The firstname of the author")
String firstname
@RestApiObjectField(description = "The lastname of the author")
String lastname
//Shhh keep it secret, it will not be print in response (!presentInResponse)
//Let say that if not set (!mandatory), we set birthday to 01/01/1970 (defaultValue)...
@RestApiObjectField(
description = "The birthday date of the author",
presentInResponse = false,
mandatory = false,
defaultValue = "01/01/1970"
)
Date birthday
@RestApiObjectField(
apiFieldName= "books",
description = "Books id written by this author",
allowedType = "List",
useForCreation = false
)
static hasMany = [book:Book]
@RestApiObjectFields(params=[
//This field is in response but not in domain (concat firstname and lastname). We can doc it on transients...
@RestApiObjectField(
apiFieldName= "fullname",
description = "The first and lastname",
allowedType = "String",
useForCreation = false)
])
static transients = []
Declare on a domain property
Parameter | Description | Comment |
---|---|---|
String description | The field description | |
String allowedType | The field type | Optional, by default, the type of the field is retrieve by the plugin. |
String apiFieldName | The field name in response | Optional, by default the name will be automatically retrieve from the class field name. Use case: If the field has another name in the response (response.theAuthor instead of response.author), you have to set the value with "theAuthor". |
boolean useForCreation | Field is used for creation | Optional, by default true (all fields are used for creation). Use case: An auto-id or a createdDate field should be set with useForCreation=false if there are not used when creating the domain. |
boolean mandatory | Field is mandatory. Only usefull if useForCreation=true. | Optional, by default all fields use for creation are mandatory. Use case: If the category is not mandatory to create a book, set mandatory to false. |
boolean defaultValue | Field default value. Only usefull if mandatory = false | Optional, by default an optional field (not mandatory) has the default value according its java type (number = 0, object = null, list = [],...) Use case: If by default, the category of a book is "None", set defaultValue = "None". |
boolean presentInResponse | Field is used for creation but no present in response. | Optional, by default all fields are present in response. Use case: If you have a User domain. You can add a user with name and password properties. But you don't want to have the password in response. The password will be "useForCreation" but not "presentInResponse". |
Sometimes, your service may response transient fields or fields from other domain. You can add annotations on other Grails properties like hasMany, transients,... The first example, an author may have a "fullname" (firstname + " " + lastname) in its JSON/XML response. Its a transient field (we only store firstname and lastname).
@RestApiObjectFields(params=[
@RestApiObjectField(
apiFieldName= "fullname",
description = "The first and last name",
allowedType = "String",
useForCreation = false)
])
static transients = []
The second example, an author may have a list of book. Let says that we only response the list of book's id (e.g. author.book=[1,5,6]).
@RestApiObjectField(
apiFieldName= "books",
description = "Books id written by this author",
allowedType = "List",
useForCreation = false)
static hasMany = [book:Book]
Sometimes, a service may response a special type (not a domain).
@RestApiMethod(description="Get stats data for an author")
@RestApiParams(params=[
@RestApiParam(
name="max",
type="int",
paramType = RestApiParamType.PATH,
description = "Max number of author to retrieve")
])
def stats() {
def author = Author.read(params.id)
render ([fullname:author + " " + author.lastname, numberOfBook: author.book.size()] as JSON).toString()
}
The response is an object with fullname and numberOfBook properties.
You can create a class. You can call it "CustomResponseDoc" (or something else...) and put it wherever you want in your src/ directory.
Change the config property customClassName with the full class path (customClassName="my.package.CustomResponseDoc").
To define a new resource type, add a new field and fill the RestApiObjectField and RestApiObjectField(s).
package my.package
class CustomResponseDoc {
@RestApiObjectField(description = "An author stats")
@RestApiObjectFields(params=[
@RestApiObjectField(
apiFieldName = "fullname",
description = "The author fullname",
allowedType = "String",
useForCreation = false),
@RestApiObjectField(
apiFieldName = "numberOfBook",
description = "The number of books created by this author",
allowedType = "Integer",
useForCreation = false)
])
static def stats
[...]
}
Now you can add @RestApiResponseObject(objectIdentifier = "[stats]") on the method stats() from your controller.
There are some default values defined for each configuration fields. You can simply override them. The config path is grails.plugins.restapidoc.
Name | Description | Default |
---|---|---|
String docVersion | The documentation version | "0.1" |
String basePath | The path for your app. If you don't override this, you can't play with doc playground. | "Fill with basePath config" |
String outputFileGeneration | The file where to store the JSON after generation | "restapidoc.json" |
String outputFileReading | The JSON file to read when looking for the report. Usefull if you upload the file in a specific location on the server. | "restapidoc.json" |
String customClassName | The class path for all customs domains (Define custom domain). You must put the full package path and class name. | null |
String defaultFormat | The default format in URL .$format => .json |
"json" |
String defaultResponseType | The default response type | MediaType.APPLICATION_JSON_VALUE |
String controllerPrefix | Usefull if you have a prefix in your controller. With MyBookController, the domain will be MyBook. If controllerPrefix is "My", It will be "Book" | "" |
String controllerSuffix | See controllerPrefix | "Controller" |
String defaultFormatString | The string representing the format in each service path. This will be replaced by "defaultFormat" value (from config) or extensions value (from annotation doc) | "{format}" |
List defaultObjectFields | You can add some default doc fields for all your domains. Example: grails.plugins.restapidoc.defaultObjectFields = [[name:"id", type:"Long", useFroCreation="false"]] |
[] |
List defaultErrorAll | Default errors to define for all methods. For example: ["400": "Bad Request: missing parameters or bad message format", "401": "Unauthorized: must be auth", "403": "Forbidden: role error", "404": "Object not found"] |
[] |
List defaultErrorGet | Same as defaultErrorAll, but only for GET methods | [] |
List defaultErrorPost | Same as defaultErrorAll, but only for POST methods. For example: [ "409": "Object already exist" ] |
[ ] |
List defaultErrorPut | Same as defaultErrorAll, but only for PUT methods | [ ] |
List defaultErrorDelete | Same as defaultErrorAll, but only for DELETE methods | [] |
Map verbPerMethodPrefix | Map to link a method prefix name with a verb. Only used if no verb is defined in @RestApiMethod and no URL Mappings rules are found for the method. You can override or add new rules (e.g. "modify": PUT) |
[ "show" : "GET", "list" : "GET", "save" : "POST", "add" : "POST", "update" : "PUT", "edit" : "PUT", "delete" : "DELETE", "remove" : "DELETE" ] |
Map pathPerMethodPrefix | Same as verbPerMethodPrefix but for the path. Only used if no path is defined in @RestApiMethod and no URL Mappings rules are found for the method. | [ "show" : "show/{id}", "list" : "list", "add" : "add", "save" : "save", "update" : "udpate/{id}", "edit" : "edit/{id}", "delete" : "delete/{id}", "remove" : "delete/{id}", ] |
String grailsDomainDefaultType | The default type for your Grails Domain. You may set "Long" if you only set an id for a Book.author in a book response. By default, it will be "Author" |
null, meaning that it will be the domain Class name |
List defaultParamsQueryAll | Default params for all your queries | [] |
List defaultParamsQuerySingle | Default params for all your methods where listing = false | [] |
List defaultParamsQueryMultiple | Default params for all your methods where listing = true For example: defaultParamsQueryMultiple = [ [name:"max",description:"Pagination: Number of record per page (default 0 = no pagination)",type:"int"], [name:"offset",description:"Pagination: Offset of first record (default 0 = first record)",type:"int"] ] |
[] |
If you find that you need a feature that RestApiDoc does not currently support, either let me know via the Github issue tracker, or fork the project and and easily extend RestApidDoc plugin!
Copyright © 2014 Rollus Loïc