Skip to content

Commit

Permalink
https://github.com/atuttle/Taffy/issues/239
Browse files Browse the repository at this point in the history
Added the ability to specify a comma delimited list of URIs for taffy_uri, allowing you to specify multiple endpoint URIs to be served by the same resource .  It will not recognize commas inside of regular expression brackets {}.

Fixed an issue where status.skippedResources was being overwritten when hot-swapping.

Added tests for the new alias', as well as reconfiguring the tests to keep testing the skippedResources properly.

Added a test for conflicting URI errors.
  • Loading branch information
ryanguill committed Dec 9, 2014
1 parent 91ddbc4 commit ae4979e
Show file tree
Hide file tree
Showing 8 changed files with 199 additions and 67 deletions.
119 changes: 80 additions & 39 deletions core/api.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -514,9 +514,9 @@
<cfif directoryExists(local.resourcePath)>
<!--- setup internal bean factory --->
<cfset local._taffy.factory = createObject("component", "taffy.core.factory").init() />
<cfset local._taffy.factory.loadBeansFromPath(local.resourcePath, guessResourcesCFCPath(), guessResourcesFullPath(), true) />
<cfset local._taffy.factory.loadBeansFromPath(local.resourcePath, guessResourcesCFCPath(), guessResourcesFullPath(), true, local._taffy) />
<cfset local._taffy.beanList = local._taffy.factory.getBeanList() />
<cfset local._taffy.endpoints = cacheBeanMetaData(local._taffy.factory, local._taffy.beanList) />
<cfset local._taffy.endpoints = cacheBeanMetaData(local._taffy.factory, local._taffy.beanList, local._taffy) />
<cfset local._taffy.status.internalBeanFactoryUsed = true />
<!---
if both an external bean factory and the internal factory are in use (because of /resources folder),
Expand Down Expand Up @@ -929,21 +929,50 @@
<cfabort />
</cffunction>

<cffunction name="splitURIs" access="private" returntype="array" output="false">
<cfargument name="input" type="string" required="true" />

<cfset var inBracketCount = 0 />
<cfset var output = [] />
<cfset var s = createObject("java", "java.lang.StringBuffer").init("") />
<cfset var c = "" />
<cfloop array="#trim(input).toCharArray()#" index="c">
<cfif c EQ "{">
<cfset inBracketCount += 1 />
<cfelseif c EQ "}" AND inBracketCount GT 0>
<cfset inBracketCount -= 1 />
</cfif>
<cfif c EQ "," AND inBracketCount EQ 0>
<cfset arrayAppend(output, s.toString()) />
<cfset s.setLength(0) />
<cfelse>
<cfset s.append(c) />
</cfif>
</cfloop>
<cfif s.length() GT 0>
<cfset arrayAppend(output, s.toString()) />
</cfif>
<cfreturn output />
</cffunction>

<cffunction name="cacheBeanMetaData" access="private" output="false">
<cfargument name="factory" required="true" />
<cfargument name="beanList" type="string" required="true" />
<cfargument name="taffyRef" type="any" required="True" />
<cfset var local = StructNew() />
<cfset local.endpoints = StructNew() />
<cfloop list="#arguments.beanList#" index="local.beanName">
<!--- get the cfc metadata that defines the uri for that cfc --->
<cfset local.cfcMetadata = getMetaData(arguments.factory.getBean(local.beanName)) />
<cfset local.uri = '' />
<cfset local.uriAttr = '' />
<cfif structKeyExists(local.cfcMetadata, "taffy_uri")>
<cfset local.uri = local.cfcMetadata["taffy_uri"] />
<cfset local.uriAttr = local.cfcMetadata["taffy_uri"] />
<cfelseif structKeyExists(local.cfcMetadata, "taffy:uri")>
<cfset local.uri = local.cfcMetadata["taffy:uri"] />
<cfset local.uriAttr = local.cfcMetadata["taffy:uri"] />
</cfif>

<cfset local.uris = splitURIs(local.uriAttr) />

