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

Add support for user-provided route tags. #316

Merged
merged 3 commits into from
May 14, 2019
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -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!")
Expand All @@ -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
Expand All @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]]] =
Expand Down Expand Up @@ -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),
Expand Down
21 changes: 18 additions & 3 deletions swagger/src/main/scala/org/http4s/rho/swagger/SwaggerSyntax.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,27 @@ 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 */
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)))
}
}
4 changes: 3 additions & 1 deletion swagger/src/main/scala/org/http4s/rho/swagger/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,45 @@ 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" |>> { () => "" }

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") |>> { () => "" }
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 {
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" |>> { () => "" },
Expand Down