Skip to content

Commit

Permalink
fixed a sbt plugin bug / added example and documentation on how to us…
Browse files Browse the repository at this point in the history
…e the plugin fixes #99 (#106)

* fix sbt plugin to recreate swagger.json file rather than writing over it.

* added an example

* update documentation to use sbt plugin instead

* added domain package to example

* fixed swagger json test in sbt plugin

* make swagger.json available for run

* added the ability to set a different routes file in sbt plugin
  • Loading branch information
kailuowang committed Aug 25, 2016
1 parent 739b8b6 commit ba42b9d
Show file tree
Hide file tree
Showing 40 changed files with 1,092 additions and 80 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ project/plugins/project/
# Scala-IDE specific
.scala_dependencies
.worksheet

.DS_Store
.idea

*.sw*
92 changes: 36 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,63 +73,43 @@ The result swagger specs will look like:
============================
## Get Started

In short you need to create a controller that uses the library to generate the swagger spec and make it available as an endpoint.
Then you just need to have a swagger UI instance to consumer that swagger spec.
In short you need to add sbt-play-swagger plugin which generates swagger.json on package time,
then you just need to have a swagger UI instance to consumer that swagger spec.
You can find the setup in the example project as well.


#### Step 1
add Swagger API dependency to your sbt
```scala
resolvers += Resolver.jcenterRepo
For play2.5 add Swagger sbt plugin dependency to your plugins.sbt

libraryDependencies += "com.iheart" %% "play-swagger" % "0.4.0" //find the latest version in the download badge at the top
```
For play 2.4 please use a special release build with play 2.4 binary.
```scala
libraryDependencies += "com.iheart" %% "play-swagger" % "0.4.0-PLAY2.4" //find the latest version in the download badge at the top
addSbtPlugin("com.iheart" % "sbt-play-swagger" % "0.4.2")
```

#### Step 2
Play swagger is just a library that generates a swagger spec json for you.
You can do anything you want with that json object (e.g. save it to a file), but the most common usage would be serving it in an endpoint in your play app.
Here is how:
Add a controller to your Play app that serves the swagger spec

For play 2.4 please use a special release build with play 2.4 binary.
```scala
import play.api.libs.concurrent.Execution.Implicits._
import com.iheart.playSwagger.SwaggerSpecGenerator

class ApiSpecs @Inject() (cached: Cached) extends Controller {
implicit val cl = getClass.getClassLoader

// The root package of your domain classes, play-swagger will automatically generate definitions when it encounters class references in this package.
// In our case it would be "com.iheart", play-swagger supports multiple domain package names
val domainPackage = "YOUR.DOMAIN.PACKAGE"
val secondDomainPackage = "YOUR.OtherDOMAIN.PACKAGE"
private lazy val generator = SwaggerSpecGenerator(domainPackage, secondDomainPackage)

def specs = cached("swaggerDef") { //it would be beneficial to cache this endpoint as we do here, but it's not required if you don't expect much traffic.
Action.async { _ =>
Future.fromTry(generator.generate()).map(Ok(_)) //generate() can also taking in an optional arg of the route file name.
}
}
addSbtPlugin("com.iheart" % "sbt-play-swagger" % "0.4.2-PLAY2.4")

}
```

#### Step 3
add an end point to the routes file
Then enable it for your Play app - in build.sbt add `SwaggerPlugin` to the root project like
```Scala
lazy val root = (project in file(".")).enablePlugins(PlayScala, SwaggerPlugin) //enable plugin
```
###
# summary: swagger definition
# description: for swagger UI to consume
###
GET /docs/swagger.json @controllers.swagger.ApiSpecs.specs

Also in build.sht add domain package names for play-swagger to auto generate swagger definitions for domain classes mentioned in your routes
```Scala
swaggerDomainNameSpaces := Seq("models")
```

#### Step 4
This plugin adds a sbt task `swagger`, with which you can generate the `swagger.json` for testing purpose.

This plugin will generate the `swagger.json`and make it available under path `assets/swagger.json` on `sbt package` and `sbt run`.

Alternatively, you can create a controller that uses play-swagger lib to generate the json and serve it, this way you can manipulate the swagger.json at runtime. See [here](docs/AlternativeSetup.md) for details.


#### Step 2
Add a base swagger.yml (or swagger.json) to your resources (for example, conf folder in the play application). This one needs to provide all the required fields according to swagger spec.

E.g.
```yml
---
Expand All @@ -138,25 +118,22 @@ E.g.
title: "Poweramp API"
description: "Power your music"
version: "1.0.0"
host: api2.iheart.com
schemes:
- "https"
consumes:
- application/json
produces:
- application/json

```