<cfif structKeyExists(local.cfcMetaData, "taffy:aopbean")>
<cfset local.cachedBeanName = local.cfcMetaData["taffy:aopbean"] />
<cfelseif structKeyExists(local.cfcMetaData, "taffy_aopbean")>
Expand All @@ -952,42 +981,54 @@
<cfset local.cachedBeanName = local.beanName />
</cfif>

<!--- if it doesn't have a uri, then it's not a resource --->
<cfif len(local.uri)>
<cfset local.metaInfo = convertURItoRegex(local.uri) />
<cfif structKeyExists(local.endpoints, local.metaInfo.uriRegex)>
<cfthrow
message="Duplicate URI scheme detected. All URIs must be unique (excluding tokens)."
detail="The URI for `#local.beanName#` conflicts with the existing URI definition of `#local.endpoints[local.metaInfo.uriRegex].beanName#`"
errorcode="taffy.resources.DuplicateUriPattern"
/>
</cfif>
<cfset local.endpoints[local.metaInfo.uriRegex] = { beanName = local.cachedBeanName, tokens = local.metaInfo.tokens, methods = structNew(), srcURI = local.uri } />
<cfif structKeyExists(local.cfcMetadata, "functions")>
<cfloop array="#local.cfcMetadata.functions#" index="local.f">
<cfif local.f.name eq "get" or local.f.name eq "post" or local.f.name eq "put" or local.f.name eq "patch" or local.f.name eq "delete" or local.f.name eq "head" or local.f.name eq "options">
<cfset local.endpoints[local.metaInfo.uriRegex].methods[local.f.name] = local.f.name />

<!--- also support future/misc verbs via metadata --->
<cfelseif structKeyExists(local.f,"taffy:verb")>
<cfset local.endpoints[local.metaInfo.uriRegex].methods[local.f["taffy:verb"]] = local.f.name />
<cfelseif structKeyExists(local.f,"taffy_verb")>
<cfset local.endpoints[local.metaInfo.uriRegex].methods[local.f["taffy_verb"]] = local.f.name />
<!--- if it doesn't have any uris, then it's not a resource --->
<cfif arrayLen(local.uris)>
<cfloop array="#local.uris#" index="local.uri">
<cftry>
<cfset local.uri = trim(local.uri) />
<cfset local.metaInfo = convertURItoRegex(local.uri) />
<cfif structKeyExists(local.endpoints, local.metaInfo.uriRegex)>
<cfthrow
message="Duplicate URI scheme detected. All URIs must be unique (excluding tokens)."
detail="The URI (#local.uri#) for `#local.beanName#` conflicts with the existing URI definition of `#local.endpoints[local.metaInfo.uriRegex].beanName#`"
errorcode="taffy.resources.DuplicateUriPattern"
/>
</cfif>

<!--- cache any extra function metadata for use in onTaffyRequest --->
<cfset local.extraMetadata = duplicate(local.f) />
<cfset structDelete(local.extraMetadata, "name") />
<cfset structDelete(local.extraMetadata, "parameters") />
<cfset structDelete(local.extraMetadata, "hint") />
<cfparam name="local.endpoints['#local.metaInfo.uriRegex#'].metadata" default="#structNew()#" />
<cfif not structIsEmpty(local.extraMetadata)>
<cfset local.endpoints[local.metaInfo.uriRegex].metadata[local.f.name] = local.extraMetadata />
<cfelse>
<cfset local.endpoints[local.metaInfo.uriRegex].metadata[local.f.name] = StructNew() />
<cfset local.endpoints[local.metaInfo.uriRegex] = { beanName = local.cachedBeanName, tokens = local.metaInfo.tokens, methods = structNew(), srcURI = local.uri } />
<cfif structKeyExists(local.cfcMetadata, "functions")>
<cfloop array="#local.cfcMetadata.functions#" index="local.f">
<cfif local.f.name eq "get" or local.f.name eq "post" or local.f.name eq "put" or local.f.name eq "patch" or local.f.name eq "delete" or local.f.name eq "head" or local.f.name eq "options">
<cfset local.endpoints[local.metaInfo.uriRegex].methods[local.f.name] = local.f.name />

