From 39fc33f39db6d8a521db3a5b93600b5a96253615 Mon Sep 17 00:00:00 2001 From: Stefan Sprenger Date: Fri, 29 Mar 2019 10:47:04 +0100 Subject: [PATCH] Initial commit --- .../$model__Camel$Controller.scala | 46 +++ .../app/views/$model__camel$/form.scala.html | 12 + .g8/form/default.properties | 2 + .../$model__Camel$ControllerSpec.scala | 75 +++++ .gitignore | 8 + app/controllers/DocumentController.scala | 17 + build.sbt | 17 + conf/application.conf | 1 + conf/logback.xml | 41 +++ conf/messages | 1 + conf/routes | 5 + project/build.properties | 1 + project/plugins.sbt | 2 + test/controllers/DocumentControllerSpec.scala | 311 ++++++++++++++++++ 14 files changed, 539 insertions(+) create mode 100644 .g8/form/app/controllers/$model__Camel$Controller.scala create mode 100644 .g8/form/app/views/$model__camel$/form.scala.html create mode 100644 .g8/form/default.properties create mode 100644 .g8/form/test/controllers/$model__Camel$ControllerSpec.scala create mode 100644 .gitignore create mode 100644 app/controllers/DocumentController.scala create mode 100644 build.sbt create mode 100644 conf/application.conf create mode 100644 conf/logback.xml create mode 100644 conf/messages create mode 100644 conf/routes create mode 100644 project/build.properties create mode 100644 project/plugins.sbt create mode 100644 test/controllers/DocumentControllerSpec.scala diff --git a/.g8/form/app/controllers/$model__Camel$Controller.scala b/.g8/form/app/controllers/$model__Camel$Controller.scala new file mode 100644 index 0000000..aff914c --- /dev/null +++ b/.g8/form/app/controllers/$model__Camel$Controller.scala @@ -0,0 +1,46 @@ +package controllers + +import javax.inject._ +import play.api.mvc._ + +import play.api.data._ +import play.api.data.Forms._ + +case class $model;format="Camel"$Data(name: String, age: Int) + +// NOTE: Add the following to conf/routes to enable compilation of this class: +/* +GET /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Get +POST /$model;format="camel"$ controllers.$model;format="Camel"$Controller.$model;format="camel"$Post +*/ + +/** + * $model;format="Camel"$ form controller for Play Scala + */ +class $model;format="Camel"$Controller @Inject()(mcc: MessagesControllerComponents) extends MessagesAbstractController(mcc) { + + val $model;format="camel"$Form = Form( + mapping( + "name" -> text, + "age" -> number + )($model;format="Camel"$Data.apply)($model;format="Camel"$Data.unapply) + ) + + def $model;format="camel"$Get() = Action { implicit request: MessagesRequest[AnyContent] => + Ok(views.html.$model;format="camel"$.form($model;format="camel"$Form)) + } + + def $model;format="camel"$Post() = Action { implicit request: MessagesRequest[AnyContent] => + $model;format="camel"$Form.bindFromRequest.fold( + formWithErrors => { + // binding failure, you retrieve the form containing errors: + BadRequest(views.html.$model;format="camel"$.form(formWithErrors)) + }, + $model;format="camel"$Data => { + /* binding success, you get the actual value. */ + /* flashing uses a short lived cookie */ + Redirect(routes.$model;format="Camel"$Controller.$model;format="camel"$Get()).flashing("success" -> ("Successful " + $model;format="camel"$Data.toString)) + } + ) + } +} diff --git a/.g8/form/app/views/$model__camel$/form.scala.html b/.g8/form/app/views/$model__camel$/form.scala.html new file mode 100644 index 0000000..14674ba --- /dev/null +++ b/.g8/form/app/views/$model__camel$/form.scala.html @@ -0,0 +1,12 @@ +@($model;format="camel"$Form: Form[$model;format="Camel"$Data])(implicit request: MessagesRequestHeader) + +

$model;format="camel"$ form