#### Step 5a
Deploy a swagger ui and point to the swagger spec end point, or
#### Step 3a
Deploy a swagger ui and point to the swagger spec end point at 'assets/swagger.json', or

#### Step 5b
#### Step 3b
Alternatively you can use swagger-ui webjar and have you play app serving the swagger ui:

Add the following dependency
```scala
libraryDependencies += "org.webjars" % "swagger-ui" % "2.1.4"
libraryDependencies += "org.webjars" % "swagger-ui" % "2.2.0"
```

Add the following to your route file
Expand All @@ -166,9 +143,9 @@ GET /docs/swagger-ui/*file controllers.Assets.at(path:String="/public/l
```

Then you should be able to open the swagger ui at
http://localhost:9000/docs/swagger-ui/index.html?url=/docs/swagger.json

The sbt-play-swagger plugin will generate the swagger.json on `sbt run` or `sbt package`
you should be able to open the swagger ui at
http://localhost:9000/docs/swagger-ui/index.html?url=/assets/swagger.json


============================
Expand Down Expand Up @@ -207,19 +184,22 @@ POST /tracks controller.Api.createTrack()
Again, play-swagger will generate the definition for com.iheart.api.Track case class

#### How do I use a different "host" for different environment?
The library returns play JsObject, you can change however you want like
Use the [alternative setup](docs/AlternativeSetup.md). The library returns play JsObject, you can change however you want like
```scala
val spec: Try[JsObject] = ps.generate().map(_ + ("host" -> JsString(myHost)))
```


#### How to use a route file different from the default "routes"?
In build.sbt, add
```Scala
swaggerRoutesFile := "my-routes"
```

or if you took the [alternative setup](docs/AlternativeSetup.md)
```scala
SwaggerSpecGenerator(domainPackage).generate("myRoutes.routes")
```

#### Where to find more examples?
In the [tests](/src/test/scala/com/iheart/playSwagger/SwaggerSpecGeneratorSpec.scala)!



4 changes: 3 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ lazy val playSwagger = project.in(file("core"))

lazy val sbtPlaySwagger = project.in(file("sbtPlugin"))
.settings(Publish.sbtPluginSettings ++ Format.settings ++ ScriptedTesting.settings)
.settings(addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.6" % Provided))
.settings(
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.0.6" % Provided),
addSbtPlugin("com.typesafe.sbt" % "sbt-web" % "1.3.0" % Provided))
.enablePlugins(BuildInfoPlugin)
.settings(
buildInfoKeys := Seq[BuildInfoKey](name, version),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import java.nio.file.{Files, Paths, StandardOpenOption}
object SwaggerSpecRunner extends App {
implicit def cl = getClass.getClassLoader

private def fileArg = Paths.get(args.head)
private def domainNameSpaceArgs = args.tail
private def swaggerJson = SwaggerSpecGenerator(domainNameSpaceArgs: _*).generate().get.toString
val (targetFile :: routesFile :: domainNameSpaceArgs) = args.toList
private def fileArg = Paths.get(targetFile)
private def swaggerJson = SwaggerSpecGenerator(domainNameSpaceArgs: _*).generate(routesFile).get.toString

Files.write(fileArg, swaggerJson.getBytes, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
}
Files.write(fileArg, swaggerJson.getBytes, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)
}
54 changes: 54 additions & 0 deletions docs/AlternativeSetup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@

In short you need to create a controller that uses the library to generate the swagger spec and make it available as an endpoint.

#### Step 1
For play 2.5 add Swagger API dependency to your sbt
```scala
resolvers += Resolver.jcenterRepo

libraryDependencies += "com.iheart" %% "play-swagger" % "0.4.0" //find the latest version in the download badge at the top
```

For play 2.4 please use a special release build with play 2.4 binary.
```scala
libraryDependencies += "com.iheart" %% "play-swagger" % "0.4.0-PLAY2.4" //find the latest version in the download badge at the top
```

#### Step 2
Add a controller that uses Play swagger as a library to generates a swagger spec json and serves it as an endpoint.

```scala
import play.api.libs.concurrent.Execution.Implicits._
import com.iheart.playSwagger.SwaggerSpecGenerator

class ApiSpecs @Inject() (cached: Cached) extends Controller {
implicit val cl = getClass.getClassLoader

// The root package of your domain classes, play-swagger will automatically generate definitions when it encounters class references in this package.
// In our case it would be "com.iheart", play-swagger supports multiple domain package names
val domainPackage = "YOUR.DOMAIN.PACKAGE"
val secondDomainPackage = "YOUR.OtherDOMAIN.PACKAGE"
private lazy val generator = SwaggerSpecGenerator(domainPackage, secondDomainPackage)

def specs = cached("swaggerDef") { //it would be beneficial to cache this endpoint as we do here, but it's not required if you don't expect much traffic.
Action.async { _ =>
Future.fromTry(generator.generate()).map(Ok(_)) //generate() can also taking in an optional arg of the route file name.
}
}

}
```

#### Step 3
add an end point to the routes file
```
###
# summary: swagger definition
# description: for swagger UI to consume
###
GET /assets/swagger.json @controllers.swagger.ApiSpecs.specs
```


Then follow Step 2 through Step 3 in the sbt-play-swagger setup in the main README.md
8 changes: 8 additions & 0 deletions example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
logs
target
/.idea
/.idea_modules
/.classpath
/.project
/.settings
/RUNNING_PID
8 changes: 8 additions & 0 deletions example/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
This software is licensed under the Apache 2 license, quoted below.

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project 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.
1 change: 1 addition & 0 deletions example/README
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is an example Play Application using play-swagger with sbt-play-swagger plugin
33 changes: 33 additions & 0 deletions example/app/Filters.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import javax.inject._
import play.api._
import play.api.http.HttpFilters
import play.api.mvc._

import filters.ExampleFilter

/**
* This class configures filters that run on every request. This
* class is queried by Play to get a list of filters.
*
* Play will automatically use filters from any class called
* `Filters` that is placed the root package. You can load filters
* from a different class by adding a `play.http.filters` setting to
* the `application.conf` configuration file.
*
* @param env Basic environment settings for the current application.
* @param exampleFilter A demonstration filter that adds a header to
* each response.
*/
@Singleton
class Filters @Inject() (
env: Environment,
exampleFilter: ExampleFilter) extends HttpFilters {

override val filters = {
// Use the example filter if we're running development mode. If
// we're running in production or test mode then don't use any
// filters at all.
if (env.mode == Mode.Dev) Seq(exampleFilter) else Seq.empty
}

}
28 changes: 28 additions & 0 deletions example/app/Module.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import com.google.inject.AbstractModule
import java.time.Clock