<!--- also support future/misc verbs via metadata --->
<cfelseif structKeyExists(local.f,"taffy:verb")>
<cfset local.endpoints[local.metaInfo.uriRegex].methods[local.f["taffy:verb"]] = local.f.name />
<cfelseif structKeyExists(local.f,"taffy_verb")>
<cfset local.endpoints[local.metaInfo.uriRegex].methods[local.f["taffy_verb"]] = local.f.name />
</cfif>

<!--- cache any extra function metadata for use in onTaffyRequest --->
<cfset local.extraMetadata = duplicate(local.f) />
<cfset structDelete(local.extraMetadata, "name") />
<cfset structDelete(local.extraMetadata, "parameters") />
<cfset structDelete(local.extraMetadata, "hint") />
<cfparam name="local.endpoints['#local.metaInfo.uriRegex#'].metadata" default="#structNew()#" />
<cfif not structIsEmpty(local.extraMetadata)>
<cfset local.endpoints[local.metaInfo.uriRegex].metadata[local.f.name] = local.extraMetadata />
<cfelse>
<cfset local.endpoints[local.metaInfo.uriRegex].metadata[local.f.name] = StructNew() />
</cfif>
</cfloop>
</cfif>
</cfloop>
</cfif>
<cfcatch>
<!--- skip cfc's with errors, but save info about them for display in the dashboard --->
<cfset local.err = structNew() />
<cfset local.err.resource = local.beanName />
<cfset local.err.exception = cfcatch />
<cfset arrayAppend(arguments.taffyRef.status.skippedResources, local.err) />
</cfcatch>
</cftry>
</cfloop>
</cfif>
</cfloop>
<cfreturn local.endpoints />
Expand Down
7 changes: 4 additions & 3 deletions core/factory.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,13 @@
<cfargument name="resourcesPath" type="string" default="resources" />
<cfargument name="resourcesBasePath" type="string" default="" />
<cfargument name="isFullReload" type="boolean" default="false" />
<cfargument name="taffyRef" type="any" required="false" default="#structNew()#" />
<cfset var local = StructNew() />
<!--- cache all of the beans --->
<cfif isFullReload>
<cfset this.beans = structNew() />
<cfset application._taffy.status.skippedResources = arrayNew(1) /> <!--- empty out the array on factory reloads --->
<cfset application._taffy.beanList = "" />
<cfset arguments.taffyRef.status.skippedResources = arrayNew(1) /> <!--- empty out the array on factory reloads --->
<cfset arguments.taffyRef.beanList = "" />
</cfif>
<!--- if the folder doesn't exist, do nothing --->
<cfif not directoryExists(arguments.beanPath)>
Expand All @@ -78,7 +79,7 @@
<cfset local.err = structNew() />
<cfset local.err.resource = local.beanName />
<cfset local.err.exception = cfcatch />
<cfset arrayAppend(application._taffy.status.skippedResources, local.err) />
<cfset arrayAppend(arguments.taffyRef.status.skippedResources, local.err) />
</cfcatch>
</cftry>
</cfloop>
Expand Down
25 changes: 15 additions & 10 deletions dashboard/dashboard.cfm
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,13 @@
<cfloop from="1" to="#arrayLen(application._taffy.status.skippedResources)#" index="local.i">
<cfset local.err = application._taffy.status.skippedResources[local.i] />
<div class="alert alert-warning">
<strong class="label label-warning"><cfoutput>#local.err.resource#</cfoutput></strong> contains a syntax error.
<cfif structKeyExists(local.err.exception, 'tagContext')>
<strong>Error on line #local.err.exception.tagcontext[1].line#:</strong>
<cfif structKeyExists(local.err, "Exception") AND structKeyExists(local.err.Exception, "ErrorCode") AND local.err.Exception.ErrorCode EQ "taffy.resources.DuplicateUriPattern">
<strong class="label label-warning"><cfoutput>#local.err.resource#</cfoutput></strong> contains a conflicting URI.
<cfelse>
<strong class="label label-warning"><cfoutput>#local.err.resource#</cfoutput></strong> contains a syntax error.
<cfif structKeyExists(local.err.exception, 'tagContext')>
<strong>Error on line #local.err.exception.tagcontext[1].line#:</strong>
</cfif>
</cfif>
<hr/>
<code>
Expand All @@ -203,10 +207,11 @@
<cfoutput>
<cfloop from="1" to="#arrayLen(application._taffy.uriMatchOrder)#" index="local.resource">
<cfset local.currentResource = application._taffy.endpoints[application._taffy.uriMatchOrder[local.resource]] />
<cfset local.resourceHTTPID = local.currentResource.beanName & "_" & hash(local.currentResource.srcURI) />
<div class="panel panel-default">
<div class="panel-heading">
<h4 class="panel-title">
<a href="###local.currentResource.beanName#" class="accordion-toggle" data-toggle="collapse" data-parent="##resourcesAccordion">
<a href="###local.resourceHTTPID#" class="accordion-toggle" data-toggle="collapse" data-parent="##resourcesAccordion">
#local.currentResource.beanName#
</a>
<cfloop list="DELETE|warning,PATCH|warning,PUT|warning,POST|danger,GET|primary" index="local.verb">
Expand All @@ -219,7 +224,7 @@
<code style="float:right; margin-top: -15px; display: inline-block;">#local.currentResource.srcUri#</code>
</h4>
</div>
<div class="panel-collapse collapse" id="#local.currentResource.beanName#">
<div class="panel-collapse collapse" id="#local.resourceHTTPID#">
<div class="panel-body resourceWrapper">
<div class="col-md-6 runner">
<div class="well resource" data-uri="#local.currentResource.srcUri#" data-bean-name="#local.currentResource.beanName#">
Expand Down Expand Up @@ -268,10 +273,10 @@
<cfloop from="1" to="#arrayLen(local.currentResource.tokens)#" index="local.token">
<div class="form-group row">
<div class="col-md-3">
<label class="control-label" for="token_#local.currentResource.beanName#_#local.currentResource.tokens[local.token]#">#local.currentResource.tokens[local.token]#:</label>
<label class="control-label" for="token_#local.resourceHTTPID#_#local.currentResource.tokens[local.token]#">#local.currentResource.tokens[local.token]#:</label>
</div>
<div class="col-md-6">
<input id="token_#local.currentResource.beanName#_#local.currentResource.tokens[local.token]#" name="#local.currentResource.tokens[local.token]#" type="text" class="form-control input-sm" />
<input id="token_#local.resourceHTTPID#_#local.currentResource.tokens[local.token]#" name="#local.currentResource.tokens[local.token]#" type="text" class="form-control input-sm" />
</div>
</div>
</cfloop>
Expand All @@ -289,7 +294,7 @@