+ +@request.flash.get("success").getOrElse("") + +@helper.form(action = routes.$model;format="Camel"$Controller.$model;format="camel"$Post()) { + @helper.CSRF.formField + @helper.inputText($model;format="camel"$Form("name")) + @helper.inputText($model;format="camel"$Form("age")) + +} diff --git a/.g8/form/default.properties b/.g8/form/default.properties new file mode 100644 index 0000000..32090f3 --- /dev/null +++ b/.g8/form/default.properties @@ -0,0 +1,2 @@ +description = Generates a Controller with form handling +model = user diff --git a/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala b/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala new file mode 100644 index 0000000..9d3e31b --- /dev/null +++ b/.g8/form/test/controllers/$model__Camel$ControllerSpec.scala @@ -0,0 +1,75 @@ +package controllers + +import javax.inject._ +import play.api._ +import play.api.mvc._ +import play.api.i18n._ + +import play.api.data._ +import play.api.data.Forms._ + +import org.scalatestplus.play._ +import play.api.test._ +import play.api.test.Helpers._ + +import play.filters.csrf.CSRF.Token +import play.filters.csrf.{CSRFConfigProvider, CSRFFilter} + +/** + * $model;format="Camel"$ form controller specs + */ +class $model;format="Camel"$ControllerSpec extends PlaySpec with GuiceOneAppPerTest with Injecting { + + // Provide stubs for components based off Helpers.stubControllerComponents() + class StubComponents(cc:ControllerComponents = stubControllerComponents()) extends MessagesControllerComponents { + override val parsers: PlayBodyParsers = cc.parsers + override val messagesApi: MessagesApi = cc.messagesApi + override val langs: Langs = cc.langs + override val fileMimeTypes: FileMimeTypes = cc.fileMimeTypes + override val executionContext: ExecutionContext = cc.executionContext + override val actionBuilder: ActionBuilder[Request, AnyContent] = cc.actionBuilder + override val messagesActionBuilder: MessagesActionBuilder = new DefaultMessagesActionBuilderImpl(parsers.default, messagesApi)(executionContext) + } + + "$model;format="Camel"$Controller GET" should { + + "render the index page from a new instance of controller" in { + val controller = new $model;format="Camel"$Controller(new StubComponents()) + val request = FakeRequest().withCSRFToken + val home = controller.$model;format="camel"$Get().apply(request) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + + "render the index page from the application" in { + val controller = inject[$model;format="Camel"$Controller] + val request = FakeRequest().withCSRFToken + val home = controller.$model;format="camel"$Get().apply(request) + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + + "render the index page from the router" in { + val request = CSRFTokenHelper.addCSRFToken(FakeRequest(GET, "/derp")) + val home = route(app, request).get + + status(home) mustBe OK + contentType(home) mustBe Some("text/html") + } + } + + "$model;format="Camel"$Controller POST" should { + "process form" in { + val request = { + FakeRequest(POST, "/$model;format="camel"$") + .withFormUrlEncodedBody("name" -> "play", "age" -> "4") + } + val home = route(app, request).get + + status(home) mustBe SEE_OTHER + } + } + +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eb372fc --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +logs +target +/.idea +/.idea_modules +/.classpath +/.project +/.settings +/RUNNING_PID diff --git a/app/controllers/DocumentController.scala b/app/controllers/DocumentController.scala new file mode 100644 index 0000000..0500d37 --- /dev/null +++ b/app/controllers/DocumentController.scala @@ -0,0 +1,17 @@ +package controllers + +import javax.inject.Inject + +import play.api._ +import play.api.libs.json.Json +import play.api.mvc._ + +class DocumentController @Inject()( + controllerComponents: ControllerComponents +)( +) extends AbstractController(controllerComponents) { + + def index = Action { + Ok(Json.obj("Message" -> "Please implement me!")) + } +} diff --git a/build.sbt b/build.sbt new file mode 100644 index 0000000..4593069 --- /dev/null +++ b/build.sbt @@ -0,0 +1,17 @@ +name := """hashtag-extractor""" +organization := "io.iaa" + +version := "1.0-SNAPSHOT" + +lazy val root = (project in file(".")).enablePlugins(PlayScala) + +scalaVersion := "2.12.8" + +libraryDependencies += guice +libraryDependencies += "org.scalatestplus.play" %% "scalatestplus-play" % "4.0.1" % Test + +// Adds additional packages into Twirl +//TwirlKeys.templateImports += "io.iaa.controllers._" + +// Adds additional packages into conf/routes +// play.sbt.routes.RoutesKeys.routesImport += "io.iaa.binders._" diff --git a/conf/application.conf b/conf/application.conf new file mode 100644 index 0000000..cb94680 --- /dev/null +++ b/conf/application.conf @@ -0,0 +1 @@ +# https://www.playframework.com/documentation/latest/Configuration diff --git a/conf/logback.xml b/conf/logback.xml new file mode 100644 index 0000000..9df7f6e --- /dev/null +++ b/conf/logback.xml @@ -0,0 +1,41 @@ + + + + + + + ${application.home:-.}/logs/application.log + + %date [%level] from %logger in %thread - %message%n%xException + + + + + + %coloredLevel %logger{15} - %message%n%xException{10} + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/conf/messages b/conf/messages new file mode 100644 index 0000000..0226738 --- /dev/null +++ b/conf/messages @@ -0,0 +1 @@ +# https://www.playframework.com/documentation/latest/ScalaI18N diff --git a/conf/routes b/conf/routes new file mode 100644 index 0000000..1d6e9fc --- /dev/null +++ b/conf/routes @@ -0,0 +1,5 @@ +# Routes +# This file defines all application routes (Higher priority routes first) +# https://www.playframework.com/documentation/latest/ScalaRouting +# +GET / controllers.DocumentController.index diff --git a/project/build.properties b/project/build.properties new file mode 100644 index 0000000..c0bab04 --- /dev/null +++ b/project/build.properties @@ -0,0 +1 @@ +sbt.version=1.2.8 diff --git a/project/plugins.sbt b/project/plugins.sbt new file mode 100644 index 0000000..908077a --- /dev/null +++ b/project/plugins.sbt @@ -0,0 +1,2 @@ +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.7.0") +addSbtPlugin("org.foundweekends.giter8" % "sbt-giter8-scaffold" % "0.11.0") diff --git a/test/controllers/DocumentControllerSpec.scala b/test/controllers/DocumentControllerSpec.scala new file mode 100644 index 0000000..36fa797 --- /dev/null +++ b/test/controllers/DocumentControllerSpec.scala @@ -0,0 +1,311 @@ +package controllers + +import java.sql.Date + +import org.scalatest._ +import org.scalatestplus.play._ +import org.scalatestplus.play.guice._ +import play.api.mvc._ +import play.api.test._ +import play.api.test.Helpers._ + +/* + * A document may be implemented as follows: + * + * case class Document(id: Int, created_at: Date, title: String, body: string) { + * def hashtags() = { + * // extract hashtags from attribute body + * } + * } + */ +class DocumentControllerSpec extends PlaySpec + with BeforeAndAfterEach + with GuiceOneAppPerTest + with Injecting { + override def beforeEach() { + /* + * Note that you typically do not need to provide an id when creating database records through an ORM or any other access layer. + * create Document(id = 1, title = "Quarkus, a Kubernetes Native Java Framework", body = "Red Hat has released #Quarkus, a #Kubernetes native #Java framework tailored for GraalVM and OpenJDK HotSpot.") + * create Document(id = 2, title = "Java 11 Released", body = "#Java 11 has arrived. The new release is the first planned appearance of #Oracle's #LTS releases, although #Oracle has also grandfathered in Java 8 as an LTS release to help bridge the gap between the old release model and the new approach.") + */ + } + + "GET /documents" should { + "return HTTP status OK (200)" in { + val result = route(app, FakeRequest(GET, "/documents")).get + status(result) mustBe OK + } + + "return JSON" in { + val result = route(app, FakeRequest(GET, "/documents")).get + contentType(result) mustBe Some("application/json") + } + + "return all documents" in { + val result = route(app, FakeRequest(GET, "/documents")).get + val responseBody = contentAsString(result) + + responseBody must include("Quarkus, a Kubernetes Native Java Framework") + responseBody must include("Java 11 Released") + responseBody mustNot include("Swift 5 Now Officially Available") + } + } + + "GET /documents/:id" should { + "return OK" in { + val result = route(app, FakeRequest(GET, "/documents/1")).get + status(result) mustBe OK + } + + "return JSON" in { + val result = route(app, FakeRequest(GET, "/documents/1")).get + contentType(result) mustBe Some("application/json") + } + + "return only the document requested" in { + val result = route(app, FakeRequest(GET, "/documents/1")).get + val responseBody = contentAsString(result) + + responseBody must include("Quarkus, a Kubernetes Native Java Framework") + responseBody mustNot include("Java 11 Released") + } + + "return all extracted hashtags transformed to lowercase" in { + val result = route(app, FakeRequest(GET, "/documents/1")).get + val responseBody = contentAsString(result) + + responseBody must include(""""hashtags": ["quarkus", "kubernetes", "java"]""") + } + } + + "POST /documents with a valid document" should { + "return HTTP status CREATED (201)" in { + val result = route( + app, + FakeRequest( + POST, + "/documents", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Mashreq Bank’s Lean Agile Journey", + | "body": "After having seen and evidenced the tangible benefit of #lean at Mashreq Bank, #agile was seen as a natural progression, an evolutionary step." + } """ + )).get + status(result) mustBe CREATED + } + + "return JSON" in { + val result = route( + app, + FakeRequest( + POST, + "/documents", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Mashreq Bank’s Lean Agile Journey", + | "body": "After having seen and evidenced the tangible benefit of #lean at Mashreq Bank, #agile was seen as a natural progression, an evolutionary step." + } """ + )).get + contentType(result) mustBe Some("application/json") + } + + "return the new document" in { + val result = route( + app, + FakeRequest( + POST, + "/documents", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Mashreq Bank’s Lean Agile Journey", + | "body": "After having seen and evidenced the tangible benefit of #lean at Mashreq Bank, #agile was seen as a natural progression, an evolutionary step." + } """ + )).get + contentAsString(result) must include("Mashreq Bank’s Lean Agile Journey") + } + + "extract hashtags" in { + val result = route( + app, + FakeRequest( + POST, + "/documents", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Mashreq Bank’s Lean Agile Journey", + | "body": "After having seen and evidenced the tangible benefit of #lean at Mashreq Bank, #agile was seen as a natural progression, an evolutionary step." + } """ + )).get + contentAsString(result) must include(""""hashtags": ["lean", "agile"]""") + } + + "add the document to the database" in { + val result = route( + app, + FakeRequest( + POST, + "/documents", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Mashreq Bank’s Lean Agile Journey", + | "body": "After having seen and evidenced the tangible benefit of #lean at Mashreq Bank, #agile was seen as a natural progression, an evolutionary step." + } """ + )).get + status(result) mustBe CREATED + val indexResult = route(app, FakeRequest(GET, "/documents")).get + contentAsString(indexResult) must include("Mashreq Bank’s Lean Agile Journey") + } + } + + "POST /documents with an invalid document" should { + "return HTTP status BAD REQUEST (400)" in { + val result = route( + app, + FakeRequest( + POST, + "/documents", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Mashreq Bank’s Lean Agile Journey" + } """ + )).get + status(result) mustBe BAD_REQUEST + } + + "not add the document to the database" in { + val result = route( + app, + FakeRequest( + POST, + "/documents", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Mashreq Bank’s Lean Agile Journey" + } """ + )).get + status(result) mustBe BAD_REQUEST + val indexResult = route(app, FakeRequest(GET, "/documents")).get + contentAsString(indexResult) mustNot include("Mashreq Bank’s Lean Agile Journey") + } + } + + "PUT /documents/:id with a valid document" should { + "return HTTP status ACCEPTED (202)" in { + val result = route( + app, + FakeRequest( + PUT, + "/documents/1", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Quarkus, an awesome Kubernetes Native Java Framework", + | "body": "Quarkus is fast and simply #awesome." + } """ + )).get + status(result) mustBe ACCEPTED + } + + "return JSON" in { + val result = route( + app, + FakeRequest( + PUT, + "/documents/1", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Quarkus, an awesome Kubernetes Native Java Framework", + | "body": "Quarkus is fast and simply #awesome." + } """ + )).get + contentType(result) mustBe Some("application/json") + } + + "return the updated document" in { + val result = route( + app, + FakeRequest( + PUT, + "/documents/1", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Quarkus, an awesome Kubernetes Native Java Framework", + | "body": "Quarkus is fast and simply #awesome." + } """ + )).get + contentAsString(result) must include("Quarkus, an awesome Kubernetes Native Java Framework") + } + + "change the document in the database" in { + val result = route( + app, + FakeRequest( + PUT, + "/documents/1", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Quarkus, an awesome Kubernetes Native Java Framework", + | "body": "Quarkus is fast and simply #awesome." + } """ + )).get + status(result) mustBe ACCEPTED + + val indexResult = route(app, FakeRequest(GET, "/documents")).get + val responseBody = contentAsString(indexResult) + + responseBody must include("Quarkus, an awesome Kubernetes Native Java Framework") + responseBody mustNot include("Quarkus, a Kubernetes Native Java Framework") + } + } + + "PUT /documents/:id with an invalid document" should { + "return HTTP status BAD REQUEST (400)" in { + val result = route( + app, + FakeRequest( + PUT, + "/documents/1", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Quarkus, an awesome Kubernetes Native Java Framework" + } """ + )).get + status(result) mustBe BAD_REQUEST + } + + "not update the database" in { + val result = route( + app, + FakeRequest( + PUT, + "/documents/1", + FakeHeaders(List("HOST"->"localhost", "Content-type"->"application/json")), + """ { + | "title": "Quarkus, an awesome Kubernetes Native Java Framework" + } """ + )).get + status(result) mustBe BAD_REQUEST + + val indexResult = route(app, FakeRequest(GET, "/documents")).get + val responseBody = contentAsString(indexResult) + + responseBody mustNot include("Quarkus, an awesome Kubernetes Native Java Framework") + responseBody must include("Quarkus, a Kubernetes Native Java Framework") + } + } + + "DELETE /documents/:id" should { + "return HTTP status ACCEPTED (202)" in { + val result = route(app, FakeRequest(DELETE, "/documents/1")).get + status(result) mustBe ACCEPTED + } + + "delete the given document from the database" in { + val result = route(app, FakeRequest(DELETE, "/documents/1")).get + status(result) mustBe ACCEPTED + val indexResult = route(app, FakeRequest(GET, "/documents")).get + val responseBody = contentAsString(indexResult) + + responseBody mustNot include("Quarkus") + } + } +}