import services.{ApplicationTimer, AtomicCounter, Counter}

/**
* This class is a Guice module that tells Guice how to bind several
* different types. This Guice module is created when the Play
* application starts.
* Play will automatically use any class called `Module` that is in
* the root package. You can create modules in other locations by
* adding `play.modules.enabled` settings to the `application.conf`
* configuration file.
*/
class Module extends AbstractModule {

override def configure() = {
// Use the system clock as the default implementation of Clock
bind(classOf[Clock]).toInstance(Clock.systemDefaultZone)
// Ask Guice to create an instance of ApplicationTimer when the
// application starts.
bind(classOf[ApplicationTimer]).asEagerSingleton()
// Set AtomicCounter as the implementation for Counter.
bind(classOf[Counter]).to(classOf[AtomicCounter])
}

}
43 changes: 43 additions & 0 deletions example/app/controllers/AsyncController.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package controllers

import akka.actor.ActorSystem
import javax.inject._
import models.Message
import play.api._
import play.api.libs.json.Json
import play.api.mvc._
import scala.concurrent.{ExecutionContext, Future, Promise}
import scala.concurrent.duration._

/**
* This controller creates an `Action` that demonstrates how to write
* simple asynchronous code in a controller. It uses a timer to
* asynchronously delay sending a response for 1 second.
*
* @param actorSystem We need the `ActorSystem`'s `Scheduler` to
* run code after a delay.
* @param exec We need an `ExecutionContext` to execute our
* asynchronous code.
*/
@Singleton
class AsyncController @Inject() (actorSystem: ActorSystem)(implicit exec: ExecutionContext) extends Controller {
implicit val fmt = Json.format[Message]
/**
* Create an Action that returns a plain text message after a delay
* of 1 second.
*
* The configuration in the `routes` file means that this method
* will be called when the application receives a `GET` request with
* a path of `/message`.
*/
def message = Action.async {
getFutureMessage(1.second).map { msg => Ok(Json.toJson(msg)) }
}

private def getFutureMessage(delayTime: FiniteDuration): Future[Message] = {
val promise: Promise[Message] = Promise[Message]()
actorSystem.scheduler.scheduleOnce(delayTime) { promise.success(Message("Hi!")) }
promise.future
}

}
Loading

0 comments on commit ba42b9d

Please sign in to comment.