<div class="reqBody">
<h4>Request Body:</h4>
<textarea id="#local.currentResource.beanName#_RequestBody" class="form-control input-sm" rows="5"></textarea>
<textarea id="#local.resourceHTTPID#_RequestBody" class="form-control input-sm" rows="5"></textarea>
<cfset local.md = getMetaData(application._taffy.factory.getBean(local.currentResource.beanName)) />
<cfif structKeyExists(local.md,"functions")>
<cfset local.functions = local.md.functions />
Expand Down Expand Up @@ -322,8 +327,8 @@
</cfloop>
<!--- save to page JS for runtime reference --->
<script>
taffy.resources['#local.currentResource.beanName#'] = taffy.resources['#local.currentResource.beanName#'] || {};
taffy.resources['#local.currentResource.beanName#']['#lcase(local.functions[local.f].name)#'] = #serializeJson(local.args)#;
taffy.resources['#local.resourceHTTPID#'] = taffy.resources['#local.currentResource.beanName#'] || {};
taffy.resources['#local.resourceHTTPID#']['#lcase(local.functions[local.f].name)#'] = #serializeJson(local.args)#;
</script>
</cfif>
</cfloop>
Expand Down
11 changes: 11 additions & 0 deletions tests/resources/ConflictingURI.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/conflict/{URI}, /conflict/{URI}" hint="I have multiple uris">

