Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve MappingRules to match dynamic actions of custom Rest Controllers #58

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 10 additions & 15 deletions src/groovy/org/restapidoc/utils/BuildPathMap.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -48,27 +48,23 @@ class BuildPathMap extends AnsiConsoleUrlMappingsRenderer {
final controllerUrlMappings = mappingsByController.get(controller)
for (UrlMapping urlMapping in controllerUrlMappings) {
def urlPattern = establishUrlPattern(urlMapping, isAnsiEnabled, longestMapping)

if (urlMapping?.actionName) {
// urlMapping can be either a string or a closure that returns the result
if (urlMapping?.actionName instanceof String) {
rules.addRule(controller.toString(), urlMapping.actionName.toString(), cleanString(urlPattern), urlMapping.httpMethod, grailsApplication.mergedConfig.grails.plugins.restapidoc.defaultFormat)
} else {
urlMapping?.actionName.each { actName ->
urlPattern = urlPattern.replace("\${", "{") //replace ${format} with {format}
rules.addRule(controller.toString(), actName.value, cleanString(urlPattern), actName.key, grailsApplication.mergedConfig.grails.plugins.restapidoc.defaultFormat)
}
// urlMapping can be either a string or a closure that returns the map result
if (urlMapping?.actionName instanceof Map) {
urlMapping?.actionName.each { actName ->
urlPattern = urlPattern.replace("\${", "{") //replace ${format} with {format}
rules.addRule(controller.toString(), actName.value, cleanString(urlPattern), actName.key, grailsApplication.mergedConfig.grails.plugins.restapidoc.defaultFormat)
}
}
else {
String actionName = urlMapping.actionName?.toString() ?: ""
rules.addRule(controller.toString(), actionName, cleanString(urlPattern), urlMapping.httpMethod, grailsApplication.mergedConfig.grails.plugins.restapidoc.defaultFormat)
}
}

}

return rules
}
//string from url mapping are dirty (escape char,...)
public static String cleanString(String dirtyString) {

Pattern escapeCodePattern = Pattern.compile(
"\u001B" // escape code
+ "\\["
Expand All @@ -77,7 +73,6 @@ class BuildPathMap extends AnsiConsoleUrlMappingsRenderer {
+ "[@-~]"
+ "|\\\$" // Url mapping returns parameters formatted like this : ${parameter}.
);
escapeCodePattern.matcher(dirtyString).replaceAll("");

escapeCodePattern.matcher(dirtyString).replaceAll("").trim();
}
}
82 changes: 45 additions & 37 deletions src/groovy/org/restapidoc/utils/JSONDocUtilsLight.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -149,26 +149,55 @@ public class JSONDocUtilsLight extends JSONDocUtils {
}

public RestApiMethodDoc extractMethodDocs(Set<Class<?>> objectClasses, Method method, Class<?> controller, MappingRules rules, String extension) {
//Controller name
String controllerName = controller.simpleName
if (controllerName.endsWith(CONTROLLER_SUFFIX)) {
controllerName = controllerName.substring(0, controllerName.size() - CONTROLLER_SUFFIX.size())
}

//Retrieve the path/verb to go to this method
MappingRulesEntry rule = rules.getRule(controller.simpleName, method.name)
String verb = method.getAnnotation(RestApiMethod.class).verb().name()
//URL Params
def urlParams = []
def queryParameters = []
if (method.isAnnotationPresent(RestApiParams.class)) {
urlParams = RestApiParamDoc.buildFromAnnotation(method.getAnnotation(RestApiParams.class), RestApiParamType.PATH,method)
queryParameters = RestApiParamDoc.buildFromAnnotation(method.getAnnotation(RestApiParams.class), RestApiParamType.QUERY,method)
}

//Retrieve the path to go to this method
MappingRulesEntry rule = rules.matchRule(controllerName, method.name, urlParams, extension)
String path

//Retrieve the verb to go to this method
def annotation = method.getAnnotation(RestApiMethod.class)
String verb = method.getAnnotation(RestApiMethod.class).verb().name()
println "annotation.verb()=${annotation.verb()}"

if (annotation.verb() != RestApiVerb.NULL) {
//verb is defined in the annotation
verb = method.getAnnotation(RestApiMethod.class).verb().name().toUpperCase()
}
else if (rule) {
//verb is defined in the urlmapping
verb = rule.verb
}
else {
verb = "GET"
//if no explicit url mapping rules, take dynamic rule
VERB_PER_METHOD_PREFIX.each {
if (method.name.startsWith(it.key)) {
verb = it.value
}
}
}

if (!annotation.path().equals("Undefined")) {
//path is defined in the annotation
path = method.getAnnotation(RestApiMethod.class).path()
} else if (rule) {
//path is defined in the urlmapping
path = rule.path

path = fillPathAction(rule.path, method.name)
} else {
//nothing is defined
String controllerName = controller.simpleName
if (controllerName.endsWith(CONTROLLER_SUFFIX)) {
controllerName = controllerName.substring(0, controllerName.size() - CONTROLLER_SUFFIX.size())
}
controllerName = splitCamelToBlank(Introspector.decapitalize(controllerName))

String actionWithPathParam = "/" + method.name
Expand All @@ -184,43 +213,15 @@ public class JSONDocUtilsLight extends JSONDocUtils {

path = "/" + controllerName + actionWithPathParam + format
}

path = extension ? path.replace(DEFAULT_FORMAT_NAME, extension) : path

println "annotation.verb()=${annotation.verb()}"

if (annotation.verb() != RestApiVerb.NULL) {
//verb is defined in the annotation
verb = method.getAnnotation(RestApiMethod.class).verb().name().toUpperCase()
} else if (rule) {
//verb is defined in the urlmapping
verb = rule.verb

} else {
verb = "GET"
//if no explicit url mapping rules, take dynamic rule
VERB_PER_METHOD_PREFIX.each {
if (method.name.startsWith(it.key)) {
verb = it.value
}
}

}

RestApiMethodDoc apiMethodDoc = RestApiMethodDoc.buildFromAnnotation(method.getAnnotation(RestApiMethod.class), path, verb, DEFAULT_TYPE);
apiMethodDoc.methodName = method.name

if (method.isAnnotationPresent(RestApiHeaders.class)) {
apiMethodDoc.setHeaders(RestApiMethodDoc.buildFromAnnotation(method.getAnnotation(RestApiHeaders.class)));
}

def urlParams = []
def queryParameters = []
if (method.isAnnotationPresent(RestApiParams.class)) {
urlParams = RestApiParamDoc.buildFromAnnotation(method.getAnnotation(RestApiParams.class), RestApiParamType.PATH,method)
queryParameters = RestApiParamDoc.buildFromAnnotation(method.getAnnotation(RestApiParams.class), RestApiParamType.QUERY,method)
}

DEFAULT_PARAMS_QUERY_ALL.each {
queryParameters.add(new RestApiParamDoc(it.name, it.description, it.type, "false", new String[0], Enum, ""))
}
Expand Down Expand Up @@ -371,4 +372,11 @@ public class JSONDocUtilsLight extends JSONDocUtils {
}
return str
}

static String fillPathAction(String path, String action) {
if (path.contains('{action}')) {
return path.replaceFirst(/\{action}/, action)
}
return path
}
}
132 changes: 113 additions & 19 deletions src/groovy/org/restapidoc/utils/MappingRulesEntry.groovy
Original file line number Diff line number Diff line change
@@ -1,42 +1,136 @@
package org.restapidoc.utils

import groovy.util.logging.Log
import org.restapidoc.pojo.RestApiParamDoc

/**
* Created by lrollus on 1/10/14.
*/
@Log
class MappingRules {

// static String FIRSTCHARPATH = "/api"
Map<String, List<MappingRulesEntry>> rules = new TreeMap<String, MappingRulesEntry>()

public void addRule(String controllerName, String actionName, String path, String verb, String defaultFormat = "") {
def entries = rules.get(controllerName) ?: new ArrayList<MappingRulesEntry>()
entries.push(new MappingRulesEntry(path: path, verb: verb, action: actionName, format: defaultFormat))
rules.put(controllerName, entries)
}

/**
* Match a mapping rule from grails UrlMappings
* @param controller Controller name to look at
* @param action Controller action to match
* @param urlParams Paths params as declared in UrlMappings and constraints
* @param format Output format (json, xml, ...)
* @return
*/
MappingRulesEntry matchRule(String controller, String action, List<RestApiParamDoc> urlParams = [], String format) {
MappingRulesEntry matchRule
String controllerName = camelUpperCase(controller)
log.info("controller: $controllerName / action: $action")
List<MappingRulesEntry> entries = rules.get(controllerName)
if (!entries) return

// Find by action
matchRule = matchByAction(controllerName, action)
if (matchRule) {
println "Matched rule by action: " + matchRule
return matchRule
}
else {
// Find by URL params
matchRule = matchByURLParams(controllerName, urlParams, format)
println "Matched rule by URL params: " + matchRule
}
return matchRule
}

// static String DEFAULT_FORMAT = "json"
/**
* Macth mapping rule by controller action
* @param controllerName
* @param action
* @return
*/
private MappingRulesEntry matchByAction(String controllerName, String action) {
if (!action) return null
return rules.get(controllerName).find { it.action == action }
}

Map<String, MappingRulesEntry> rules = new TreeMap<String, MappingRulesEntry>()
/**
* Match mapping rule by path params
* @param controllerName
* @param urlParams
* @param format
* @return
*/
private MappingRulesEntry matchByURLParams(String controllerName, List<RestApiParamDoc> urlParams, String format = "") {
if (urlParams) {
def params = urlParams.collect { it.name }
def matches = rules.get(controllerName)?.findAll { matchAllParams(it.path, params) }
if (matches) {
if (matches.size() > 1) {
// Find out if format has been set
matches = matches.findAll { it.format == format }
}
return matches[0]
}
return null
}
}

public void addRule(String controllerName, String actioName, String path, String verb, String defaultFormat) {
String key = (controllerName + "." + actioName).toUpperCase()
key = key.replace("CONTROLLER", "")
protected String camelUpperCase(String stringToSplit) {
if (!stringToSplit) return ""
String result = stringToSplit[0].toLowerCase()
for (int i = 1; i < stringToSplit.size(); i++) {
def car = stringToSplit[i]
if (car != car.toUpperCase()) {
result = result + car
} else {
result = result + car.toUpperCase()
}
}

String shortPath = path
// if(shortPath.startsWith(FIRSTCHARPATH)) {
// shortPath = shortPath.substring(FIRSTCHARPATH.size())
// }
//shortPath = shortPath.replace("{format}",defaultFormat)
return result.trim()
}

rules.put(key, new MappingRulesEntry(path: shortPath, verb: verb))
/**
* Check if rule path match all given params.
* Reserved tokens like {action} or {format} are filtered.
*
* @param path
* @param params
* @return
*/
protected boolean matchAllParams(String path, List<String> params) {
def filtered = ['action', 'format']
def pattern = /\{\w*}/
def pathParams = (path =~ pattern).findAll { !(param(it) in filtered) }.collect { param(it) }
return params.sort() == pathParams.sort()
}

public MappingRulesEntry getRule(String controllerName, String actionName) {
String key = (controllerName + "." + actionName).toUpperCase()
key = key.replace("CONTROLLER", "")
rules.get(key)
private String param(String tokenParam) {
String param = tokenParam.replaceFirst(/\{/, '')
param = param.replaceFirst(/}/, '')
return param
}

@Override
public String toString() {
return "MappingRules{" +
"rules=" + rules +
'}';
}
}

class MappingRulesEntry {
public String path
public String verb

String path
String verb
String action
String format

public String toString() {
return "path = $path , verb = $verb"
return "path = $path , verb = $verb , action = $action , format = $format"
}
}
Loading