diff --git a/models/RoutesParser.cfc b/models/RoutesParser.cfc index cc1c6a5..6d6dc01 100644 --- a/models/RoutesParser.cfc +++ b/models/RoutesParser.cfc @@ -284,6 +284,21 @@ component accessors="true" threadsafe singleton { for ( var methodList in actions ) { // handle any delimited method lists for ( var methodName in listToArray( methodList ) ) { + // + // If we have handler component metadata (when do we expect it to be null?), + // and we have function metadata (when do we expect it to be null?), + // and there is an appropriate annotation on the function from that function's metadata, + // then do not expose this function to swagger docs. + // + if ( !isNull( arguments.handlerMetadata ) ) { + var functionMetadata = getFunctionMetadata( handlerMetadata = arguments.handlerMetadata, functionName = lCase( actions[ methodName ] ) ); + if ( !isNull( functionMetadata ) ) { + if ( !isDocumentationEnabled( functionMetadata = functionMetadata ) ) { + continue; + } + } + } + // method not in error methods if ( !arrayFindNoCase( errorMethods, actions[ methodList ] ) ) { // Create new path template @@ -324,6 +339,11 @@ component accessors="true" threadsafe singleton { } } + if ( structIsEmpty( path ) ) { + // if we skipped over every possible verb, there's nothing to place into swagger docs for this route + return; + } + // Strip out any typing placeholders in routes var pathSegments = listToArray( arguments.pathKey, "/" ); var typingParams = [ "numeric", "alpha", "regex:" ]; @@ -592,6 +612,23 @@ component accessors="true" threadsafe singleton { } } + /** + * return true if the supplied metadata represents a function that has NOT been explicitly marked as disabled + */ + private boolean function isDocumentationEnabled( required struct functionMetadata ) { + // + // function foo() {} <-- no attr, documentation is enabled (depending on module config) + // function foo() cbSwagger {} <-- has attr with no value, documentation is enabled (depending on module config) + // function foo() cbSwagger=true {} <-- has attr, with truthy value, documentation is enabled (depending on module config) + // function foo() cbSwagger=false {} <-- has attr, with falsy value, documentation is disabled (overrides module config) + // function foo() cbSwagger=xyz {} <-- has attr, with non-booleanish value, which we consider falsy, documentation is disabled (overrides module config) + // + return !structKeyExists( functionMetadata, "cbSwagger" ) + ? true // there is no `cbSwagger` attribute, so the default is to assume true. + // booleanish test first, to handle docbloc attrs that have no associated value being assigned `true` values + : (isValid("boolean", functionMetadata.cbSwagger) && functionMetadata.cbSwagger) || len( functionMetadata.cbSwagger ) == 0; + } + private void function appendConventionSamples( required string type, required any methodName, diff --git a/test-harness/config/Router.cfc b/test-harness/config/Router.cfc index 2fe2d46..379b1ae 100755 --- a/test-harness/config/Router.cfc +++ b/test-harness/config/Router.cfc @@ -54,6 +54,24 @@ component{ action=defaultAPIActions ); + // Would be included in docs, but function def has @noCbSwagger attribute + addRoute( + pattern='/api/v1/noCbSwagger', + handler='api.v1.Users', + action={ + "GET": "sharedRouteDifferentHTTPMethods_get_shouldNotBeExposed", + "POST": "sharedRouteDifferentHTTPMethods_post_shouldBeExposed" + } + ); + + addRoute( + pattern='/api/v1/noCbSwagger2', + handler='api.v1.Users', + action = { + "GET": "loneRoute_get_shouldNotBeExposed" + } + ); + // @app_routes@ // Conventions-Based Routing diff --git a/test-harness/handlers/api/v1/Users.cfc b/test-harness/handlers/api/v1/Users.cfc index 164225b..3792290 100755 --- a/test-harness/handlers/api/v1/Users.cfc +++ b/test-harness/handlers/api/v1/Users.cfc @@ -78,4 +78,15 @@ component displayname="API.v1.Users" { }; } + /** + * @cbSwagger false + */ + function sharedRouteDifferentHTTPMethods_get_shouldNotBeExposed() {} + function sharedRouteDifferentHTTPMethods_post_shouldBeExposed() {} + + /** + * @cbSwagger false + */ + function loneRoute_get_shouldNotBeExposed() {} + } diff --git a/test-harness/tests/specs/RoutesParserTest.cfc b/test-harness/tests/specs/RoutesParserTest.cfc index 2c1e3cd..aea56d5 100644 --- a/test-harness/tests/specs/RoutesParserTest.cfc +++ b/test-harness/tests/specs/RoutesParserTest.cfc @@ -80,6 +80,10 @@ component expect( isJSON( APIDoc.asJSON() ) ).toBeTrue(); + expect( NormalizedDoc.paths["/api/v1/noCbSwagger"] ).toHaveKey( "post", "post function marked cbSwagger=false" ); + expect( NormalizedDoc.paths["/api/v1/noCbSwagger"] ).notToHaveKey( "get", "get function was not marked cbSwagger=false" ); + expect( NormalizedDoc.paths ).notToHaveKey( "/api/v1/noCbSwagger2", "the lone function handling this was marked cbSwagger=false" ); + variables.APIDoc = APIDoc; } ); @@ -128,17 +132,30 @@ component expect( arrayLen( CBRoutes ) ).toBeGT( 0 ); + // in cases where we are testing that some route does not end up in the docs, + // we want to ensure we DO see that the route exists, and consequently that its absence from the docs + // is due to an intentional exclusion of a route that positively exists. + var requireTheseRoutesExistWithoutDocs = {"/api/v1/noCbSwagger2": 1} + // Tests that all of our configured paths exist for ( var routePrefix in apiPrefixes ) { for ( var route in cbRoutes ) { if ( left( route.pattern, len( routePrefix ) ) == routePrefix ) { var translatedPath = swaggerUtil.translatePath( route.pattern ); if ( !len( route.moduleRouting ) ) { - expect( normalizedDoc[ "paths" ] ).toHaveKey( translatedPath ); + if ( structKeyExists(requireTheseRoutesExistWithoutDocs, translatedPath) ) { + expect( normalizedDoc[ "paths" ] ).notToHaveKey( translatedPath, "expected to have discarded this route from the docs" ); + structDelete( requireTheseRoutesExistWithoutDocs, translatedPath ) + } + else { + expect( normalizedDoc[ "paths" ] ).toHaveKey( translatedPath ); + } } } } } + + expect( requireTheseRoutesExistWithoutDocs ).toBeEmpty( "all routes expected to not have docs were seen" ); } ); it( "Tests the API Document for module introspection", function(){