<cffunction name="get">
<cfargument name="uri" type="string" default="0" />
<cfset local.res = {} />
<cfset local.res.uri = arguments.uri />

<cfreturn representationOf(local.res) />
</cffunction>

</cfcomponent>
21 changes: 21 additions & 0 deletions tests/resources/EchoURIAlias.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<cfcomponent extends="taffy.core.resource" taffy:uri="/echo_alias/{id}, /echo_alias" hint="I have multiple uris">

<cffunction name="get">
<cfargument name="id" default="0" />
<cfset local.res = {} />
<cfset local.res.id = arguments.id />

<cfreturn representationOf(local.res) />
</cffunction>

<cffunction name="put">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>

<cffunction name="post">
<cfargument name="id" />
<cfreturn representationOf(arguments).withStatus(200) />
</cffunction>

</cfcomponent>
26 changes: 26 additions & 0 deletions tests/tests/TestConflict.cfc
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<cfcomponent extends="base">

<cffunction name="setup">
<cfset local.apiRootURL = getDirectoryFromPath(cgi.script_name) />
<cfset local.apiRootURL = listDeleteAt(local.apiRootURL,listLen(local.apiRootURL,'/'),'/') />
<cfhttp method="GET" url="http://#CGI.SERVER_NAME#:#CGI.SERVER_PORT##local.apiRootURL#/index.cfm?#application._taffy.settings.reloadkey#=#application._taffy.settings.reloadPassword#" />
</cffunction>

<cfscript>
function beforeTests(){
variables.taffy = createObject("component","taffy.tests.Application");
makePublic(variables.taffy, "getBeanFactory");
variables.factory = variables.taffy.getBeanFactory();
variables.factory.loadBeansFromPath( expandPath('/taffy/tests/resourcesConflict'), 'taffy.tests.resourcesConflict', expandPath('/taffy/tests/resourcesConflict'), true, variables.taffy);

}

function conflicting_URIs_get_skipped() {
assertEquals(1, arrayLen(application._taffy.status.skippedResources), "Conflicting URIs not showing in errors");
var err = application._taffy.status.skippedResources[1];
assertEquals("taffy.resources.DuplicateUriPattern", local.err.Exception.ErrorCode);
}
</cfscript>


</cfcomponent>
27 changes: 27 additions & 0 deletions tests/tests/TestCore.cfc
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,33 @@
assertTrue(isArray(local.response._body));
assertTrue(arrayLen(local.response._body) == 3);
}

function comma_delim_list_of_uris_for_alias(){

//works with /echo_alias/{ID}
local.result = apiCall("get", "/echo_alias/4", "");
// debug(local.result);
assertEquals(200, val(local.result.statusCode));
assertEquals(serializeJSON({ID=4}), local.result.fileContent);

//works with /echo_alias
local.result = apiCall("get", "/echo_alias", "");
// debug(local.result);
assertEquals(200, val(local.result.statusCode));
assertEquals(serializeJSON({ID=0}), local.result.fileContent);

//works with /echo_alias/ (trailing slash)
local.result = apiCall("get", "/echo_alias/", "");
// debug(local.result);
assertEquals(200, val(local.result.statusCode));
assertEquals(serializeJSON({ID=0}), local.result.fileContent);

//works with /echo_alias?ID=x
local.result = apiCall("get", "/echo_alias", "ID=2");
// debug(local.result);
assertEquals(200, val(local.result.statusCode));
assertEquals(serializeJSON({ID=2}), local.result.fileContent);
}
</cfscript>


Expand Down
Loading

0 comments on commit ae4979e

Please sign in to comment.