├── .github
└── settings.yml
├── .gitignore
├── .mergify.yml
├── .travis.yml
├── LICENSE
├── NOTICE
├── README.md
├── app
├── ErrorHandler.scala
├── Module.scala
├── RequestHandler.scala
└── v1
│ └── post
│ ├── PostActionBuilder.scala
│ ├── PostController.scala
│ ├── PostRepository.scala
│ ├── PostResourceHandler.scala
│ └── PostRouter.scala
├── build.gradle
├── build.sbt
├── conf
├── application.conf
├── generated.keystore
├── logback.xml
├── routes
└── secure.conf
├── docs
├── build.sbt
└── src
│ └── main
│ └── paradox
│ ├── appendix.md
│ ├── index.md
│ └── part-1
│ └── index.md
├── gatling
└── simulation
│ └── GatlingSpec.scala
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── project
├── Common.scala
├── build.properties
└── plugins.sbt
├── scripts
├── test-gradle
├── test-sbt
└── validate-gatling
└── test
└── controllers
└── PostRouterSpec.scala
/.github/settings.yml:
--------------------------------------------------------------------------------
1 | # These settings are synced to GitHub by https://probot.github.io/apps/settings/
2 | repository:
3 | homepage: "https://developer.lightbend.com/start/?group=play"
4 | topics: playframework, example, example-project, sample, sample-app, jvm, webapp
5 | private: false
6 | has_issues: true
7 | # We don't need projects in sample projects
8 | has_projects: false
9 | # We don't need wiki in sample projects
10 | has_wiki: false
11 | has_downloads: true
12 | default_branch: 2.7.x
13 | allow_squash_merge: true
14 | allow_merge_commit: false
15 | allow_rebase_merge: false
16 |
17 | teams:
18 | - name: core
19 | permission: admin
20 | - name: integrators
21 | permission: write
22 | - name: write-bots
23 | permission: write
24 |
25 | branches:
26 | - name: "[0-9].*.x"
27 | protection:
28 | # We don't require reviews for sample applications because they are mainly
29 | # updated by template-control, which is an automated process
30 | required_pull_request_reviews: null
31 | # Required. Require status checks to pass before merging. Set to null to disable
32 | required_status_checks:
33 | # Required. The list of status checks to require in order to merge into this branch
34 | contexts: ["Travis CI - Pull Request", "typesafe-cla-validator"]
35 |
36 | # Labels: tailored list of labels to be used by sample applications
37 | labels:
38 | - color: f9d0c4
39 | name: "closed:declined"
40 | - color: f9d0c4
41 | name: "closed:duplicated"
42 | oldname: duplicate
43 | - color: f9d0c4
44 | name: "closed:invalid"
45 | oldname: invalid
46 | - color: f9d0c4
47 | name: "closed:question"
48 | oldname: question
49 | - color: f9d0c4
50 | name: "closed:wontfix"
51 | oldname: wontfix
52 | - color: 7057ff
53 | name: "good first issue"
54 | - color: 7057ff
55 | name: "Hacktoberfest"
56 | - color: 7057ff
57 | name: "help wanted"
58 | - color: cceecc
59 | name: "status:backlog"
60 | oldname: backlog
61 | - color: b60205
62 | name: "status:block-merge"
63 | oldname: block-merge
64 | - color: b60205
65 | name: "status:blocked"
66 | - color: 0e8a16
67 | name: "status:in-progress"
68 | - color: 0e8a16
69 | name: "status:merge-when-green"
70 | oldname: merge-when-green
71 | - color: fbca04
72 | name: "status:needs-backport"
73 | - color: fbca04
74 | name: "status:needs-forwardport"
75 | - color: fbca04
76 | name: "status:needs-info"
77 | - color: fbca04
78 | name: "status:needs-verification"
79 | - color: 0e8a16
80 | name: "status:ready"
81 | - color: fbca04
82 | name: "status:to-review"
83 | oldname: review
84 | - color: c5def5
85 | name: "topic:build/tests"
86 | - color: c5def5
87 | name: "topic:dev-environment"
88 | - color: c5def5
89 | name: "topic:documentation"
90 | - color: c5def5
91 | name: "topic:jdk-next"
92 | - color: b60205
93 | name: "type:defect"
94 | oldname: bug
95 | - color: 0052cc
96 | name: "type:feature"
97 | - color: 0052cc
98 | name: "type:improvement"
99 | oldname: enhancement
100 | - color: 0052cc
101 | name: "type:updates"
102 | - color: bf0d92
103 | name: "type:template-control"
104 | oldname: template-control
105 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | logs
3 | target
4 | /.idea
5 | /.idea_modules
6 | /.classpath
7 | /.gradle
8 | /.project
9 | /.settings
10 | /RUNNING_PID
11 |
12 | /.vscode
13 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: Merge PRs that are ready
3 | conditions:
4 | - status-success=Travis CI - Pull Request
5 | - status-success=typesafe-cla-validator
6 | - "#approved-reviews-by>=1"
7 | - "#review-requested=0"
8 | - "#changes-requested-reviews-by=0"
9 | - label!=status:block-merge
10 | actions:
11 | merge:
12 | method: squash
13 | strict: smart
14 |
15 | - name: Merge TemplateControl's PRs that are ready
16 | conditions:
17 | - status-success=Travis CI - Pull Request
18 | - "#review-requested=0"
19 | - "#changes-requested-reviews-by=0"
20 | - label!=status:block-merge
21 | - label=status:merge-when-green
22 | - label!=status:block-merge
23 | actions:
24 | merge:
25 | method: squash
26 | strict: smart
27 |
28 | - name: Delete the PR branch after merge
29 | conditions:
30 | - merged
31 | actions:
32 | delete_head_branch: {}
33 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: scala
2 | scala: 2.12.8
3 | script: $SCRIPT
4 |
5 | env:
6 | matrix:
7 | - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.8.202-08
8 | - SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-2
9 | - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.202-08
10 | - SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-2
11 |
12 | matrix:
13 | fast_finish: true
14 | allow_failures:
15 | - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.8.202-08 # current gradle doesn't support play 2.7
16 | - env: SCRIPT=scripts/test-gradle TRAVIS_JDK=adopt@1.11.0-2 # current gradle doesn't support play 2.7
17 | - env: SCRIPT=scripts/test-sbt TRAVIS_JDK=adopt@1.11.0-2 # not fully supported but allows problem discovery
18 |
19 | before_install: curl -Ls https://git.io/jabba | bash && . ~/.jabba/jabba.sh
20 | install: jabba install "$TRAVIS_JDK" && jabba use "$_" && java -Xmx32m -version
21 |
22 | cache:
23 | directories:
24 | - "$HOME/.gradle/caches"
25 | - "$HOME/.ivy2/cache"
26 | - "$HOME/.jabba/jdk"
27 | - "$HOME/.sbt"
28 |
29 | before_cache:
30 | - find $HOME/.ivy2 -name "ivydata-*.properties" -delete
31 | - find $HOME/.sbt -name "*.lock" -delete
32 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | CC0 1.0 Universal
2 |
3 | Statement of Purpose
4 |
5 | The laws of most jurisdictions throughout the world automatically confer
6 | exclusive Copyright and Related Rights (defined below) upon the creator and
7 | subsequent owner(s) (each and all, an "owner") of an original work of
8 | authorship and/or a database (each, a "Work").
9 |
10 | Certain owners wish to permanently relinquish those rights to a Work for the
11 | purpose of contributing to a commons of creative, cultural and scientific
12 | works ("Commons") that the public can reliably and without fear of later
13 | claims of infringement build upon, modify, incorporate in other works, reuse
14 | and redistribute as freely as possible in any form whatsoever and for any
15 | purposes, including without limitation commercial purposes. These owners may
16 | contribute to the Commons to promote the ideal of a free culture and the
17 | further production of creative, cultural and scientific works, or to gain
18 | reputation or greater distribution for their Work in part through the use and
19 | efforts of others.
20 |
21 | For these and/or other purposes and motivations, and without any expectation
22 | of additional consideration or compensation, the person associating CC0 with a
23 | Work (the "Affirmer"), to the extent that he or she is an owner of Copyright
24 | and Related Rights in the Work, voluntarily elects to apply CC0 to the Work
25 | and publicly distribute the Work under its terms, with knowledge of his or her
26 | Copyright and Related Rights in the Work and the meaning and intended legal
27 | effect of CC0 on those rights.
28 |
29 | 1. Copyright and Related Rights. A Work made available under CC0 may be
30 | protected by copyright and related or neighboring rights ("Copyright and
31 | Related Rights"). Copyright and Related Rights include, but are not limited
32 | to, the following:
33 |
34 | i. the right to reproduce, adapt, distribute, perform, display, communicate,
35 | and translate a Work;
36 |
37 | ii. moral rights retained by the original author(s) and/or performer(s);
38 |
39 | iii. publicity and privacy rights pertaining to a person's image or likeness
40 | depicted in a Work;
41 |
42 | iv. rights protecting against unfair competition in regards to a Work,
43 | subject to the limitations in paragraph 4(a), below;
44 |
45 | v. rights protecting the extraction, dissemination, use and reuse of data in
46 | a Work;
47 |
48 | vi. database rights (such as those arising under Directive 96/9/EC of the
49 | European Parliament and of the Council of 11 March 1996 on the legal
50 | protection of databases, and under any national implementation thereof,
51 | including any amended or successor version of such directive); and
52 |
53 | vii. other similar, equivalent or corresponding rights throughout the world
54 | based on applicable law or treaty, and any national implementations thereof.
55 |
56 | 2. Waiver. To the greatest extent permitted by, but not in contravention of,
57 | applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and
58 | unconditionally waives, abandons, and surrenders all of Affirmer's Copyright
59 | and Related Rights and associated claims and causes of action, whether now
60 | known or unknown (including existing as well as future claims and causes of
61 | action), in the Work (i) in all territories worldwide, (ii) for the maximum
62 | duration provided by applicable law or treaty (including future time
63 | extensions), (iii) in any current or future medium and for any number of
64 | copies, and (iv) for any purpose whatsoever, including without limitation
65 | commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes
66 | the Waiver for the benefit of each member of the public at large and to the
67 | detriment of Affirmer's heirs and successors, fully intending that such Waiver
68 | shall not be subject to revocation, rescission, cancellation, termination, or
69 | any other legal or equitable action to disrupt the quiet enjoyment of the Work
70 | by the public as contemplated by Affirmer's express Statement of Purpose.
71 |
72 | 3. Public License Fallback. Should any part of the Waiver for any reason be
73 | judged legally invalid or ineffective under applicable law, then the Waiver
74 | shall be preserved to the maximum extent permitted taking into account
75 | Affirmer's express Statement of Purpose. In addition, to the extent the Waiver
76 | is so judged Affirmer hereby grants to each affected person a royalty-free,
77 | non transferable, non sublicensable, non exclusive, irrevocable and
78 | unconditional license to exercise Affirmer's Copyright and Related Rights in
79 | the Work (i) in all territories worldwide, (ii) for the maximum duration
80 | provided by applicable law or treaty (including future time extensions), (iii)
81 | in any current or future medium and for any number of copies, and (iv) for any
82 | purpose whatsoever, including without limitation commercial, advertising or
83 | promotional purposes (the "License"). The License shall be deemed effective as
84 | of the date CC0 was applied by Affirmer to the Work. Should any part of the
85 | License for any reason be judged legally invalid or ineffective under
86 | applicable law, such partial invalidity or ineffectiveness shall not
87 | invalidate the remainder of the License, and in such case Affirmer hereby
88 | affirms that he or she will not (i) exercise any of his or her remaining
89 | Copyright and Related Rights in the Work or (ii) assert any associated claims
90 | and causes of action with respect to the Work, in either case contrary to
91 | Affirmer's express Statement of Purpose.
92 |
93 | 4. Limitations and Disclaimers.
94 |
95 | a. No trademark or patent rights held by Affirmer are waived, abandoned,
96 | surrendered, licensed or otherwise affected by this document.
97 |
98 | b. Affirmer offers the Work as-is and makes no representations or warranties
99 | of any kind concerning the Work, express, implied, statutory or otherwise,
100 | including without limitation warranties of title, merchantability, fitness
101 | for a particular purpose, non infringement, or the absence of latent or
102 | other defects, accuracy, or the present or absence of errors, whether or not
103 | discoverable, all to the greatest extent permissible under applicable law.
104 |
105 | c. Affirmer disclaims responsibility for clearing rights of other persons
106 | that may apply to the Work or any use thereof, including without limitation
107 | any person's Copyright and Related Rights in the Work. Further, Affirmer
108 | disclaims responsibility for obtaining any necessary consents, permissions
109 | or other rights required for any use of the Work.
110 |
111 | d. Affirmer understands and acknowledges that Creative Commons is not a
112 | party to this document and has no duty or obligation with respect to this
113 | CC0 or use of the Work.
114 |
115 | For more information, please see
116 |
117 |
--------------------------------------------------------------------------------
/NOTICE:
--------------------------------------------------------------------------------
1 | Written by Lightbend
2 |
3 | To the extent possible under law, the author(s) have dedicated all copyright and
4 | related and neighboring rights to this software to the public domain worldwide.
5 | This software is distributed without any warranty.
6 |
7 | You should have received a copy of the CC0 Public Domain Dedication along with
8 | this software. If not, see .
9 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | MOVED TO https://github.com/playframework/play-samples
2 |
--------------------------------------------------------------------------------
/app/ErrorHandler.scala:
--------------------------------------------------------------------------------
1 | import javax.inject.{Inject, Provider}
2 |
3 | import play.api._
4 | import play.api.http.DefaultHttpErrorHandler
5 | import play.api.http.Status._
6 | import play.api.libs.json.Json
7 | import play.api.mvc.Results._
8 | import play.api.mvc._
9 | import play.api.routing.Router
10 | import play.core.SourceMapper
11 |
12 | import scala.concurrent._
13 |
14 | /**
15 | * Provides a stripped down error handler that does not use HTML in error pages, and
16 | * prints out debugging output.
17 | *
18 | * https://www.playframework.com/documentation/latest/ScalaErrorHandling
19 | */
20 | class ErrorHandler(environment: Environment,
21 | configuration: Configuration,
22 | sourceMapper: Option[SourceMapper] = None,
23 | optionRouter: => Option[Router] = None)
24 | extends DefaultHttpErrorHandler(environment,
25 | configuration,
26 | sourceMapper,
27 | optionRouter) {
28 |
29 | private val logger =
30 | org.slf4j.LoggerFactory.getLogger("application.ErrorHandler")
31 |
32 | // This maps through Guice so that the above constructor can call methods.
33 | @Inject
34 | def this(environment: Environment,
35 | configuration: Configuration,
36 | sourceMapper: OptionalSourceMapper,
37 | router: Provider[Router]) = {
38 | this(environment,
39 | configuration,
40 | sourceMapper.sourceMapper,
41 | Some(router.get))
42 | }
43 |
44 | override def onClientError(request: RequestHeader,
45 | statusCode: Int,
46 | message: String): Future[Result] = {
47 | logger.debug(
48 | s"onClientError: statusCode = $statusCode, uri = ${request.uri}, message = $message")
49 |
50 | Future.successful {
51 | val result = statusCode match {
52 | case BAD_REQUEST =>
53 | Results.BadRequest(message)
54 | case FORBIDDEN =>
55 | Results.Forbidden(message)
56 | case NOT_FOUND =>
57 | Results.NotFound(message)
58 | case clientError if statusCode >= 400 && statusCode < 500 =>
59 | Results.Status(statusCode)
60 | case nonClientError =>
61 | val msg =
62 | s"onClientError invoked with non client error status code $statusCode: $message"
63 | throw new IllegalArgumentException(msg)
64 | }
65 | result
66 | }
67 | }
68 |
69 | override protected def onDevServerError(
70 | request: RequestHeader,
71 | exception: UsefulException): Future[Result] = {
72 | Future.successful(
73 | InternalServerError(Json.obj("exception" -> exception.toString)))
74 | }
75 |
76 | override protected def onProdServerError(
77 | request: RequestHeader,
78 | exception: UsefulException): Future[Result] = {
79 | Future.successful(InternalServerError)
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/app/Module.scala:
--------------------------------------------------------------------------------
1 | import javax.inject._
2 |
3 | import com.google.inject.AbstractModule
4 | import net.codingwell.scalaguice.ScalaModule
5 | import play.api.{Configuration, Environment}
6 | import v1.post._
7 |
8 | /**
9 | * Sets up custom components for Play.
10 | *
11 | * https://www.playframework.com/documentation/latest/ScalaDependencyInjection
12 | */
13 | class Module(environment: Environment, configuration: Configuration)
14 | extends AbstractModule
15 | with ScalaModule {
16 |
17 | override def configure() = {
18 | bind[PostRepository].to[PostRepositoryImpl].in[Singleton]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/RequestHandler.scala:
--------------------------------------------------------------------------------
1 | import javax.inject.Inject
2 | import play.api.OptionalDevContext
3 | import play.api.http._
4 | import play.api.mvc._
5 | import play.api.mvc.request.RequestTarget
6 | import play.api.routing.Router
7 | import play.core.WebCommands
8 |
9 | /**
10 | * Handles all requests.
11 | *
12 | * https://www.playframework.com/documentation/2.5.x/ScalaHttpRequestHandlers#extending-the-default-request-handler
13 | */
14 | class RequestHandler @Inject()(webCommands: WebCommands,
15 | optDevContext: OptionalDevContext,
16 | router: Router,
17 | errorHandler: HttpErrorHandler,
18 | configuration: HttpConfiguration,
19 | filters: HttpFilters)
20 | extends DefaultHttpRequestHandler(webCommands,
21 | optDevContext,
22 | router,
23 | errorHandler,
24 | configuration,
25 | filters) {
26 |
27 | override def handlerForRequest(
28 | request: RequestHeader): (RequestHeader, Handler) = {
29 | super.handlerForRequest {
30 | // ensures that REST API does not need a trailing "/"
31 | if (isREST(request)) {
32 | addTrailingSlash(request)
33 | } else {
34 | request
35 | }
36 | }
37 | }
38 |
39 | private def isREST(request: RequestHeader) = {
40 | request.uri match {
41 | case uri: String if uri.contains("post") => true
42 | case _ => false
43 | }
44 | }
45 |
46 | private def addTrailingSlash(origReq: RequestHeader): RequestHeader = {
47 | if (!origReq.path.endsWith("/")) {
48 | val path = origReq.path + "/"
49 | if (origReq.rawQueryString.isEmpty) {
50 | origReq.withTarget(
51 | RequestTarget(path = path, uriString = path, queryString = Map())
52 | )
53 | } else {
54 | origReq.withTarget(
55 | RequestTarget(path = path,
56 | uriString = origReq.uri,
57 | queryString = origReq.queryString)
58 | )
59 | }
60 | } else {
61 | origReq
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/app/v1/post/PostActionBuilder.scala:
--------------------------------------------------------------------------------
1 | package v1.post
2 |
3 | import javax.inject.Inject
4 |
5 | import net.logstash.logback.marker.LogstashMarker
6 | import play.api.{Logger, MarkerContext}
7 | import play.api.http.{FileMimeTypes, HttpVerbs}
8 | import play.api.i18n.{Langs, MessagesApi}
9 | import play.api.mvc._
10 |
11 | import scala.concurrent.{ExecutionContext, Future}
12 |
13 | /**
14 | * A wrapped request for post resources.
15 | *
16 | * This is commonly used to hold request-specific information like
17 | * security credentials, and useful shortcut methods.
18 | */
19 | trait PostRequestHeader
20 | extends MessagesRequestHeader
21 | with PreferredMessagesProvider
22 | class PostRequest[A](request: Request[A], val messagesApi: MessagesApi)
23 | extends WrappedRequest(request)
24 | with PostRequestHeader
25 |
26 | /**
27 | * Provides an implicit marker that will show the request in all logger statements.
28 | */
29 | trait RequestMarkerContext {
30 | import net.logstash.logback.marker.Markers
31 |
32 | private def marker(tuple: (String, Any)) = Markers.append(tuple._1, tuple._2)
33 |
34 | private implicit class RichLogstashMarker(marker1: LogstashMarker) {
35 | def &&(marker2: LogstashMarker): LogstashMarker = marker1.and(marker2)
36 | }
37 |
38 | implicit def requestHeaderToMarkerContext(
39 | implicit request: RequestHeader): MarkerContext = {
40 | MarkerContext {
41 | marker("id" -> request.id) && marker("host" -> request.host) && marker(
42 | "remoteAddress" -> request.remoteAddress)
43 | }
44 | }
45 |
46 | }
47 |
48 | /**
49 | * The action builder for the Post resource.
50 | *
51 | * This is the place to put logging, metrics, to augment
52 | * the request with contextual data, and manipulate the
53 | * result.
54 | */
55 | class PostActionBuilder @Inject()(messagesApi: MessagesApi,
56 | playBodyParsers: PlayBodyParsers)(
57 | implicit val executionContext: ExecutionContext)
58 | extends ActionBuilder[PostRequest, AnyContent]
59 | with RequestMarkerContext
60 | with HttpVerbs {
61 |
62 | override val parser: BodyParser[AnyContent] = playBodyParsers.anyContent
63 |
64 | type PostRequestBlock[A] = PostRequest[A] => Future[Result]
65 |
66 | private val logger = Logger(this.getClass)
67 |
68 | override def invokeBlock[A](request: Request[A],
69 | block: PostRequestBlock[A]): Future[Result] = {
70 | // Convert to marker context and use request in block
71 | implicit val markerContext: MarkerContext = requestHeaderToMarkerContext(
72 | request)
73 | logger.trace(s"invokeBlock: ")
74 |
75 | val future = block(new PostRequest(request, messagesApi))
76 |
77 | future.map { result =>
78 | request.method match {
79 | case GET | HEAD =>
80 | result.withHeaders("Cache-Control" -> s"max-age: 100")
81 | case other =>
82 | result
83 | }
84 | }
85 | }
86 | }
87 |
88 | /**
89 | * Packages up the component dependencies for the post controller.
90 | *
91 | * This is a good way to minimize the surface area exposed to the controller, so the
92 | * controller only has to have one thing injected.
93 | */
94 | case class PostControllerComponents @Inject()(
95 | postActionBuilder: PostActionBuilder,
96 | postResourceHandler: PostResourceHandler,
97 | actionBuilder: DefaultActionBuilder,
98 | parsers: PlayBodyParsers,
99 | messagesApi: MessagesApi,
100 | langs: Langs,
101 | fileMimeTypes: FileMimeTypes,
102 | executionContext: scala.concurrent.ExecutionContext)
103 | extends ControllerComponents
104 |
105 | /**
106 | * Exposes actions and handler to the PostController by wiring the injected state into the base class.
107 | */
108 | class PostBaseController @Inject()(pcc: PostControllerComponents)
109 | extends BaseController
110 | with RequestMarkerContext {
111 | override protected def controllerComponents: ControllerComponents = pcc
112 |
113 | def PostAction: PostActionBuilder = pcc.postActionBuilder
114 |
115 | def postResourceHandler: PostResourceHandler = pcc.postResourceHandler
116 | }
117 |
--------------------------------------------------------------------------------
/app/v1/post/PostController.scala:
--------------------------------------------------------------------------------
1 | package v1.post
2 |
3 | import javax.inject.Inject
4 |
5 | import play.api.Logger
6 | import play.api.data.Form
7 | import play.api.libs.json.Json
8 | import play.api.mvc._
9 |
10 | import scala.concurrent.{ExecutionContext, Future}
11 |
12 | case class PostFormInput(title: String, body: String)
13 |
14 | /**
15 | * Takes HTTP requests and produces JSON.
16 | */
17 | class PostController @Inject()(cc: PostControllerComponents)(
18 | implicit ec: ExecutionContext)
19 | extends PostBaseController(cc) {
20 |
21 | private val logger = Logger(getClass)
22 |
23 | private val form: Form[PostFormInput] = {
24 | import play.api.data.Forms._
25 |
26 | Form(
27 | mapping(
28 | "title" -> nonEmptyText,
29 | "body" -> text
30 | )(PostFormInput.apply)(PostFormInput.unapply)
31 | )
32 | }
33 |
34 | def index: Action[AnyContent] = PostAction.async { implicit request =>
35 | logger.trace("index: ")
36 | postResourceHandler.find.map { posts =>
37 | Ok(Json.toJson(posts))
38 | }
39 | }
40 |
41 | def process: Action[AnyContent] = PostAction.async { implicit request =>
42 | logger.trace("process: ")
43 | processJsonPost()
44 | }
45 |
46 | def show(id: String): Action[AnyContent] = PostAction.async {
47 | implicit request =>
48 | logger.trace(s"show: id = $id")
49 | postResourceHandler.lookup(id).map { post =>
50 | Ok(Json.toJson(post))
51 | }
52 | }
53 |
54 | private def processJsonPost[A]()(
55 | implicit request: PostRequest[A]): Future[Result] = {
56 | def failure(badForm: Form[PostFormInput]) = {
57 | Future.successful(BadRequest(badForm.errorsAsJson))
58 | }
59 |
60 | def success(input: PostFormInput) = {
61 | postResourceHandler.create(input).map { post =>
62 | Created(Json.toJson(post)).withHeaders(LOCATION -> post.link)
63 | }
64 | }
65 |
66 | form.bindFromRequest().fold(failure, success)
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/app/v1/post/PostRepository.scala:
--------------------------------------------------------------------------------
1 | package v1.post
2 |
3 | import javax.inject.{Inject, Singleton}
4 |
5 | import akka.actor.ActorSystem
6 | import play.api.libs.concurrent.CustomExecutionContext
7 | import play.api.{Logger, MarkerContext}
8 |
9 | import scala.concurrent.Future
10 |
11 | final case class PostData(id: PostId, title: String, body: String)
12 |
13 | class PostId private (val underlying: Int) extends AnyVal {
14 | override def toString: String = underlying.toString
15 | }
16 |
17 | object PostId {
18 | def apply(raw: String): PostId = {
19 | require(raw != null)
20 | new PostId(Integer.parseInt(raw))
21 | }
22 | }
23 |
24 | class PostExecutionContext @Inject()(actorSystem: ActorSystem)
25 | extends CustomExecutionContext(actorSystem, "repository.dispatcher")
26 |
27 | /**
28 | * A pure non-blocking interface for the PostRepository.
29 | */
30 | trait PostRepository {
31 | def create(data: PostData)(implicit mc: MarkerContext): Future[PostId]
32 |
33 | def list()(implicit mc: MarkerContext): Future[Iterable[PostData]]
34 |
35 | def get(id: PostId)(implicit mc: MarkerContext): Future[Option[PostData]]
36 | }
37 |
38 | /**
39 | * A trivial implementation for the Post Repository.
40 | *
41 | * A custom execution context is used here to establish that blocking operations should be
42 | * executed in a different thread than Play's ExecutionContext, which is used for CPU bound tasks
43 | * such as rendering.
44 | */
45 | @Singleton
46 | class PostRepositoryImpl @Inject()()(implicit ec: PostExecutionContext)
47 | extends PostRepository {
48 |
49 | private val logger = Logger(this.getClass)
50 |
51 | private val postList = List(
52 | PostData(PostId("1"), "title 1", "blog post 1"),
53 | PostData(PostId("2"), "title 2", "blog post 2"),
54 | PostData(PostId("3"), "title 3", "blog post 3"),
55 | PostData(PostId("4"), "title 4", "blog post 4"),
56 | PostData(PostId("5"), "title 5", "blog post 5")
57 | )
58 |
59 | override def list()(
60 | implicit mc: MarkerContext): Future[Iterable[PostData]] = {
61 | Future {
62 | logger.trace(s"list: ")
63 | postList
64 | }
65 | }
66 |
67 | override def get(id: PostId)(
68 | implicit mc: MarkerContext): Future[Option[PostData]] = {
69 | Future {
70 | logger.trace(s"get: id = $id")
71 | postList.find(post => post.id == id)
72 | }
73 | }
74 |
75 | def create(data: PostData)(implicit mc: MarkerContext): Future[PostId] = {
76 | Future {
77 | logger.trace(s"create: data = $data")
78 | data.id
79 | }
80 | }
81 |
82 | }
83 |
--------------------------------------------------------------------------------
/app/v1/post/PostResourceHandler.scala:
--------------------------------------------------------------------------------
1 | package v1.post
2 |
3 | import javax.inject.{Inject, Provider}
4 |
5 | import play.api.MarkerContext
6 |
7 | import scala.concurrent.{ExecutionContext, Future}
8 | import play.api.libs.json._
9 |
10 | /**
11 | * DTO for displaying post information.
12 | */
13 | case class PostResource(id: String, link: String, title: String, body: String)
14 |
15 | object PostResource {
16 | /**
17 | * Mapping to read/write a PostResource out as a JSON value.
18 | */
19 | implicit val format: Format[PostResource] = Json.format
20 | }
21 |
22 |
23 | /**
24 | * Controls access to the backend data, returning [[PostResource]]
25 | */
26 | class PostResourceHandler @Inject()(
27 | routerProvider: Provider[PostRouter],
28 | postRepository: PostRepository)(implicit ec: ExecutionContext) {
29 |
30 | def create(postInput: PostFormInput)(
31 | implicit mc: MarkerContext): Future[PostResource] = {
32 | val data = PostData(PostId("999"), postInput.title, postInput.body)
33 | // We don't actually create the post, so return what we have
34 | postRepository.create(data).map { id =>
35 | createPostResource(data)
36 | }
37 | }
38 |
39 | def lookup(id: String)(
40 | implicit mc: MarkerContext): Future[Option[PostResource]] = {
41 | val postFuture = postRepository.get(PostId(id))
42 | postFuture.map { maybePostData =>
43 | maybePostData.map { postData =>
44 | createPostResource(postData)
45 | }
46 | }
47 | }
48 |
49 | def find(implicit mc: MarkerContext): Future[Iterable[PostResource]] = {
50 | postRepository.list().map { postDataList =>
51 | postDataList.map(postData => createPostResource(postData))
52 | }
53 | }
54 |
55 | private def createPostResource(p: PostData): PostResource = {
56 | PostResource(p.id.toString, routerProvider.get.link(p.id), p.title, p.body)
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/app/v1/post/PostRouter.scala:
--------------------------------------------------------------------------------
1 | package v1.post
2 |
3 | import javax.inject.Inject
4 |
5 | import play.api.routing.Router.Routes
6 | import play.api.routing.SimpleRouter
7 | import play.api.routing.sird._
8 |
9 | /**
10 | * Routes and URLs to the PostResource controller.
11 | */
12 | class PostRouter @Inject()(controller: PostController) extends SimpleRouter {
13 | val prefix = "/v1/posts"
14 |
15 | def link(id: PostId): String = {
16 | import com.netaporter.uri.dsl._
17 | val url = prefix / id.toString
18 | url.toString()
19 | }
20 |
21 | override def routes: Routes = {
22 | case GET(p"/") =>
23 | controller.index
24 |
25 | case POST(p"/") =>
26 | controller.process
27 |
28 | case GET(p"/$id") =>
29 | controller.show(id)
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'play'
3 | id 'idea'
4 | id "com.github.lkishalmi.gatling" version "0.7.1"
5 | }
6 |
7 | def playVersion = "2.7.0"
8 | def scalaVersion = System.getProperty("scala.binary.version", /* default = */ "2.12")
9 | def gatlingVersion = "3.0.0.1"
10 |
11 | model {
12 | components {
13 | play {
14 | platform play: playVersion, scala: scalaVersion, java: '1.8'
15 | injectedRoutesGenerator = true
16 |
17 | sources {
18 | twirlTemplates {
19 | defaultImports = TwirlImports.SCALA
20 | }
21 | }
22 | }
23 | }
24 | }
25 |
26 | project.sourceSets {
27 | gatling {
28 | scala.srcDirs = ["gatling"]
29 | }
30 | }
31 |
32 | gatling {
33 | sourceRoot = "gatling"
34 | simulationsDir = "gatling"
35 | toolVersion = gatlingVersion
36 | }
37 |
38 | dependencies {
39 | play "com.typesafe.play:play-guice_$scalaVersion:$playVersion"
40 | play "com.typesafe.play:play-logback_$scalaVersion:$playVersion"
41 | play "com.typesafe.play:filters-helpers_$scalaVersion:$playVersion"
42 |
43 | play "org.joda:joda-convert:2.1.2"
44 | play "net.logstash.logback:logstash-logback-encoder:5.6"
45 |
46 | play "com.netaporter:scala-uri_$scalaVersion:0.4.16"
47 | play "net.codingwell:scala-guice_$scalaVersion:4.2.1"
48 |
49 | playTest "org.scalatestplus.play:scalatestplus-play_$scalaVersion:4.0.0-RC2"
50 | playTest "io.gatling.highcharts:gatling-charts-highcharts:$gatlingVersion"
51 | playTest "io.gatling:gatling-test-framework:$gatlingVersion"
52 | }
53 |
54 | repositories {
55 | jcenter()
56 | maven {
57 | name "lightbend-maven-releases"
58 | url "https://repo.lightbend.com/lightbend/maven-release"
59 | }
60 | ivy {
61 | name "lightbend-ivy-release"
62 | url "https://repo.lightbend.com/lightbend/ivy-releases"
63 | layout "ivy"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/build.sbt:
--------------------------------------------------------------------------------
1 | import play.sbt.PlaySettings
2 | import sbt.Keys._
3 |
4 | lazy val GatlingTest = config("gatling") extend Test
5 |
6 | scalaVersion in ThisBuild := "2.12.7"
7 |
8 | libraryDependencies += guice
9 | libraryDependencies += "org.joda" % "joda-convert" % "2.1.2"
10 | libraryDependencies += "net.logstash.logback" % "logstash-logback-encoder" % "5.2"
11 |
12 | libraryDependencies += "com.netaporter" %% "scala-uri" % "0.4.16"
13 | libraryDependencies += "net.codingwell" %% "scala-guice" % "4.2.1"
14 |
15 | libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "4.0.1" % Test
16 | libraryDependencies += "io.gatling.highcharts" % "gatling-charts-highcharts" % "3.0.1.1" % Test
17 | libraryDependencies += "io.gatling" % "gatling-test-framework" % "3.0.1.1" % Test
18 |
19 | // The Play project itself
20 | lazy val root = (project in file("."))
21 | .enablePlugins(Common, PlayService, PlayLayoutPlugin, GatlingPlugin)
22 | .configs(GatlingTest)
23 | .settings(inConfig(GatlingTest)(Defaults.testSettings): _*)
24 | .settings(
25 | name := """play-scala-rest-api-example""",
26 | scalaSource in GatlingTest := baseDirectory.value / "/gatling/simulation"
27 | )
28 |
29 | // Documentation for this project:
30 | // sbt "project docs" "~ paradox"
31 | // open docs/target/paradox/site/index.html
32 | lazy val docs = (project in file("docs")).enablePlugins(ParadoxPlugin).
33 | settings(
34 | paradoxProperties += ("download_url" -> "https://example.lightbend.com/v1/download/play-rest-api")
35 | )
36 |
--------------------------------------------------------------------------------
/conf/application.conf:
--------------------------------------------------------------------------------
1 | include "secure"
2 |
3 | # db connections = ((physical_core_count * 2) + effective_spindle_count)
4 | fixedConnectionPool = 5
5 |
6 | repository.dispatcher {
7 | executor = "thread-pool-executor"
8 | throughput = 1
9 | thread-pool-executor {
10 | fixed-pool-size = ${fixedConnectionPool}
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/conf/generated.keystore:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/playframework/play-scala-rest-api-example/d7f6ddcc01c0f2b1bd6d4c93b5185183f387a3f6/conf/generated.keystore
--------------------------------------------------------------------------------
/conf/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | ${application.home:-.}/logs/application.log
8 |
9 | %date [%level] from %logger in %thread - %message%n%xException
10 |
11 |
12 |
13 |
14 | ${application.home:-.}/logs/application.json
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ${application.home:-.}/logs/metrics.log
31 |
32 | %date [%level] from %logger in %thread - %message%n%xException
33 |
34 |
35 |
36 |
37 |
38 | %coloredLevel %logger{15} - %message%n%xException{10}
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
--------------------------------------------------------------------------------
/conf/routes:
--------------------------------------------------------------------------------
1 | -> /v1/posts v1.post.PostRouter
--------------------------------------------------------------------------------
/conf/secure.conf:
--------------------------------------------------------------------------------
1 | # Set up Play for HTTPS and locked down allowed hosts.
2 | # Nothing in here is required for REST, but it's a good default.
3 | play {
4 | http {
5 | cookies.strict = true
6 |
7 | session.secure = true
8 | session.httpOnly = true
9 |
10 | flash.secure = true
11 | flash.httpOnly = true
12 |
13 | forwarded.trustedProxies = ["::1", "127.0.0.1"]
14 | }
15 |
16 | i18n {
17 | langCookieSecure = true
18 | langCookieHttpOnly = true
19 | }
20 |
21 | filters {
22 | csrf {
23 | cookie.secure = true
24 | }
25 |
26 | hosts {
27 | allowed = ["localhost:9443", "localhost:9000"]
28 | }
29 |
30 | hsts {
31 | maxAge = 1 minute # don't interfere with other projects
32 | secureHost = "localhost"
33 | securePort = 9443
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/docs/build.sbt:
--------------------------------------------------------------------------------
1 | // You will need private bintray credentials to publish this with Lightbend theme
2 | // credentials += Credentials("Bintray", "dl.bintray.com", "", "")
3 | // resolvers += "bintray-typesafe-internal-maven-releases" at "https://dl.bintray.com/typesafe/internal-maven-releases/"
4 | // paradoxTheme := Some("com.lightbend.paradox" % "paradox-theme-lightbend" % "0.2.3")
5 |
6 | // Uses the out of the box generic theme.
7 | paradoxTheme := Some(builtinParadoxTheme("generic"))
8 |
9 |
--------------------------------------------------------------------------------
/docs/src/main/paradox/appendix.md:
--------------------------------------------------------------------------------
1 |
2 | # Appendix
3 |
4 | This appendix covers how to download, run, use and load test Play.
5 |
6 | ## Requirements
7 |
8 | You will need a JDK 1.8 that is more recent than b20. You can download the JDK from [here](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html).
9 |
10 | You will need to have [git](https://git-scm.com/) installed.
11 |
12 | ## Downloading
13 |
14 | You can clone the example project from Github:
15 |
16 | ```bash
17 | git clone https://github.com/playframework/play-scala-rest-api-example.git
18 | ```
19 |
20 | ## Running
21 |
22 | You need to download and install sbt for this application to run. You can do that by going to the [sbt download page](http://www.scala-sbt.org/download.html) and following the instructions for your platform.
23 |
24 | Once you have sbt installed, the following at the command prompt will download any required library dependencies, and start up Play in development mode:
25 |
26 | ```bash
27 | sbt run
28 | ```
29 |
30 | Play will start up on the HTTP port at . You don't need to deploy or reload anything -- changing any source code while the server is running will automatically recompile and hot-reload the application on the next HTTP request. You can read more about using Play [here](https://www.playframework.com/documentation/latest/PlayConsole).
31 |
32 | ## Usage
33 |
34 | If you call the same URL from the command line, you’ll see JSON. Using [httpie](https://httpie.org/), we can execute the command:
35 |
36 | ```bash
37 | http --verbose http://localhost:9000/v1/posts
38 | ```
39 |
40 | And get back:
41 |
42 | ```
43 | GET /v1/posts HTTP/1.1
44 | ```
45 |
46 | Likewise, you can also send a POST directly as JSON:
47 |
48 | ```bash
49 | http --verbose POST http://localhost:9000/v1/posts title="hello" body="world"
50 | ```
51 |
52 | and get:
53 |
54 | ```
55 | POST /v1/posts HTTP/1.1
56 | ```
57 |
58 | ## Load Testing
59 |
60 | The best way to see what Play can do is to run a load test. We've included [Gatling](https://gatling.io/) in this test project for integrated load testing.
61 |
62 | Start Play in production mode, by [staging the application](https://www.playframework.com/documentation/latest/Deploying) and running the play scripts:
63 |
64 | ```bash
65 | sbt stage
66 | ./target/universal/stage/bin/play-scala-rest-api-example -Dplay.http.secret.key=testing
67 | ```
68 |
69 | Then you'll start the Gatling load test up (it's already integrated into the project):
70 |
71 | ```bash
72 | sbt gatling:test
73 | ```
74 |
75 | For best results, start the gatling load test up on another machine so you do not have contending resources. You can edit the [Gatling simulation](http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure), and change the numbers as appropriate.
76 |
77 | Once the test completes, you'll see an HTML file containing the load test chart:
78 |
79 | ```bash
80 | ./rest-api/target/gatling/gatlingspec-1472579540405/index.html
81 | ```
82 |
83 | That will contain your load test results.
84 |
--------------------------------------------------------------------------------
/docs/src/main/paradox/index.md:
--------------------------------------------------------------------------------
1 | # Making a REST API with Play
2 |
3 | This is a multi-part guide to walk you through how to make a RESTful API with JSON using [Play Framework](https://playframework.com).
4 |
5 | We’ll demonstrate with a "best practices" REST API. You can get source code for this guide two ways:
6 |
7 | ## From Lightbend Tech Hub
8 |
9 | Download a pre-packaged bundle with this link [https://example.lightbend.com/v1/download/play-scala-rest-api-example](https://example.lightbend.com/v1/download/play-scala-rest-api-example)
10 |
11 | **Linux/Mac:**
12 |
13 | ```bash
14 | unzip play-scala-rest-api-example.zip
15 | cd play-scala-rest-api-example
16 | ./sbt
17 | ```
18 |
19 | **Windows:**
20 |
21 | 1. Unzip the download
22 | 1. From a command line `cd` into the directory where you expanded the downloaded `zip` file and run:
23 |
24 | ```bash
25 | sbt.bat
26 | ```
27 |
28 | ## [From Github](https://github.com/playframework/play-scala-rest-api-example/tree/2.6.x):
29 |
30 | ```bash
31 | git clone https://github.com/playframework/play-scala-rest-api-example.git
32 | git checkout 2.6.x
33 | ```
34 |
35 | This example is in Scala, but Play also has a [Java API](https://www.playframework.com/documentation/latest/JavaHome) which looks and acts just like the [Scala API](https://www.playframework.com/documentation/latest/ScalaHome), and has a corresponding [play-java-rest-api-example](https://github.com/playframework/play-java-rest-api-example) project. For instructions on running and using the project, please see the [[appendix]]. This project also comes with an integrated [Gatling](http://gatling.io/) load test -- again, instructions are in the appendix.
36 |
37 | Note that there’s more involved in a REST API -- monitoring, representation, and managing access to back end resources -- that we'll cover in subsequent posts. But first, let's address why Play is so effective as a REST API.
38 |
39 | ## When to use Play
40 |
41 | Play makes a good REST API implementation because Play does the right thing out of the box. Play makes simple things easy, makes hard things possible, and encourages code that scales because it works in sympathy with the JVM and the underlying hardware. But "safe and does the right thing" is the boring answer.
42 |
43 | The fun answer is that [Play is **fast**](https://www.lightbend.com/blog/why-is-play-framework-so-fast).
44 |
45 | In fact, Play is so fast that you have to turn off machines so that the rest of your architecture can keep up. The Hootsuite team was able to **reduce the number of servers by 80%** by [switching to Play](https://www.lightbend.com/resources/case-studies-and-stories/how-hootsuite-modernized-its-url-shortener). if you deploy Play with the same infrastructure that you were using for other web frameworks, you are effectively staging a denial of service attack against your own database.
46 |
47 | Play is fast because Play is **built on reactive bedrock**. Play starts from a reactive core, and builds on reactive principles all the way from the ground. Play breaks network packets into a stream of small chunks of bytes. It keeps a small pool of work stealing threads, mapped to the number of cores in the machine, and keeps those threads fed with those chunks. Play exposes those byte chunks to the application for body parsing, Server Sent Events and WebSockets through [Akka Streams](http://doc.akka.io/docs/akka/2.4/scala/stream/stream-introduction.html) -- the Reactive Streams implementation designed by the people who invented [Reactive Streams](http://www.reactive-streams.org/) and wrote the [Reactive Manifesto](http://www.reactivemanifesto.org/).
48 |
49 | Linkedin uses Play throughout its infrastructure. It wins on all [four quadrants of scalability](http://www.slideshare.net/brikis98/the-play-framework-at-linkedin/128-Outline1_Getting_started_with_Play2) ([video](https://youtu.be/8z3h4Uv9YbE)). Play's average "request per second" comes in around [tens of k on a basic quad core w/o any intentional tuning](https://twitter.com/kevinbowling1/status/764188720140398592) -- and it only gets better.
50 |
51 | Play provides an easy to use MVC paradigm, including hot-reloading without any JVM bytecode magic or container overhead. Startup time for a developer on Play was **reduced by roughly 7 times** for [Walmart Canada](https://www.lightbend.com/resources/case-studies-and-stories/walmart-boosts-conversions-by-20-with-lightbend-reactive-platform), and using Play **reduced development times by 2x to 3x**.
52 |
53 | Play combines this with a **reactive programming API** that lets you write async, non-blocking code in a straightforward fashion without worrying about complex and confusing "callback hell." In both Java or Scala, Play works on the same principle: leverage the asynchronous computation API that the language provides to you. In Play, you work with [`java.util.concurrent.CompletionStage`](https://docs.oracle.com/javase/8/docs/technotes/guides/concurrency/changes8.html) or [`scala.concurrent.Future`](http://docs.scala-lang.org/overviews/core/futures.html) API directly, and Play passes that asynchronous computation back through the framework.
54 |
55 | Finally, Play is modular and extensible. Play works with multiple runtime and compile time dependency injection frameworks like [Guice](https://www.playframework.com/documentation/latest/ScalaDependencyInjection), [Macwire](https://di-in-scala.github.io/), [Dagger](https://github.com/playframework/play-java-dagger2-example), and leverages DI principles to integrate authentication and authorization frameworks built on top of Play.
56 |
57 | ## Community
58 |
59 | To learn more about Play, check out the [Play tutorials](https://playframework.com/documentation/latest/Tutorials) and see more examples and blog posts about Play, including streaming [Server Side Events](https://github.com/playframework/play-streaming-scala) and first class [WebSocket support](https://github.com/playframework/play-websocket-scala).
60 |
61 | To get more involved and if you have questions, join the [forums](https://discuss.playframework.com) at and follow [PlayFramework on Twitter](https://twitter.com/playframework).
62 |
63 | ## Microservices vs REST APIs
64 |
65 | One thing to note here is that although this guide covers how to make a REST API in Play, it only covers Play itself and deploying Play. Building a REST API in Play does not automatically make it a "microservice" because it does not cover larger scale concerns about microservices such as ensuring resiliency, consistency, or monitoring.
66 |
67 | For full scale microservices, you want [Lagom](http://www.lagomframework.com/), which builds on top of Play -- a microservices framework for dealing with the ["data on the outside"](https://blog.acolyer.org/2016/09/13/data-on-the-outside-versus-data-on-the-inside/) problem, set up with persistence and service APIs that ensure that the service always stays up and responsive even in the face of chaos monkeys and network partitions.
68 |
69 | With that caveat, let's @ref[start working with Play](part-1/index.md)!
70 |
71 | @@@index
72 |
73 | * [Basics](part-1/index.md)
74 | * [Appendix](appendix.md)
75 |
76 | @@@
77 |
--------------------------------------------------------------------------------
/docs/src/main/paradox/part-1/index.md:
--------------------------------------------------------------------------------
1 | # Basics
2 |
3 | This guide will walk you through how to make a REST API with JSON using [Play Framework](https://playframework.com).
4 |
5 | To see the associated Github project, please go to or clone the project:
6 |
7 | ```bash
8 | git clone https://github.com/playframework/play-scala-rest-api-example.git
9 | ```
10 |
11 | We're going to be showing an already working Play project with most of the code available under the `app/v1` directory. There will be several different versions of the same project as this series expands, so you can compare different versions of the project against each other.
12 |
13 | To run Play on your own local computer, please see the instructions in the @ref[appendix](../appendix.md).
14 |
15 | ## Introduction
16 |
17 | We'll start off with a REST API that displays information for blog posts. Users should be able to write a title and a body of a blog post and create new blog posts, edit existing blog posts, and delete new blog posts.
18 |
19 | ## Modelling a Post Resource
20 |
21 | The way to do this in REST is to model the represented state as a resource. A blog post resource will have a unique id, a URL hyperlink that indicates the canonical location of the resource, the title of the blog post, and the body of the blog post.
22 |
23 | This resource is represented as a single case class in the Play application [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostResourceHandler.scala#L13):
24 |
25 | ```scala
26 | case class PostResource(
27 | id: String,
28 | link: String,
29 | title: String,
30 | body: String
31 | )
32 | ```
33 |
34 | This resource is mapped to and from JSON on the front end using Play, and is mapped to and from a persistent datastore on the backend using a handler.
35 |
36 | Play handles HTTP routing and representation for the REST API and makes it easy to write a non-blocking, asynchronous API that is an order of magnitude more efficient than other web application frameworks.
37 |
38 | ## Routing Post Requests
39 |
40 | Play has two complimentary routing mechanisms. In the conf directory, there's a file called "routes" which contains entries for the HTTP method and a relative URL path, and points it at an action in a controller.
41 |
42 | ```
43 | GET / controllers.HomeController.index()
44 | ```
45 |
46 | This is useful for situations where a front end service is rendering HTML. However, Play also contains a more powerful routing DSL that we will use for the REST API.
47 |
48 | For every HTTP request starting with `/v1/posts`, Play routes it to a dedicated `PostRouter` class to handle the Posts resource, through the [`conf/routes`](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/conf/routes) file:
49 |
50 | ```
51 | -> /v1/posts v1.post.PostRouter
52 | ```
53 |
54 | The `PostRouter` examines the URL and extracts data to pass along to the controller [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostRouter.scala):
55 |
56 | ```scala
57 | package v1.post
58 |
59 | import javax.inject.Inject
60 |
61 | import play.api.routing.Router.Routes
62 | import play.api.routing.SimpleRouter
63 | import play.api.routing.sird._
64 |
65 | class PostRouter @Inject()(controller: PostController) extends SimpleRouter {
66 | val prefix = "/v1/posts"
67 |
68 | def link(id: PostId): String = {
69 | import com.netaporter.uri.dsl._
70 | val url = prefix / id.toString
71 | url.toString()
72 | }
73 |
74 | override def routes: Routes = {
75 | case GET(p"/") =>
76 | controller.index
77 |
78 | case POST(p"/") =>
79 | controller.process
80 |
81 | case GET(p"/$id") =>
82 | controller.show(id)
83 | }
84 |
85 | }
86 | ```
87 |
88 | Play’s [routing DSL](https://www.playframework.com/documentation/latest/ScalaSirdRouter) (technically "String Interpolation Routing DSL", aka SIRD) shows how data can be extracted from the URL concisely and cleanly. SIRD is based around HTTP methods and a string interpolated extractor object – this means that when we type the string “/$id” and prefix it with “p”, then the path parameter id can be extracted and used in the block. Naturally, there are also operators to extract queries, regular expressions, and even add custom extractors. If you have a URL as follows:
89 |
90 | ```
91 | /posts/?sort=ascending&count=5
92 | ```
93 |
94 | Then you can extract the "sort" and "count" parameters in a single line:
95 |
96 | ```scala
97 | GET("/" ? q_?"sort=$sort" & q_?”count=${ int(count) }")
98 | ```
99 |
100 | SIRD is especially useful in a REST API where there can be many possible query parameters. Cake Solutions covers SIRD in more depth in a [fantastic blog post](http://www.cakesolutions.net/teamblogs/all-you-need-to-know-about-plays-routing-dsl).
101 |
102 | ## Using a Controller
103 |
104 | The `PostRouter` has a `PostController` injected into it through standard [JSR-330 dependency injection](https://github.com/google/guice/wiki/JSR330) [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostRouter.scala#L12):
105 |
106 | ```scala
107 | class PostRouter @Inject()(controller: PostController) extends SimpleRouter
108 | ```
109 |
110 | Before heading into the `PostController`, let's discuss how controllers work in Play.
111 |
112 | A controller [handles the work of processing](https://www.playframework.com/documentation/latest/ScalaActions) the HTTP request into an HTTP response in the context of an Action: it's where page rendering and HTML form processing happen. A controller extends [`play.api.mvc.BaseController`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.BaseController), which contains a number of utility methods and constants for working with HTTP. In particular, a `Controller` contains `Result` objects such as `Ok` and `Redirect`, and `HeaderNames` like `ACCEPT`.
113 |
114 | The methods in a controller consist of a method returning an [Action](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.Action). The Action provides the "engine" to Play.
115 |
116 | Using the action, the controller passes in a block of code that takes a [`Request`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.Request) passed in as implicit – this means that any in-scope method that takes an implicit request as a parameter will use this request automatically. Then, the block must return either a [`Result`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.mvc.Result), or a [`Future[Result]`](http://www.scala-lang.org/api/current/index.html#scala.concurrent.Future), depending on whether or not the action was called as `action { ... }` or [`action.async { ... }`](https://www.playframework.com/documentation/latest/ScalaAsync#How-to-create-a-Future[Result]).
117 |
118 | ### Handling GET Requests
119 |
120 | Here's a simple example of a Controller:
121 |
122 | ```scala
123 | import javax.inject.Inject
124 | import play.api.mvc._
125 |
126 | import scala.concurrent._
127 |
128 | class MyController @Inject()(val controllerComponents: ControllerComponents) extends BaseController {
129 |
130 | def index1: Action[AnyContent] = Action { implicit request =>
131 | val r: Result = Ok("hello world")
132 | r
133 | }
134 |
135 | def asyncIndex: Action[AnyContent] = Action.async { implicit request =>
136 | val r: Future[Result] = Future.successful(Ok("hello world"))
137 | r
138 | }
139 | }
140 | ```
141 |
142 | In this example, `index1` and `asyncIndex` have exactly the same behavior. Internally, it makes no difference whether we call `Result` or `Future[Result]` -- Play is non-blocking all the way through.
143 |
144 | However, if you're already working with `Future`, async makes it easier to pass that `Future` around. You can read more about this in the [handling asynchronous results](https://www.playframework.com/documentation/latest/ScalaAsync) section of the Play documentation.
145 |
146 | The PostController methods dealing with GET requests is [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostController.scala). Let's take a look at the most important parts:
147 |
148 | ```scala
149 | package v1.post
150 |
151 | import javax.inject.Inject
152 |
153 | import play.api.Logger
154 | import play.api.data.Form
155 | import play.api.libs.json.Json
156 | import play.api.mvc._
157 |
158 | import scala.concurrent.{ ExecutionContext, Future }
159 |
160 | class PostController @Inject()(cc: PostControllerComponents)(implicit ec: ExecutionContext)
161 | extends PostBaseController(cc) {
162 |
163 | def index: Action[AnyContent] = PostAction.async { implicit request =>
164 | logger.trace("index: ")
165 | postResourceHandler.find.map { posts =>
166 | Ok(Json.toJson(posts))
167 | }
168 | }
169 |
170 | def show(id: String): Action[AnyContent] = PostAction.async { implicit request =>
171 | logger.trace(s"show: id = $id")
172 | postResourceHandler.lookup(id).map { post =>
173 | Ok(Json.toJson(post))
174 | }
175 | }
176 | }
177 | ```
178 |
179 | Let's take `show` as an example. Here, the action defines a workflow for a request that maps to a single resource, i.e. `GET /v1/posts/123`.
180 |
181 | ```scala
182 | def show(id: String): Action[AnyContent] = PostAction.async { implicit request =>
183 | logger.trace(s"show: id = $id")
184 | postResourceHandler.lookup(id).map { post =>
185 | Ok(Json.toJson(post))
186 | }
187 | }
188 | ```
189 |
190 | The `id` is passed in as a `String`, and the handler looks up and returns a `PostResource`. The `Ok()` sends back a `Result` with a status code of "200 OK", containing a response body consisting of the `PostResource` serialized as JSON.
191 |
192 | ### Processing Form Input
193 |
194 | Handling a `POST` request is also easy and is done through the `process` method:
195 |
196 | ```scala
197 | private val form: Form[PostFormInput] = {
198 | import play.api.data.Forms._
199 |
200 | Form(
201 | mapping(
202 | "title" -> nonEmptyText,
203 | "body" -> text
204 | )(PostFormInput.apply)(PostFormInput.unapply)
205 | )
206 | }
207 |
208 | def process: Action[AnyContent] = PostAction.async { implicit request =>
209 | logger.trace("process: ")
210 | processJsonPost()
211 | }
212 |
213 | private def processJsonPost[A]()(implicit request: PostRequest[A]): Future[Result] = {
214 | def failure(badForm: Form[PostFormInput]) = {
215 | Future.successful(BadRequest(badForm.errorsAsJson))
216 | }
217 |
218 | def success(input: PostFormInput) = {
219 | postResourceHandler.create(input).map { post =>
220 | Created(Json.toJson(post)).withHeaders(LOCATION -> post.link)
221 | }
222 | }
223 |
224 | form.bindFromRequest().fold(failure, success)
225 | }
226 | ```
227 |
228 | Here, the `process` action is an action wrapper, and `processJsonPost` does most of the work. In `processJsonPost`, we get to the [form processing](https://www.playframework.com/documentation/latest/ScalaForms) part of the code.
229 |
230 | Here, `form.bindFromRequest()` will map input from the HTTP request to a [`play.api.data.Form`](https://www.playframework.com/documentation/latest/api/scala/index.html#play.api.data.Form), and handles form validation and error reporting.
231 |
232 | If the `PostFormInput` passes validation, it's passed to the resource handler, using the `success` method. If the form processing fails, then the `failure` method is called and the `FormError` is returned in JSON format.
233 |
234 | ```scala
235 | private val form: Form[PostFormInput] = {
236 | import play.api.data.Forms._
237 |
238 | Form(
239 | mapping(
240 | "title" -> nonEmptyText,
241 | "body" -> text
242 | )(PostFormInput.apply)(PostFormInput.unapply)
243 | )
244 | }
245 | ```
246 |
247 | The form binds to the HTTP request using the names in the mapping -- `title` and `body` to the `PostFormInput` case class [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostController.scala#L12).
248 |
249 | ```scala
250 | case class PostFormInput(title: String, body: String)
251 | ```
252 |
253 | That's all you need to do to handle a basic web application! As with most things, there are more details that need to be handled. That's where creating custom Actions comes in.
254 |
255 | ## Using Actions
256 |
257 | We saw in the `PostController` that each method is connected to an Action through the `PostAction.async` method [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostController.scala#L33):
258 |
259 | ```scala
260 | def index: Action[AnyContent] = PostAction.async { implicit request =>
261 | logger.trace("index: ")
262 | postResourceHandler.find.map { posts =>
263 | Ok(Json.toJson(posts))
264 | }
265 | }
266 | ```
267 |
268 | The `PostAction.async` is a [custom action builder](https://www.playframework.com/documentation/2.6.x/ScalaActionsComposition#Custom-action-builders) defined [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostActionBuilder.scala#L49-L53) that can handle `PostRequest`s (see definition [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostActionBuilder.scala#L20)):
269 |
270 | `PostAction` is involved in each action in the controller -- it mediates the paperwork involved with processing a request into a response, adding context to the request and enriching the response with headers and cookies. ActionBuilders are essential for handling authentication, authorization and monitoring functionality.
271 |
272 | ActionBuilders work through a process called [action composition](https://www.playframework.com/documentation/latest/ScalaActionsComposition). The ActionBuilder class has a method called `invokeBlock` that takes in a `Request` and a function (also known as a block, lambda or closure) that accepts a `Request` of a given type, and produces a `Future[Result]`.
273 |
274 | So, if you want to work with an `Action` that has a "FooRequest" that has a Foo attached, it's easy:
275 |
276 | ```scala
277 | class FooRequest[A](request: Request[A], val foo: Foo) extends WrappedRequest(request)
278 |
279 | class FooAction @Inject()(parsers: PlayBodyParsers)(implicit val executionContext: ExecutionContext) extends ActionBuilder[FooRequest, AnyContent] {
280 |
281 | type FooRequestBlock[A] = FooRequest[A] => Future[Result]
282 |
283 | override def parser: BodyParser[AnyContent] = parsers.defaultBodyParser
284 |
285 | override def invokeBlock[A](request: Request[A], block: FooRequestBlock[A]): Future[Result] = {
286 | block(new FooRequest[A](request, Foo()))
287 | }
288 | }
289 | ```
290 |
291 | You create an `ActionBuilder[FooRequest, AnyContent]`, override `invokeBlock`, and then call the function with an instance of `FooRequest`.
292 |
293 | Then, when you call `fooAction`, the request type is `FooRequest`:
294 |
295 | ```scala
296 | fooAction { request: FooRequest =>
297 | Ok(request.foo.toString)
298 | }
299 | ```
300 |
301 | And `request.foo` will be added automatically.
302 |
303 | You can keep composing action builders inside each other, so you don't have to layer all the functionality in one single ActionBuilder, or you can create a custom `ActionBuilder` for each package you work with, according to your taste. For the purposes of this blog post, we'll keep everything together in a single class.
304 |
305 | You can see `PostAction` builder [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostActionBuilder.scala#L49-L78):
306 |
307 | ```scala
308 | trait PostRequestHeader extends MessagesRequestHeader with PreferredMessagesProvider
309 | class PostRequest[A](request: Request[A], val messagesApi: MessagesApi) extends WrappedRequest(request) with PostRequestHeader
310 |
311 | class PostActionBuilder @Inject()(messagesApi: MessagesApi, playBodyParsers: PlayBodyParsers)
312 | (implicit val executionContext: ExecutionContext)
313 | extends ActionBuilder[PostRequest, AnyContent]
314 | with RequestMarkerContext
315 | with HttpVerbs {
316 |
317 |
318 | val parser: BodyParser[AnyContent] = playBodyParsers.anyContent
319 |
320 |
321 | type PostRequestBlock[A] = PostRequest[A] => Future[Result]
322 |
323 |
324 | private val logger = Logger(this.getClass)
325 |
326 |
327 | override def invokeBlock[A](request: Request[A],
328 | block: PostRequestBlock[A]): Future[Result] = {
329 | // Convert to marker context and use request in block
330 | implicit val markerContext: MarkerContext = requestHeaderToMarkerContext(request)
331 | logger.trace(s"invokeBlock: ")
332 |
333 |
334 | val future = block(new PostRequest(request, messagesApi))
335 |
336 |
337 | future.map { result =>
338 | request.method match {
339 | case GET | HEAD =>
340 | result.withHeaders("Cache-Control" -> s"max-age: 100")
341 | case other =>
342 | result
343 | }
344 | }
345 | }
346 | }
347 | ```
348 |
349 | `PostAction` does a couple of different things here. The first thing it does is to log the request as it comes in. Next, it pulls out `MessagesApi` for the request, and adds that to a `PostRequest` , and runs the function, returning a `Future[Result]`.
350 |
351 | When the future completes, we map the result so we can replace it with a slightly different result. We compare the result's method against `HttpVerbs`, and if it's a GET or HEAD, we append a `Cache-Control` header with a `max-age` directive. We need an `ExecutionContext` for `future.map` operations, so we pass in the default execution context implicitly at the top of the class.
352 |
353 | Now that we have a `PostRequest`, we can call "request.messagesApi" explicitly from any action in the controller, for free, and we can append information to the result after the user action has been completed.
354 |
355 | ## Converting resources with PostResourceHandler
356 |
357 | The `PostResourceHandler` is responsible for converting backend data from a repository into a `PostResource`. We won't go into detail on the `PostRepository` details for now, only that it returns data in an backend-centric state.
358 |
359 | A REST resource has information that a backend repository does not -- it knows about the operations available on the resource, and contains URI information that a single backend may not have. As such, we want to be able to change the representation that we use internally without changing the resource that we expose publicly.
360 |
361 | You can see the `PostResourceHandler` [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostResourceHandler.scala#L35-L66):
362 |
363 | ```scala
364 | class PostResourceHandler @Inject()(
365 | routerProvider: Provider[PostRouter],
366 | postRepository: PostRepository)(implicit ec: ExecutionContext) {
367 |
368 |
369 | def create(postInput: PostFormInput)(implicit mc: MarkerContext): Future[PostResource] = {
370 | val data = PostData(PostId("999"), postInput.title, postInput.body)
371 | // We don't actually create the post, so return what we have
372 | postRepository.create(data).map { id =>
373 | createPostResource(data)
374 | }
375 | }
376 |
377 |
378 | def lookup(id: String)(implicit mc: MarkerContext): Future[Option[PostResource]] = {
379 | val postFuture = postRepository.get(PostId(id))
380 | postFuture.map { maybePostData =>
381 | maybePostData.map { postData =>
382 | createPostResource(postData)
383 | }
384 | }
385 | }
386 |
387 |
388 | def find(implicit mc: MarkerContext): Future[Iterable[PostResource]] = {
389 | postRepository.list().map { postDataList =>
390 | postDataList.map(postData => createPostResource(postData))
391 | }
392 | }
393 |
394 |
395 | private def createPostResource(p: PostData): PostResource = {
396 | PostResource(p.id.toString, routerProvider.get.link(p.id), p.title, p.body)
397 | }
398 |
399 | }
400 | ```
401 | Here, it's a straight conversion in `createPostResource`, with the only hook being that the router provides the resource's URL, since it's something that `PostData` does not have itself.
402 |
403 | ## Rendering Content as JSON
404 |
405 | Play handles the work of converting a `PostResource` through [Play JSON](https://www.playframework.com/documentation/latest/ScalaJson). Play JSON provides a DSL that looks up the conversion for the `PostResource` singleton object, so you don't need to declare it at the use point.
406 |
407 | You can see the `PostResource` object [here](https://github.com/playframework/play-scala-rest-api-example/blob/2.6.x/app/v1/post/PostResourceHandler.scala#L15-L30):
408 |
409 | ```scala
410 | object PostResource {
411 |
412 | implicit val implicitWrites = new Writes[PostResource] {
413 | def writes(post: PostResource): JsValue = {
414 | Json.obj(
415 | "id" -> post.id,
416 | "link" -> post.link,
417 | "title" -> post.title,
418 | "body" -> post.body
419 | )
420 | }
421 | }
422 | }
423 | ```
424 |
425 | Once the implicit is defined in the companion object, then it will be looked up automatically when handed an instance of the class. This means that when the controller converts to JSON, the conversion will just work, without any additional imports or setup.
426 |
427 | ```scala
428 | val json: JsValue = Json.toJson(post)
429 | ```
430 |
431 | Play JSON also has options to incrementally parse and generate JSON for continuously streaming JSON responses.
432 |
433 | ## Summary
434 |
435 | We've shown how to easy it is to put together a basic REST API in Play. Using this code, we can put together backend data, convert it to JSON and transfer it over HTTP with a minimum of fuss.
436 |
437 | In the next guide, we'll discuss content representation and provide an HTML interface that exists alongside the JSON API.
438 |
--------------------------------------------------------------------------------
/gatling/simulation/GatlingSpec.scala:
--------------------------------------------------------------------------------
1 | package simulation
2 |
3 | import io.gatling.core.Predef._
4 | import io.gatling.core.structure.{ChainBuilder, ScenarioBuilder}
5 | import io.gatling.http.Predef._
6 | import io.gatling.http.protocol.HttpProtocolBuilder
7 |
8 | import scala.concurrent.duration._
9 | import scala.language.postfixOps
10 |
11 | // run with "sbt gatling:test" on another machine so you don't have resources contending.
12 | // http://gatling.io/docs/2.2.2/general/simulation_structure.html#simulation-structure
13 | class GatlingSpec extends Simulation {
14 |
15 | // change this to another machine, make sure you have Play running in producion mode
16 | // i.e. sbt stage / sbt dist and running the script
17 | val httpConf: HttpProtocolBuilder = http.baseUrl("http://localhost:9000/v1/posts")
18 |
19 | val readClients: ScenarioBuilder = scenario("Clients").exec(Index.refreshManyTimes)
20 |
21 | setUp(
22 | // For reference, this hits 25% CPU on a 5820K with 32 GB, running both server and load test.
23 | // In general, you want to ramp up load slowly, and measure with a JVM that has been "warmed up":
24 | // https://groups.google.com/forum/#!topic/gatling/mD15aj-fyo4
25 | readClients.inject(rampUsers(10000).during(100.seconds)).protocols(httpConf)
26 | )
27 | }
28 |
29 | object Index {
30 |
31 | def refreshAfterOneSecond: ChainBuilder =
32 | exec(http("Index").get("/").check(status.is(200))).pause(1)
33 |
34 | val refreshManyTimes: ChainBuilder = repeat(10000) {
35 | refreshAfterOneSecond
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/playframework/play-scala-rest-api-example/d7f6ddcc01c0f2b1bd6d4c93b5185183f387a3f6/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-bin.zip
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStorePath=wrapper/dists
5 | zipStoreBase=GRADLE_USER_HOME
6 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Attempt to set APP_HOME
10 | # Resolve links: $0 may be a link
11 | PRG="$0"
12 | # Need this for relative symlinks.
13 | while [ -h "$PRG" ] ; do
14 | ls=`ls -ld "$PRG"`
15 | link=`expr "$ls" : '.*-> \(.*\)$'`
16 | if expr "$link" : '/.*' > /dev/null; then
17 | PRG="$link"
18 | else
19 | PRG=`dirname "$PRG"`"/$link"
20 | fi
21 | done
22 | SAVED="`pwd`"
23 | cd "`dirname \"$PRG\"`/" >/dev/null
24 | APP_HOME="`pwd -P`"
25 | cd "$SAVED" >/dev/null
26 |
27 | APP_NAME="Gradle"
28 | APP_BASE_NAME=`basename "$0"`
29 |
30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
31 | DEFAULT_JVM_OPTS=""
32 |
33 | # Use the maximum available, or set MAX_FD != -1 to use that value.
34 | MAX_FD="maximum"
35 |
36 | warn () {
37 | echo "$*"
38 | }
39 |
40 | die () {
41 | echo
42 | echo "$*"
43 | echo
44 | exit 1
45 | }
46 |
47 | # OS specific support (must be 'true' or 'false').
48 | cygwin=false
49 | msys=false
50 | darwin=false
51 | nonstop=false
52 | case "`uname`" in
53 | CYGWIN* )
54 | cygwin=true
55 | ;;
56 | Darwin* )
57 | darwin=true
58 | ;;
59 | MINGW* )
60 | msys=true
61 | ;;
62 | NONSTOP* )
63 | nonstop=true
64 | ;;
65 | esac
66 |
67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
68 |
69 | # Determine the Java command to use to start the JVM.
70 | if [ -n "$JAVA_HOME" ] ; then
71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
72 | # IBM's JDK on AIX uses strange locations for the executables
73 | JAVACMD="$JAVA_HOME/jre/sh/java"
74 | else
75 | JAVACMD="$JAVA_HOME/bin/java"
76 | fi
77 | if [ ! -x "$JAVACMD" ] ; then
78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
79 |
80 | Please set the JAVA_HOME variable in your environment to match the
81 | location of your Java installation."
82 | fi
83 | else
84 | JAVACMD="java"
85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
86 |
87 | Please set the JAVA_HOME variable in your environment to match the
88 | location of your Java installation."
89 | fi
90 |
91 | # Increase the maximum file descriptors if we can.
92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
93 | MAX_FD_LIMIT=`ulimit -H -n`
94 | if [ $? -eq 0 ] ; then
95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
96 | MAX_FD="$MAX_FD_LIMIT"
97 | fi
98 | ulimit -n $MAX_FD
99 | if [ $? -ne 0 ] ; then
100 | warn "Could not set maximum file descriptor limit: $MAX_FD"
101 | fi
102 | else
103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
104 | fi
105 | fi
106 |
107 | # For Darwin, add options to specify how the application appears in the dock
108 | if $darwin; then
109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
110 | fi
111 |
112 | # For Cygwin, switch paths to Windows format before running java
113 | if $cygwin ; then
114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
116 | JAVACMD=`cygpath --unix "$JAVACMD"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Escape application args
158 | save () {
159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
160 | echo " "
161 | }
162 | APP_ARGS=$(save "$@")
163 |
164 | # Collect all arguments for the java command, following the shell quoting and substitution rules
165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
166 |
167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
169 | cd "$(dirname "$0")"
170 | fi
171 |
172 | exec "$JAVACMD" "$@"
173 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | set DIRNAME=%~dp0
12 | if "%DIRNAME%" == "" set DIRNAME=.
13 | set APP_BASE_NAME=%~n0
14 | set APP_HOME=%DIRNAME%
15 |
16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
17 | set DEFAULT_JVM_OPTS=
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windows variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 |
53 | :win9xME_args
54 | @rem Slurp the command line arguments.
55 | set CMD_LINE_ARGS=
56 | set _SKIP=2
57 |
58 | :win9xME_args_slurp
59 | if "x%~1" == "x" goto execute
60 |
61 | set CMD_LINE_ARGS=%*
62 |
63 | :execute
64 | @rem Setup the command line
65 |
66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
67 |
68 | @rem Execute Gradle
69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
70 |
71 | :end
72 | @rem End local scope for the variables with windows NT shell
73 | if "%ERRORLEVEL%"=="0" goto mainEnd
74 |
75 | :fail
76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
77 | rem the _cmd.exe /c_ return code!
78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
79 | exit /b 1
80 |
81 | :mainEnd
82 | if "%OS%"=="Windows_NT" endlocal
83 |
84 | :omega
85 |
--------------------------------------------------------------------------------
/project/Common.scala:
--------------------------------------------------------------------------------
1 | import sbt.Keys._
2 | import sbt._
3 | import sbt.plugins.JvmPlugin
4 |
5 | /**
6 | * Settings that are comment to all the SBT projects
7 | */
8 | object Common extends AutoPlugin {
9 | override def trigger = allRequirements
10 | override def requires: sbt.Plugins = JvmPlugin
11 |
12 | override def projectSettings = Seq(
13 | organization := "com.lightbend.restapi",
14 | version := "1.0-SNAPSHOT",
15 | resolvers += Resolver.typesafeRepo("releases"),
16 | javacOptions ++= Seq("-source", "1.8", "-target", "1.8"),
17 | scalacOptions ++= Seq(
18 | "-encoding",
19 | "UTF-8", // yes, this is 2 args
20 | "-target:jvm-1.8",
21 | "-deprecation",
22 | "-feature",
23 | "-unchecked",
24 | "-Yno-adapted-args",
25 | "-Ywarn-numeric-widen",
26 | "-Xfatal-warnings"
27 | ),
28 | scalacOptions in Test ++= Seq("-Yrangepos"),
29 | autoAPIMappings := true
30 | )
31 | }
32 |
--------------------------------------------------------------------------------
/project/build.properties:
--------------------------------------------------------------------------------
1 | sbt.version=1.2.8
2 |
--------------------------------------------------------------------------------
/project/plugins.sbt:
--------------------------------------------------------------------------------
1 | // The Play plugin
2 | addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.0")
3 |
4 | // sbt-paradox, used for documentation
5 | addSbtPlugin("com.lightbend.paradox" % "sbt-paradox" % "0.4.4")
6 |
7 | // Load testing tool:
8 | // http://gatling.io/docs/2.2.2/extensions/sbt_plugin.html
9 | addSbtPlugin("io.gatling" % "gatling-sbt" % "2.2.2")
10 |
11 | // Scala formatting: "sbt scalafmt"
12 | addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15")
13 |
--------------------------------------------------------------------------------
/scripts/test-gradle:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -o pipefail
5 |
6 | # Using cut because TRAVIS_SCALA_VERSION is the full Scala
7 | # version (for example 2.12.4), but Gradle expects just the
8 | # binary version (for example 2.12)
9 | scala_binary_version=$(echo $TRAVIS_SCALA_VERSION | cut -c1-4)
10 |
11 | echo "+------------------------------+"
12 | echo "| Executing tests using Gradle |"
13 | echo "+------------------------------+"
14 | ./gradlew -Dscala.binary.version=$scala_binary_version check -i --stacktrace
15 |
--------------------------------------------------------------------------------
/scripts/test-sbt:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -o pipefail
5 |
6 | echo "+----------------------------+"
7 | echo "| Executing tests using sbt |"
8 | echo "+----------------------------+"
9 | sbt ++$TRAVIS_SCALA_VERSION test
10 |
--------------------------------------------------------------------------------
/scripts/validate-gatling:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -o pipefail
5 |
6 | echo "+----------------------------+"
7 | echo "| Compiling Gatling tests |"
8 | echo "+----------------------------+"
9 | sbt ++$TRAVIS_SCALA_VERSION gatling:compile
10 |
--------------------------------------------------------------------------------
/test/controllers/PostRouterSpec.scala:
--------------------------------------------------------------------------------
1 | import org.scalatestplus.play._
2 | import org.scalatestplus.play.guice._
3 | import play.api.libs.json.{ JsResult, Json }
4 | import play.api.mvc.{ RequestHeader, Result }
5 | import play.api.test._
6 | import play.api.test.Helpers._
7 | import play.api.test.CSRFTokenHelper._
8 | import v1.post.PostResource
9 |
10 | import scala.concurrent.Future
11 |
12 | class PostRouterSpec extends PlaySpec with GuiceOneAppPerTest {
13 |
14 | "PostRouter" should {
15 |
16 | "render the list of posts" in {
17 | val request = FakeRequest(GET, "/v1/posts").withHeaders(HOST -> "localhost:9000").withCSRFToken
18 | val home:Future[Result] = route(app, request).get
19 |
20 | val posts: Seq[PostResource] = Json.fromJson[Seq[PostResource]](contentAsJson(home)).get
21 | posts.filter(_.id == "1").head mustBe (PostResource("1","/v1/posts/1", "title 1", "blog post 1" ))
22 |
23 | }
24 |
25 |
26 | }
27 |
28 | }
--------------------------------------------------------------------------------