├── .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 | } --------------------------------------------------------------------------------