From a3df08fac7b252be7f2116e99b048eaa26e69b82 Mon Sep 17 00:00:00 2001 From: Dan Moran Date: Sat, 4 May 2019 12:48:27 -0400 Subject: [PATCH 1/3] Add support for user-provided route tags. --- .../rho/swagger/SwaggerModelsBuilder.scala | 32 ++++++++++++++++++- .../http4s/rho/swagger/SwaggerSyntax.scala | 9 ++++++ .../org/http4s/rho/swagger/package.scala | 4 ++- .../swagger/SwaggerModelsBuilderSpec.scala | 31 ++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerModelsBuilder.scala b/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerModelsBuilder.scala index 54df684b..f33cbaa7 100644 --- a/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerModelsBuilder.scala +++ b/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerModelsBuilder.scala @@ -156,6 +156,36 @@ private[swagger] class SwaggerModelsBuilder(formats: SwaggerFormats) { linearizeStack(rr.path::Nil).flatMap(go(_, None)).headOption } + def collectTags[F[_]](rr: RhoRoute[F, _]): List[String] = { + + def go(stack: List[PathOperation], tags: List[String]): List[String] = + stack match { + case PathMatch("") :: xs => go(xs, tags) + case PathMatch(segment) :: xs => + tags match { + case Nil => go(xs, segment :: Nil) + case ts => go(xs, ts) + } + case PathCapture(id, _, _, _) :: xs => + tags match { + case Nil => go(xs, id :: Nil) + case ts => go(xs, ts) + } + case Nil | CaptureTail :: _ => + tags match { + case Nil => "/" :: Nil + case ts => ts + } + case MetaCons(_, meta) :: xs => + meta match { + case RouteTags(ts) => ts + case _ => go(xs, tags) + } + } + + linearizeStack(rr.path::Nil).flatMap(go(_, Nil)) + } + def collectSecurityScopes[F[_]](rr: RhoRoute[F, _]): List[Map[String, List[String]]] = { def go(stack: List[PathOperation]): Option[Map[String, List[String]]] = @@ -229,7 +259,7 @@ private[swagger] class SwaggerModelsBuilder(formats: SwaggerFormats) { val parameters = collectOperationParams(rr) Operation( - tags = pathStr.split("/").filterNot(_ == "").headOption.getOrElse("/") :: Nil, + tags = collectTags(rr), summary = collectSummary(rr), consumes = rr.validMedia.toList.map(_.show), produces = rr.responseEncodings.toList.map(_.show), diff --git a/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala b/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala index 4d05f358..1a937acf 100644 --- a/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala +++ b/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala @@ -15,4 +15,13 @@ trait SwaggerSyntax[F[_]] { def **[T <: HNil](builder: PathBuilder[F, T]): PathBuilder[F, T] = new PathBuilder(builder.method, PathAST.MetaCons(builder.path, RouteDesc(description))) } + + /** Add support for adding tags before a route using the @@ operator */ + implicit class ListOps(tags: List[String]) { + def @@(method: Method): PathBuilder[F, HNil] = + @@(new PathBuilder[F, HNil](method, PathEmpty)) + + def @@[T <: HNil](builder: PathBuilder[F, T]): PathBuilder[F, T] = + new PathBuilder(builder.method, PathAST.MetaCons(builder.path, RouteTags(tags))) + } } diff --git a/swagger/src/main/scala/org/http4s/rho/swagger/package.scala b/swagger/src/main/scala/org/http4s/rho/swagger/package.scala index 528c53c6..97be6138 100644 --- a/swagger/src/main/scala/org/http4s/rho/swagger/package.scala +++ b/swagger/src/main/scala/org/http4s/rho/swagger/package.scala @@ -2,7 +2,7 @@ package org.http4s.rho import fs2.Stream import org.http4s.Method -import org.http4s.rho.bits.{PathAST, SecurityScopesMetaData, TextMetaData} +import org.http4s.rho.bits.{Metadata, PathAST, SecurityScopesMetaData, TextMetaData} import org.http4s.rho.swagger.models.Model import shapeless.{HList, HNil} @@ -13,6 +13,8 @@ package object swagger { /** Metadata carrier for specific routes */ case class RouteDesc(msg: String) extends TextMetaData + case class RouteTags(tags: List[String]) extends Metadata + /** Scopes carrier for specific routes */ case class RouteSecurityScope(definitions: Map[String, List[String]]) extends SecurityScopesMetaData diff --git a/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala b/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala index 1fa755d8..110b24a2 100644 --- a/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala +++ b/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala @@ -260,6 +260,37 @@ class SwaggerModelsBuilderSpec extends Specification { sb.mkOperation("/foo", ra).summary must_== "foo".some } + "Get explicit route tags" in { + val ra = List("tag1", "tag2") @@ GET / "bar" |>> { () => "" } + + sb.mkOperation("/bar", ra).tags must_== List("tag1", "tag2") + } + + "Default to first segment for tags" in { + val ra = GET / "foo" / "bar" |>> { () => "" } + + sb.mkOperation("/foo/bar", ra).tags must_== List("foo") + } + + "Default to / as a tag for routes without segments" in { + val ra = GET |>> { () => "" } + sb.mkOperation("/", ra).tags must_== List("/") + } + + "Mix and match route descriptions and tags" in { + val ra1 = "foo" ** List("tag1", "tag2") @@ GET / "bar" |>> { () => "" } + val ra2 = List("tag1", "tag2") @@ ("foo" ** GET / "bar") |>> { () => "" } + + sb.mkOperation("/bar", ra1) must_== sb.mkOperation("/bar", ra2) + } + + "Preserve explicit tags after prepending segments" in { + val inner = List("tag1", "tag2") @@ GET / "bar" |>> { () => "" } + val ra = "foo" /: inner + + sb.mkOperation("/foo/bar", ra).tags must_== List("tag1", "tag2") + } + "Produce unique operation ids" in { val routes = Seq( "foo1" ** GET / "bar" |>> { () => "" }, From 51ee9492750112de1d741e68e54ce2f92fc820a4 Mon Sep 17 00:00:00 2001 From: Dan Moran Date: Tue, 7 May 2019 16:13:56 -0400 Subject: [PATCH 2/3] Add single-tag variant. --- .../scala/org/http4s/rho/swagger/SwaggerSyntax.scala | 12 +++++++++--- .../rho/swagger/SwaggerModelsBuilderSpec.scala | 8 ++++++++ 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala b/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala index 1a937acf..91b497b6 100644 --- a/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala +++ b/swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala @@ -7,13 +7,19 @@ import shapeless.HNil trait SwaggerSyntax[F[_]] { - /** Add support for adding documentation before a route using the ** operator */ - implicit class StrOps(description: String) { + /** Add support for adding documentation to routes using symbolic operators. */ + implicit class StrOps(doc: String) { def **(method: Method): PathBuilder[F, HNil] = **(new PathBuilder[F, HNil](method, PathEmpty)) def **[T <: HNil](builder: PathBuilder[F, T]): PathBuilder[F, T] = - new PathBuilder(builder.method, PathAST.MetaCons(builder.path, RouteDesc(description))) + new PathBuilder(builder.method, PathAST.MetaCons(builder.path, RouteDesc(doc))) + + def @@(method: Method): PathBuilder[F, HNil] = + @@(new PathBuilder[F, HNil](method, PathEmpty)) + + def @@[T <: HNil](builder: PathBuilder[F, T]): PathBuilder[F, T] = + new PathBuilder(builder.method, PathAST.MetaCons(builder.path, RouteTags(List(doc)))) } /** Add support for adding tags before a route using the @@ operator */ diff --git a/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala b/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala index 110b24a2..bdf40812 100644 --- a/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala +++ b/swagger/src/test/scala/org/http4s/rho/swagger/SwaggerModelsBuilderSpec.scala @@ -260,6 +260,11 @@ class SwaggerModelsBuilderSpec extends Specification { sb.mkOperation("/foo", ra).summary must_== "foo".some } + "Get explicit route tag" in { + val ra = "tag" @@ GET / "bar" |>> { () => "" } + sb.mkOperation("/bar", ra).tags must_== List("tag") + } + "Get explicit route tags" in { val ra = List("tag1", "tag2") @@ GET / "bar" |>> { () => "" } @@ -280,8 +285,11 @@ class SwaggerModelsBuilderSpec extends Specification { "Mix and match route descriptions and tags" in { val ra1 = "foo" ** List("tag1", "tag2") @@ GET / "bar" |>> { () => "" } val ra2 = List("tag1", "tag2") @@ ("foo" ** GET / "bar") |>> { () => "" } + val ra3 = "foo" ** "tag" @@ GET / "bar" |>> { () => "" } + val ra4 = "tag" @@ ("foo" ** GET / "bar") |>> { () => "" } sb.mkOperation("/bar", ra1) must_== sb.mkOperation("/bar", ra2) + sb.mkOperation("/bar", ra3) must_== sb.mkOperation("/bar", ra4) } "Preserve explicit tags after prepending segments" in { From 96ef9f8d1d5b4b85a6d64d0e78f0d38302eb649e Mon Sep 17 00:00:00 2001 From: Dan Moran Date: Tue, 7 May 2019 16:19:18 -0400 Subject: [PATCH 3/3] Add tagging examples. --- .../scala/com/http4s/rho/swagger/demo/MyRoutes.scala | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/src/main/scala/com/http4s/rho/swagger/demo/MyRoutes.scala b/examples/src/main/scala/com/http4s/rho/swagger/demo/MyRoutes.scala index 13a40109..b545d980 100644 --- a/examples/src/main/scala/com/http4s/rho/swagger/demo/MyRoutes.scala +++ b/examples/src/main/scala/com/http4s/rho/swagger/demo/MyRoutes.scala @@ -39,7 +39,7 @@ abstract class MyRoutes[F[+_] : Effect](swaggerSyntax: SwaggerSyntax[F])(implici GET |>> TemporaryRedirect(Uri(path = "/swagger-ui")) // We want to define this chunk of the service as abstract for reuse below - val hello = GET / "hello" + val hello = "hello" @@ GET / "hello" "Simple hello world route" ** hello |>> Ok("Hello world!") @@ -66,19 +66,23 @@ abstract class MyRoutes[F[+_] : Effect](swaggerSyntax: SwaggerSyntax[F])(implici } "Adds the cookie Foo=bar to the client" ** + "cookies" @@ GET / "addcookie" |>> { Ok("You now have a good cookie!").map(_.addCookie("Foo", "bar")) } "Sets the cookie Foo=barr to the client" ** + "cookies" @@ GET / "addbadcookie" |>> { Ok("You now have an evil cookie!").map(_.addCookie("Foo", "barr")) } "Checks the Foo cookie to make sure its 'bar'" ** + "cookies" @@ GET / "checkcookie" >>> requireCookie |>> Ok("Good job, you have the cookie!") "Clears the cookies" ** + "cookies" @@ GET / "clearcookies" |>> { req: Request[F] => val hs = req.headers.get(headers.Cookie) match { case None => Headers.empty @@ -89,12 +93,14 @@ abstract class MyRoutes[F[+_] : Effect](swaggerSyntax: SwaggerSyntax[F])(implici Ok("Deleted cookies!").map(_.withHeaders(hs)) } - "This route allows your to post stuff" ** - POST / "post" ^ EntityDecoder.text[F] |>> { body: String => + "This route allows your to post stuff" ** + List("post", "stuff") @@ + POST / "post" ^ EntityDecoder.text[F] |>> { body: String => "You posted: " + body } "This route allows your to post stuff with query parameters" ** + List("post", "stuff", "query") @@ POST / "post-query" +? param[String]("query") ^ EntityDecoder.text[F] |>> { (query: String, body: String) => s"You queried '$query' and posted: $body" }