From a6642296562218682360f9760c2147da6bbc81d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20LAROCHE?= Date: Thu, 9 Feb 2017 18:13:10 +0100 Subject: [PATCH] Allow sub-routes --- .../play/modules/swagger/SwaggerPlugin.scala | 121 ++++++++++-------- play-2.5/swagger-play2/build.sbt | 3 + .../test-resources/delegated.routes | 4 + .../swagger-play2/test-resources/delegation | 1 + .../test-resources/subdelegated.routes | 3 + .../test/PlayApiListingCacheSpec.scala | 66 +++++----- .../test/PlayApiScannerSpec.scala | 46 ++++--- .../test/PlayDelegatedApiScannerSpec.scala | 48 +++++++ .../test/testdata/DelegatedController.scala | 24 ++++ 9 files changed, 207 insertions(+), 109 deletions(-) create mode 100644 play-2.5/swagger-play2/test-resources/delegated.routes create mode 100644 play-2.5/swagger-play2/test-resources/delegation create mode 100644 play-2.5/swagger-play2/test-resources/subdelegated.routes create mode 100644 play-2.5/swagger-play2/test/PlayDelegatedApiScannerSpec.scala create mode 100644 play-2.5/swagger-play2/test/testdata/DelegatedController.scala diff --git a/play-2.5/swagger-play2/app/play/modules/swagger/SwaggerPlugin.scala b/play-2.5/swagger-play2/app/play/modules/swagger/SwaggerPlugin.scala index 7d2e6d9..e31b50c 100644 --- a/play-2.5/swagger-play2/app/play/modules/swagger/SwaggerPlugin.scala +++ b/play-2.5/swagger-play2/app/play/modules/swagger/SwaggerPlugin.scala @@ -1,33 +1,34 @@ /** - * Copyright 2014 Reverb Technologies, Inc. - * - * Licensed 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. - */ + * Copyright 2014 Reverb Technologies, Inc. + * + * Licensed 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 play.modules.swagger import java.io.File import javax.inject.Inject + import io.swagger.config.{FilterFactory, ScannerFactory} -import play.modules.swagger.util.SwaggerContext import io.swagger.core.filter.SwaggerSpecFilter import play.api.inject.ApplicationLifecycle -import play.api.{Logger, Application} import play.api.routing.Router -import scala.concurrent.Future -import scala.collection.JavaConversions._ -import play.routes.compiler.{Route => PlayRoute, Include => PlayInclude, RoutesFileParser, StaticPart} +import play.api.{Application, Logger} +import play.modules.swagger.util.SwaggerContext +import play.routes.compiler.{RoutesFileParser, StaticPart, Include => PlayInclude, Route => PlayRoute} +import scala.collection.JavaConversions._ +import scala.concurrent.Future import scala.io.Source trait SwaggerPlugin @@ -54,33 +55,33 @@ class SwaggerPluginImpl @Inject()(lifecycle: ApplicationLifecycle, router: Route val title = config.getString("swagger.api.info.title") match { case None => "" - case Some(value)=> value + case Some(value) => value } val description = config.getString("swagger.api.info.description") match { case None => "" - case Some(value)=> value + case Some(value) => value } val termsOfServiceUrl = config.getString("swagger.api.info.termsOfServiceUrl") match { case None => "" - case Some(value)=> value + case Some(value) => value } val contact = config.getString("swagger.api.info.contact") match { case None => "" - case Some(value)=> value + case Some(value) => value } val license = config.getString("swagger.api.info.license") match { case None => "" - case Some(value)=> value + case Some(value) => value } val licenseUrl = config.getString("swagger.api.info.licenseUrl") match { // licenceUrl needs to be a valid URL to validate against schema case None => "http://licenseUrl" - case Some(value)=> value + case Some(value) => value } SwaggerContext.registerClassLoader(app.classloader) @@ -106,44 +107,23 @@ class SwaggerPluginImpl @Inject()(lifecycle: ApplicationLifecycle, router: Route val routes = parseRoutes def parseRoutes: List[PlayRoute] = { - def playRoutesClassNameToFileName(className: String) = className.replace(".Routes", ".routes") val routesFile = config.underlying.hasPath("play.http.router") match { case false => "routes" case true => config.getString("play.http.router") match { case None => "routes" - case Some(value)=> playRoutesClassNameToFileName(value) + case Some(value) => SwaggerPluginHelper.playRoutesClassNameToFileName(value) } } - //Parses multiple route files recursively - def parseRoutesHelper(routesFile: String, prefix: String): List[PlayRoute] = { - logger.debug(s"Processing route file '$routesFile' with prefix '$prefix'") - - val routesContent = Source.fromInputStream(app.classloader.getResourceAsStream(routesFile)).mkString - val parsedRoutes = RoutesFileParser.parseContent(routesContent,new File(routesFile)) - val routes = parsedRoutes.right.get.collect { - case (route: PlayRoute) => { - logger.debug(s"Adding route '$route'") - Seq(route.copy(path = route.path.copy(parts = StaticPart(prefix) +: route.path.parts))) - } - case (include: PlayInclude) => { - logger.debug(s"Processing route include $include") - parseRoutesHelper(playRoutesClassNameToFileName(include.router), include.prefix) - } - }.flatten - logger.debug(s"Finished processing route file '$routesFile'") - routes - } - parseRoutesHelper(routesFile, "") + + SwaggerPluginHelper.parseRoutes(routesFile, "", logger.debug(_), app.classloader) } - val routesRules = Map(routes map - { route => - { - val routeName = s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" - (routeName -> route) - } - } : _*) + val routesRules = Map(routes map { route => { + val routeName = s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" + (routeName -> route) + } + }: _*) val route = new RouteWrapper(routesRules) RouteFactory.setRoute(route) @@ -174,3 +154,36 @@ class SwaggerPluginImpl @Inject()(lifecycle: ApplicationLifecycle, router: Route } } + +object SwaggerPluginHelper { + def playRoutesClassNameToFileName(className: String): String = className.replace(".Routes", ".routes") + + //Parses multiple route files recursively + def parseRoutes(routesFile: String, prefix: String, debug: String => Unit, classLoader: ClassLoader): List[PlayRoute] = { + debug(s"Processing route file '$routesFile' with prefix '$prefix'") + + val routesContent = Source.fromInputStream(classLoader.getResourceAsStream(routesFile)).mkString + val parsedRoutes = RoutesFileParser.parseContent(routesContent, new File(routesFile)) + val routes = parsedRoutes.right.get.collect { + case (route: PlayRoute) => + debug(s"Adding route '$route'") + (prefix, route.path.parts) match { + case ("", _) => Seq(route) + case (_, Seq()) => Seq(route.copy(path = route.path.copy(parts = StaticPart(prefix) +: route.path.parts))) + case (_, Seq(StaticPart(""))) => Seq(route.copy(path = route.path.copy(parts = StaticPart(prefix) +: route.path.parts))) + case (_, Seq(StaticPart("/"))) => Seq(route.copy(path = route.path.copy(parts = StaticPart(prefix) +: route.path.parts))) + case (_, _) => Seq(route.copy(path = route.path.copy(parts = StaticPart(prefix) +: StaticPart("/") +: route.path.parts))) + } + case (include: PlayInclude) => + debug(s"Processing route include $include") + val newPrefix = if(prefix == "") { + include.prefix + } else { + s"$prefix/${include.prefix}" + } + parseRoutes(playRoutesClassNameToFileName(include.router), newPrefix, debug, classLoader) + }.flatten + debug(s"Finished processing route file '$routesFile'") + routes + } +} \ No newline at end of file diff --git a/play-2.5/swagger-play2/build.sbt b/play-2.5/swagger-play2/build.sbt index 7fc0ce0..d838307 100644 --- a/play-2.5/swagger-play2/build.sbt +++ b/play-2.5/swagger-play2/build.sbt @@ -76,3 +76,6 @@ pomExtra := { } lazy val root = (project in file(".")).enablePlugins(PlayScala) + +resourceDirectory in Test := baseDirectory.value / "test-resources" +parallelExecution in Test := false \ No newline at end of file diff --git a/play-2.5/swagger-play2/test-resources/delegated.routes b/play-2.5/swagger-play2/test-resources/delegated.routes new file mode 100644 index 0000000..8c12d5d --- /dev/null +++ b/play-2.5/swagger-play2/test-resources/delegated.routes @@ -0,0 +1,4 @@ +-> /subdelegated subdelegated.Routes + +GET /my/action testdata.DelegatedController.list +GET /all testdata.DelegatedController.list2 \ No newline at end of file diff --git a/play-2.5/swagger-play2/test-resources/delegation b/play-2.5/swagger-play2/test-resources/delegation new file mode 100644 index 0000000..123f8e1 --- /dev/null +++ b/play-2.5/swagger-play2/test-resources/delegation @@ -0,0 +1 @@ +-> /api delegated.Routes \ No newline at end of file diff --git a/play-2.5/swagger-play2/test-resources/subdelegated.routes b/play-2.5/swagger-play2/test-resources/subdelegated.routes new file mode 100644 index 0000000..6d1c0b0 --- /dev/null +++ b/play-2.5/swagger-play2/test-resources/subdelegated.routes @@ -0,0 +1,3 @@ +GET /my/action testdata.DelegatedController.list3 +GET /all testdata.DelegatedController.list4 +GET / testdata.DelegatedController.list5 \ No newline at end of file diff --git a/play-2.5/swagger-play2/test/PlayApiListingCacheSpec.scala b/play-2.5/swagger-play2/test/PlayApiListingCacheSpec.scala index bf9f0a0..2c70a70 100644 --- a/play-2.5/swagger-play2/test/PlayApiListingCacheSpec.scala +++ b/play-2.5/swagger-play2/test/PlayApiListingCacheSpec.scala @@ -1,23 +1,28 @@ import java.io.File import io.swagger.config.ScannerFactory -import io.swagger.models.{ModelImpl, HttpMethod} -import io.swagger.models.parameters.{QueryParameter, BodyParameter, PathParameter} -import io.swagger.models.properties.{RefProperty, ArrayProperty} -import play.modules.swagger._ -import org.specs2.mutable._ +import io.swagger.models.parameters.{BodyParameter, PathParameter, QueryParameter} +import io.swagger.models.properties.{ArrayProperty, RefProperty} +import io.swagger.models.{HttpMethod, ModelImpl} +import io.swagger.util.Json import org.specs2.mock.Mockito +import org.specs2.mutable._ +import org.specs2.specification.BeforeAfterAll import play.api.Logger -import io.swagger.util.Json +import play.modules.swagger._ +import play.routes.compiler.{Route => PlayRoute} + import scala.collection.JavaConversions._ import scala.collection.JavaConverters._ -import play.routes.compiler.{ Route => PlayRoute } -class PlayApiListingCacheSpec extends Specification with Mockito { +class PlayApiListingCacheSpec extends Specification with Mockito with BeforeAfterAll { + + override def afterAll(): Unit = {} // set up mock for Play Router - val routesList = { - play.routes.compiler.RoutesFileParser.parseContent(""" + val routesList: List[PlayRoute] = { + play.routes.compiler.RoutesFileParser.parseContent( + """ POST /api/document/:settlementId/files/:fileId/accept testdata.DocumentController.accept(settlementId:String,fileId:String) GET /api/search testdata.SettlementsSearcherController.search(personalNumber:String,propertyId:String) GET /api/pointsofinterest testdata.PointOfInterestController.list(eastingMin:Double,northingMin:Double,eastingMax:Double,northingMax:Double) @@ -36,19 +41,17 @@ PUT /api/dog/:id testdata.DogController.add0(id:String) } } - val routesRules = Map(routesList map - { route => - { - val routeName = s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" - routeName -> route - } - } : _*) + val routesRules = Map(routesList map { route => { + val routeName = s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" + routeName -> route + } + }: _*) + val apiVersion = "test1" val basePath = "/api" - var swaggerConfig = new PlaySwaggerConfig() - + val swaggerConfig = new PlaySwaggerConfig() swaggerConfig setDescription "description" swaggerConfig setBasePath basePath swaggerConfig setContact "contact" @@ -59,12 +62,13 @@ PUT /api/dog/:id testdata.DogController.add0(id:String) swaggerConfig setLicense "license" swaggerConfig setLicenseUrl "http://licenseUrl" - PlayConfigFactory.setConfig(swaggerConfig) + override def beforeAll(): Unit = { + ApiListingCache.cache = None + PlayConfigFactory.setConfig(swaggerConfig) + ScannerFactory.setScanner(new PlayApiScanner()) + RouteFactory.setRoute(new RouteWrapper(routesRules)) + } - var scanner = new PlayApiScanner() - ScannerFactory.setScanner(scanner) - val route = new RouteWrapper(routesRules) - RouteFactory.setRoute(route) "ApiListingCache" should { @@ -73,7 +77,7 @@ PUT /api/dog/:id testdata.DogController.add0(id:String) val docRoot = "" val swagger = ApiListingCache.listing(docRoot, "127.0.0.1") - Logger.debug ("swagger: " + toJsonString(swagger)) + Logger.debug("swagger: " + toJsonString(swagger)) swagger must beSome swagger must beSome @@ -157,18 +161,18 @@ PUT /api/dog/:id testdata.DogController.add0(id:String) val opDogGet = pathDog.getOperationMap.get(HttpMethod.GET) opDogGet.getOperationId must beEqualTo("listDogs") opDogGet.getParameters must beEmpty - opDogGet.getConsumes.asScala.toList must beEqualTo(List("application/json","application/xml")) + opDogGet.getConsumes.asScala.toList must beEqualTo(List("application/json", "application/xml")) opDogGet.getResponses.get("200").getSchema.asInstanceOf[ArrayProperty].getItems.asInstanceOf[RefProperty].getSimpleRef must beEqualTo("Dog") - opDogGet.getProduces.asScala.toList must beEqualTo(List("application/json","application/xml")) + opDogGet.getProduces.asScala.toList must beEqualTo(List("application/json", "application/xml")) val opDogPut = pathDog.getOperationMap.get(HttpMethod.PUT) opDogPut.getOperationId must beEqualTo("add1") opDogPut.getParameters.head.getName must beEqualTo("dog") opDogPut.getParameters.head.getIn must beEqualTo("body") opDogPut.getParameters.head.asInstanceOf[BodyParameter].getSchema.getReference must beEqualTo("#/definitions/Dog") - opDogPut.getConsumes.asScala.toList must beEqualTo(List("application/json","application/xml")) + opDogPut.getConsumes.asScala.toList must beEqualTo(List("application/json", "application/xml")) opDogPut.getResponses.get("200").getSchema.asInstanceOf[RefProperty].getSimpleRef must beEqualTo("ActionAnyContent") - opDogPut.getProduces.asScala.toList must beEqualTo(List("application/json","application/xml")) + opDogPut.getProduces.asScala.toList must beEqualTo(List("application/json", "application/xml")) val pathDogParam = swagger.get.getPaths.get("/dog/{id}") pathDogParam.getOperations.size must beEqualTo(1) @@ -178,8 +182,8 @@ PUT /api/dog/:id testdata.DogController.add0(id:String) opDogParamPut.getParameters.head.getName must beEqualTo("id") opDogParamPut.getParameters.head.getIn must beEqualTo("path") opDogParamPut.getParameters.head.asInstanceOf[PathParameter].getType must beEqualTo("string") - opDogParamPut.getConsumes.asScala.toList must beEqualTo(List("application/json","application/xml")) - opDogParamPut.getProduces.asScala.toList must beEqualTo(List("application/json","application/xml")) + opDogParamPut.getConsumes.asScala.toList must beEqualTo(List("application/json", "application/xml")) + opDogParamPut.getProduces.asScala.toList must beEqualTo(List("application/json", "application/xml")) opDogParamPut.getResponses.get("200").getSchema.asInstanceOf[RefProperty].getSimpleRef must beEqualTo("ActionAnyContent") val catDef = swagger.get.getDefinitions.get("Cat").asInstanceOf[ModelImpl] diff --git a/play-2.5/swagger-play2/test/PlayApiScannerSpec.scala b/play-2.5/swagger-play2/test/PlayApiScannerSpec.scala index 2c35ff4..1cdc39a 100644 --- a/play-2.5/swagger-play2/test/PlayApiScannerSpec.scala +++ b/play-2.5/swagger-play2/test/PlayApiScannerSpec.scala @@ -1,17 +1,20 @@ import java.io.File -import play.modules.swagger._ -import org.specs2.mutable._ import org.specs2.mock.Mockito -import scala.collection.JavaConversions._ +import org.specs2.mutable._ +import org.specs2.specification.BeforeAfterAll +import play.modules.swagger._ import play.modules.swagger.util.SwaggerContext -import play.routes.compiler.{ Route => PlayRoute } +import play.routes.compiler.{Route => PlayRoute} + +import scala.collection.JavaConversions._ -class PlayApiScannerSpec extends Specification with Mockito { +class PlayApiScannerSpec extends Specification with Mockito with BeforeAfterAll { // set up mock for Play Router - val routesList = { - play.routes.compiler.RoutesFileParser.parseContent(""" + val routesList: List[PlayRoute] = + play.routes.compiler.RoutesFileParser.parseContent( +""" GET /api/dog testdata.DogController.list PUT /api/dog testdata.DogController.add1 GET /api/cat @testdata.CatController.list @@ -19,30 +22,25 @@ PUT /api/cat @testdata.CatController.add1 GET /api/fly testdata.FlyController.list PUT /api/dog testdata.DogController.add1 PUT /api/dog/:id testdata.DogController.add0(id:String) - """, new File("")).right.get.collect { - case (route: PlayRoute) => { - val routeName = s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" - route - } - } - } +""", new File("")).right.get.collect { case (route: PlayRoute) => route} - val routesRules = Map(routesList map - { route => - { - val routeName = s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" - routeName -> route - } - } : _*) + val routesRules = Map(routesList map { route => + s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" -> route + }: _*) + + + override def afterAll(): Unit = {} + + override def beforeAll(): Unit = { + RouteFactory.setRoute(new RouteWrapper(routesRules)) + } - val route = new RouteWrapper(routesRules) - RouteFactory.setRoute(route) "PlayApiScanner" should { "identify correct API classes based on router and API annotations" in { val classes = new PlayApiScanner().classes() - + classes.toList.length must beEqualTo(2) classes.contains(SwaggerContext.loadClass("testdata.DogController")) must beTrue classes.contains(SwaggerContext.loadClass("testdata.CatController")) must beTrue diff --git a/play-2.5/swagger-play2/test/PlayDelegatedApiScannerSpec.scala b/play-2.5/swagger-play2/test/PlayDelegatedApiScannerSpec.scala new file mode 100644 index 0000000..72acb7a --- /dev/null +++ b/play-2.5/swagger-play2/test/PlayDelegatedApiScannerSpec.scala @@ -0,0 +1,48 @@ +import io.swagger.config.ScannerFactory +import org.specs2.mock.Mockito +import org.specs2.mutable._ +import org.specs2.specification.BeforeAfterAll +import play.modules.swagger._ +import play.routes.compiler.Route + +import scala.collection.JavaConverters._ + +class PlayDelegatedApiScannerSpec extends Specification with Mockito with BeforeAfterAll { + + val routes: List[Route] = + SwaggerPluginHelper.parseRoutes("delegation", "", _ => {}, Thread.currentThread().getContextClassLoader) + + + val routesRules: Map[String, Route] = Map(routes.map { route: Route => + s"${route.call.packageName}.${route.call.controller}$$.${route.call.method}" -> route + }: _*) + + + val swaggerConfig = new PlaySwaggerConfig() + swaggerConfig.setBasePath("") + swaggerConfig.setHost("127.0.0.1") + + override def afterAll(): Unit = {} + + override def beforeAll(): Unit = { + ApiListingCache.cache = None + PlayConfigFactory.setConfig(swaggerConfig) + ScannerFactory.setScanner(new PlayApiScanner()) + RouteFactory.setRoute(new RouteWrapper(routesRules.asJava)) + } + + + "route parsing" should { + "separate delegated paths correctly" in { + + val urls = ApiListingCache.listing("", "127.0.0.1").get.getPaths.keySet().asScala + + urls must contain("/api/all") + urls must contain("/api/my/action") + urls must contain("/api/subdelegated/all") + urls must contain("/api/subdelegated/my/action") + urls must contain("/api/subdelegated") + } + } + +} diff --git a/play-2.5/swagger-play2/test/testdata/DelegatedController.scala b/play-2.5/swagger-play2/test/testdata/DelegatedController.scala new file mode 100644 index 0000000..1912d7a --- /dev/null +++ b/play-2.5/swagger-play2/test/testdata/DelegatedController.scala @@ -0,0 +1,24 @@ +package testdata + +import io.swagger.annotations.{Api, ApiOperation} +import play.api.mvc.{Action, Controller} + +@Api +object DelegatedController extends Controller { + + @ApiOperation(value = "list") + def list = Action { _ => Ok("test case")} + + @ApiOperation(value = "list2") + def list2 = Action { _ => Ok("test case")} + + @ApiOperation(value = "list3") + def list3 = Action { _ => Ok("test case")} + + @ApiOperation(value = "list4") + def list4 = Action { _ => Ok("test case")} + + @ApiOperation(value = "list5") + def list5 = Action { _ => Ok("test case")} + +}