Skip to content

Commit

Permalink
Merge branch 'master' into 0.19
Browse files Browse the repository at this point in the history
  • Loading branch information
zarthross authored Oct 15, 2018
2 parents 46d3787 + 0633809 commit 9486603
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 41 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

[![Build Status](https://travis-ci.org/http4s/rho.svg?branch=master)](https://travis-ci.org/http4s/rho)
[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.http4s/rho-core_2.12/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.http4s/rho-core_2.12)
[![Gitter](https://img.shields.io/badge/gitter-join%20chat-green.svg)](https://gitter.im/http4s/http4s?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Gitter](https://badges.gitter.im/http4s/rho.svg)](https://gitter.im/http4s/rho?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)


```scala
Expand Down
97 changes: 62 additions & 35 deletions Rho.md
Original file line number Diff line number Diff line change
@@ -1,46 +1,64 @@
# Documenting Http4s web services with rho
Rho is a library for documenting Http4s web apis.

Note: To get the generated swagger.json, you need to visit: `{https|http}://host:port/{base path if any}/swagger.json`
Rho is a library for documenting [http4s](https://http4s.org/) web APIs.

Note: To get the generated swagger.json, you need to visit: `{https|http}://host:port/{base service path if any}/swagger.json`

Sample:

```scala
val api = new RhoService[IO] {
"Description of api endpoint" **
GET / "somePath" / pathVar[Int]("someInt", "parameter description") +? paramD[String]("name", "parameter description") |>> { (someInt: Int, name: String) => {
Ok("result")
}
GET / "somePath" / pathVar[Int]("someInt", "parameter description") +? paramD[String]("name", "parameter description") |>> {
(someInt: Int, name: String) => Ok("result")
}
}
```

So we start by creating a new RhoService which will be converted to an HttpService later. This RhoService will contain our api endpoints. Every api endpoint starts (optionally) with a description for that endpoint and then the implementation. We describe the HTTP method (GET/POST/DELETE etc) and then an implementation for the url endpoint.
So we start by creating a new RhoService which will be converted to an HttpService later. This RhoService will contain our api endpoints. Every api endpoint starts (optionally, for Swagger) with a description for that endpoint and then the implementation. We describe the HTTP method (GET/POST/DELETE etc) and then an implementation for the url endpoint.

You can specify and capture path or query parameters in your path. You specify a name and description (optional) for the variables. The available types that you can capture are:
- all primitive types
- java.util.Date (yyyy-MM-dd)
- java.util.UUID
- java.time.Instant (yyyy-MM-ddThh:mm:ssZ)
- `java.util.Date` (yyyy-MM-dd)
- `java.util.UUID`
- `java.time.Instant` (yyyy-MM-ddThh:mm:ssZ)

Checkout these methods:
- `pathVar` functions for capturing path variables
- `param` functions for capturing query parameters
- `capture` functions for capturing headers

Think of `|>>` as a function that "registers" new route matcher and associated action in RhoService.
Route matcher is at lefthand-side (represented as `TypedBuilder`) and action at righthand-side is a function returning HTTP response.
The arguments for action appear exactly as they do in the URL spec (route matcher).
So in the sample above, `someInt` is first then `name` and so on. Optionally it can accept whole `org.http4s.Request` as first argument.

Think of `|>>` as a function from url specification to an implementation which returns a Response. The arguments for `|>>` appear exactly as they do in the url spec. So in the sample above, `someInt` is first then `name` and so on.
Apart from plain URL parameters and path, you can insert header captures and body decoders for your requests, an example:

You can insert header captures and body decoders for your requests, an example:
```scala
object Auth extends org.http4s.rho.AuthedContext[IO, AuthInfo]

val api = new RhoService[IO] {
"Description of api endpoint" **
POST / "somePath" / pathVar[Int]("someInt", "parameter description") >>> auth ^ jsonOf[T] |>> { (someInt: Int, au: AuthInfo, body: T) => {
Ok("result")
}
import org.http4s.rho.swagger.syntax.io._

"Description of api endpoint" ** // Description is optional and specific to Swagger
POST / "somePath" / pathVar[Int]("someInt", "parameter description") >>> Auth.auth() ^ jsonOf[IO, T] |>> {
(someInt: Int, au: AuthInfo, body: T) => Ok("result")
}
}
```
`auth` captures the authentication information. We are specifying that the body is a json object for some T.

`Auth.auth()` captures the authentication information.
In order to provide authentication information:
1. `AuthedContext` should be transformed into `HttpService`
2. `AuthMiddleware[IO, AuthInfo]` must wrap above service

```scala
val authInfoMiddleware: org.http4s.server.AuthMiddleware[IO, AuthInfo] = ??? // Standard http4s AuthMiddleware
val authApi = Auth.toService(api.toService()) // 1
val service: org.http4s.HttpService[IO] = authInfoMiddleware.apply(authApi) // 2
```

Also, in example above, we are specifying that the body is a json object for some `T`.
Anything that has a `EntityDecoder` can be specified as a body decoder. In the example above `jsonOf` is an `EntityDecoder` provided by `circe`. Included decoders in Rho are:
- binary
- binaryChunk
Expand All @@ -59,6 +77,8 @@ case class MyClass(name: String, description: String, someBool: Boolean, tags: L
//or
case class MyClass(name: String, description: String, someBool: Boolean, tags: List[String], someObj: JsonObject)

import org.http4s.rho.swagger.models._

val myClassModel: Set[Model] = Set(
ModelImpl(
id = "MyClass",
Expand Down Expand Up @@ -101,24 +121,31 @@ val myClassModel: Set[Model] = Set(
)
)

// register this model
SwaggerSupport[IO].createRhoMiddleware(
swaggerFormats = DefaultSwaggerFormats
.withSerializers(typeOf[MyClass], myClassModel)
.withSerializers(...),
apiInfo = Info(
title = "My API",
version = "1.0.0",
description = Some("functional because who hates sleep?")
),
basePath = "/v1".some,
schemes = List(Scheme.HTTPS),
security = List(SecurityRequirement("bearer", List())),
securityDefinitions = Map(
"bearer" -> ApiKeyAuthDefinition("Authorization", In.HEADER)
))
import org.http4s.rho.RhoMiddleware
import org.http4s.rho.swagger.syntax.{io => ioSwagger}

// Create a middleware that will transform RhoService into HttpService with attached Swagger definition
val swaggerMiddleware: RhoMiddleware[IO] = ioSwagger.createRhoMiddleware(
swaggerFormats = DefaultSwaggerFormats
.withSerializers(typeOf[MyClass], myClassModel)
.withSerializers(...),
apiInfo = Info(
title = "My API",
version = "1.0.0",
description = Some("functional because who hates sleep?")
),
basePath = "/v1".some,
schemes = List(Scheme.HTTPS),
security = List(SecurityRequirement("bearer", List())),
securityDefinitions = Map(
"bearer" -> ApiKeyAuthDefinition("Authorization", In.HEADER)
))

// Create http4s HttpService
val httpService = api.toService(swaggerMiddleware)
```
Tha example above also shows how to provide basic api info and base path and security specification.
By declaring security here we get option to add api key/token in the swagger ui.

The example above also shows how to provide basic api info and base path and security specification.
By declaring security here we get option to add api key/token in the Swagger UI.

To get the generated swagger.json for this example, you would visit: `https://host:port/v1/swagger.json`
4 changes: 2 additions & 2 deletions project/Dependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import Keys._

object Dependencies {
lazy val http4sVersion = "0.19.0"
lazy val specs2Version = "4.3.4"
lazy val specs2Version = "4.3.5"

lazy val http4sServer = "org.http4s" %% "http4s-server" % http4sVersion
lazy val http4sDSL = "org.http4s" %% "http4s-dsl" % http4sVersion
Expand All @@ -13,7 +13,7 @@ object Dependencies {
lazy val http4sXmlInstances = "org.http4s" %% "http4s-scala-xml" % http4sVersion
lazy val json4s = "org.json4s" %% "json4s-ext" % "3.6.1"
lazy val json4sJackson = "org.json4s" %% "json4s-jackson" % json4s.revision
lazy val swaggerModels = "io.swagger" % "swagger-models" % "1.5.18"
lazy val swaggerModels = "io.swagger" % "swagger-models" % "1.5.21"
lazy val swaggerCore = "io.swagger" % "swagger-core" % swaggerModels.revision
lazy val logbackClassic = "ch.qos.logback" % "logback-classic" % "1.2.3"
lazy val uadetector = "net.sf.uadetector" % "uadetector-resources" % "2014.10"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ private[swagger] class SwaggerModelsBuilder(formats: SwaggerFormats) {
case _ : TypeBuilder.DataType.ComplexDataType =>
tpe :: go(x::xs)
case TypeBuilder.DataType.ContainerDataType(_, Some(_: TypeBuilder.DataType.ComplexDataType), _) =>
q.m.tpe.typeArgs.head :: go(x::xs)
q.m.tpe.dealias.typeArgs.head :: go(x::xs)
case _ => go(x::xs)
}

Expand Down Expand Up @@ -347,7 +347,8 @@ private[swagger] class SwaggerModelsBuilder(formats: SwaggerFormats) {
def mkQueryParam[F[_]](rule: QueryMetaData[F, _]): Parameter = {
val required = !(rule.m.tpe.isOption || rule.default.isDefined)

TypeBuilder.DataType(rule.m.tpe) match {
val tpe = if(rule.m.tpe.isOption) rule.m.tpe.dealias.typeArgs.head else rule.m.tpe
TypeBuilder.DataType(tpe) match {
case TypeBuilder.DataType.ComplexDataType(nm, _) =>
QueryParameter(
`type` = nm.some,
Expand Down
3 changes: 2 additions & 1 deletion swagger/src/main/scala/org/http4s/rho/swagger/models.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.http4s.rho.swagger

import io.swagger.{models => jm}
import io.swagger.models.utils.PropertyModelConverter

import scala.collection.JavaConverters._
import java.util.ArrayList
Expand Down Expand Up @@ -298,7 +299,7 @@ object models {
def toJModel: jm.Response = {
val r = new jm.Response
r.setDescription(description)
r.setSchema(fromOption(schema.map(_.toJModel)))
r.setResponseSchema(fromOption(schema.map(_.toJModel).map(new PropertyModelConverter().propertyToModel)))
r.setExamples(fromMap(examples))
r.setHeaders(fromMap(headers.mapValues(_.toJModel)))
r
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class SwaggerModelsBuilderSpec extends Specification {
val sb = new SwaggerModelsBuilder(DefaultSwaggerFormats)
val fooPath = GET / "foo"
val barPath = GET / "bar"
type OpSeq = Option[Seq[String]]

"SwaggerModelsBuilder.collectQueryParams" should {

Expand Down Expand Up @@ -87,6 +88,28 @@ class SwaggerModelsBuilderSpec extends Specification {
List(QueryParameter(`type` = "string".some, name = "name".some, required = false))
}

"handle an action with one optional seq query parameter" in {
val ra = fooPath +? param[Option[Seq[String]]]("name") |>> { (s: Option[Seq[String]]) => "" }

sb.collectQueryParams[IO](ra) must_==
List(
QueryParameter(`type` = None, name = "name".some,
items = Some(AbstractProperty(`type` = "string")),
defaultValue = None, isArray = true, required = false)
)
}

"handle an action with one optional seq query parameter using a type alias" in {
val ra = fooPath +? param[OpSeq]("name") |>> { (s: OpSeq) => "" }

sb.collectQueryParams[IO](ra) must_==
List(
QueryParameter(`type` = None, name = "name".some,
items = Some(AbstractProperty(`type` = "string")),
defaultValue = None, isArray = true, required = false)
)
}

"handle an action with one query parameter with default value" in {
val ra = fooPath +? param[Int]("id", 6) |>> { (i: Int) => "" }

Expand Down

0 comments on commit 9486603

Please sign in to comment.