From 0051ea8db660d8618e24068a9f3578384ab2c3c6 Mon Sep 17 00:00:00 2001 From: kijuky <40358+kijuky@users.noreply.github.com> Date: Fri, 3 Nov 2023 08:19:53 +0900 Subject: [PATCH] Update for play 2.9 (#13) Co-authored-by: Kizuki Yasue --- .github/workflows/test.yml | 9 +- .scalafmt.conf | 3 + README.md | 8 + build.sbt | 36 ++-- project/build.properties | 2 +- project/plugins.sbt | 6 +- sample/app/controllers/Application.scala | 39 ++--- src/main/scala/com/beachape/play/Csv.scala | 157 ++++++++++-------- .../scala/com/beachape/play/CsvSpec.scala | 46 ++++- 9 files changed, 179 insertions(+), 127 deletions(-) create mode 100644 .scalafmt.conf diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 42a5a52..69bb2cf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,18 +8,21 @@ jobs: strategy: matrix: java_version: [ - 8, - 11 + 11, + 17, + 21 ] steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Scala uses: actions/setup-java@v3 with: distribution: temurin java-version: ${{ matrix.java_version }} cache: sbt + - name: Format + run: sbt scalafmtAll sample/scalafmtAll scalafmtSbt - name: Test and coverage run: sbt clean sample/compile coverage +test; sbt coverageReport - name: Archive coverage diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..e3733c5 --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,3 @@ +version = 3.7.15 +preset = IntelliJ +runner.dialect = scala213 diff --git a/README.md b/README.md index 1b46ff3..3e94979 100755 --- a/README.md +++ b/README.md @@ -11,6 +11,14 @@ You know why you want it: ## SBT +### for play 2.9.x + +```scala +libraryDependencies ++= Seq( + "com.beachape" %% "play-csv" % "1.7" +) +``` + ### for play 2.8.x ```scala diff --git a/build.sbt b/build.sbt index 72903e8..4e40ff1 100644 --- a/build.sbt +++ b/build.sbt @@ -1,20 +1,17 @@ -import scalariform.formatter.preferences._ -import com.typesafe.sbt.SbtScalariform.ScalariformKeys - lazy val theVersion = "1.7-SNAPSHOT" -lazy val theScalaVersion = "2.13.8" +lazy val theScalaVersion = "2.13.12" lazy val root = Project(id = "root", base = file(".")) .settings(commonWithPublishSettings) .settings( name := "play-csv", - crossScalaVersions := Seq("2.13.8", "2.12.15"), + crossScalaVersions := Seq("2.13.12", "3.3.1"), crossVersion := CrossVersion.binary, libraryDependencies ++= Seq( - "org.apache.commons" % "commons-lang3" % "3.12.0", - "org.apache.commons" % "commons-text" % "1.10.0", - "com.typesafe.play" %% "play" % "2.8.19" % Provided, - "org.scalatest" %% "scalatest" % "3.2.15" % Test + "org.apache.commons" % "commons-lang3" % "3.13.0", + "org.apache.commons" % "commons-text" % "1.11.0", + "com.typesafe.play" %% "play" % "2.9.0" % Provided, + "org.scalatest" %% "scalatest" % "3.2.17" % Test ) ) @@ -24,7 +21,8 @@ lazy val sample = Project(id = "sample", base = file("sample")) .settings( libraryDependencies += guice, routesImport += "com.beachape.play.Csv" - ).dependsOn(root) + ) + .dependsOn(root) lazy val commonWithPublishSettings = commonSettings ++ @@ -36,18 +34,16 @@ lazy val commonSettings = Seq( scalaVersion := theScalaVersion, Test / testOptions += Tests.Argument("-oF") ) ++ - scalariformSettings(true) ++ - formatterPrefs ++ compilerSettings -lazy val formatterPrefs = Seq( - ScalariformKeys.preferences := ScalariformKeys.preferences.value - .setPreference(AlignParameters, true) - .setPreference(DoubleIndentConstructorArguments, true) -) - lazy val compilerSettings = Seq( - scalacOptions ++= Seq("-unchecked", "-deprecation", "-feature", "-Xlint", "-Xlog-free-terms") + scalacOptions ++= Seq( + "-unchecked", + "-deprecation", + "-feature", + "-Xlint", + "-Xlog-free-terms" + ) ) lazy val publishSettings = Seq( @@ -76,7 +72,7 @@ lazy val publishSettings = Seq( if (v.trim.endsWith("SNAPSHOT")) Some("snapshots" at s"${nexus}content/repositories/snapshots") else - Some("releases" at s"${nexus}service/local/staging/deploy/maven2") + Some("releases" at s"${nexus}service/local/staging/deploy/maven2") }.value, publishMavenStyle := true, Test / publishArtifact := false, diff --git a/project/build.properties b/project/build.properties index 563a014..e8a1e24 100755 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.7.2 +sbt.version=1.9.7 diff --git a/project/plugins.sbt b/project/plugins.sbt index bb17d24..26ec2a8 100755 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,8 +1,8 @@ // The Play plugin -addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.19") +addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.9.0") addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") -addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.8.0") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.2") -addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "1.9.3") +addSbtPlugin("org.scoverage" %% "sbt-scoverage" % "2.0.9") diff --git a/sample/app/controllers/Application.scala b/sample/app/controllers/Application.scala index 998e25f..e42dc6b 100644 --- a/sample/app/controllers/Application.scala +++ b/sample/app/controllers/Application.scala @@ -4,18 +4,19 @@ import com.beachape.play.Csv import play.api.data.Form import play.api.data.Forms._ import play.api.i18n.I18nSupport -import play.api.mvc.{ AbstractController, ControllerComponents } +import play.api.mvc.{AbstractController, ControllerComponents} import play.twirl.api.Html import javax.inject.Inject -class Application @Inject() (cc: ControllerComponents) extends AbstractController(cc) with I18nSupport { +class Application @Inject() (cc: ControllerComponents) + extends AbstractController(cc) + with I18nSupport { val form = Form("ids" -> Csv.mapping(number)) def queryParams(ids: Csv[Int]) = Action { - Ok(Html( - s""" + Ok(Html(s""" | | |

Query Params

@@ -26,8 +27,7 @@ class Application @Inject() (cc: ControllerComponents) extends AbstractControlle } def pathParams(ids: Csv[Int]) = Action { - Ok(Html( - s""" + Ok(Html(s""" | | |

Path Params

@@ -39,19 +39,15 @@ class Application @Inject() (cc: ControllerComponents) extends AbstractControlle def showForm = Action { implicit r => import views.html.helper - Ok(Html( - s""" + Ok(Html(s""" | | - | ${ - helper.form(controllers.routes.Application.postForm) { - Html( - s""" + | ${helper.form(controllers.routes.Application.postForm) { + Html(s""" |${helper.CSRF.formField} |${helper.inputText(form("ids"))} |""".stripMargin) - } - } + }} | | | @@ -59,24 +55,25 @@ class Application @Inject() (cc: ControllerComponents) extends AbstractControlle } def postForm = Action { implicit r => - form.bindFromRequest().fold( - _ => BadRequest(Html( - """ + form + .bindFromRequest() + .fold( + _ => BadRequest(Html(""" | | |

Something went wrong with binding the CSV

| | """.stripMargin)), - data => Ok(Html( - s""" + data => Ok(Html(s""" | | |

Bound

|

${data.toSeq.mkString(",")}

| | - """.stripMargin))) + """.stripMargin)) + ) } -} \ No newline at end of file +} diff --git a/src/main/scala/com/beachape/play/Csv.scala b/src/main/scala/com/beachape/play/Csv.scala index b52b71b..506c3b0 100644 --- a/src/main/scala/com/beachape/play/Csv.scala +++ b/src/main/scala/com/beachape/play/Csv.scala @@ -2,48 +2,48 @@ package com.beachape.play import org.apache.commons.lang3.StringUtils import org.apache.commons.text.StringEscapeUtils -import play.api.data.format.{ Formatter, Formats } -import play.api.data.{ Forms, FormError, Mapping } -import play.api.mvc.{ PathBindable, QueryStringBindable } +import play.api.data.format.{Formatter, Formats} +import play.api.data.{Forms, FormError, Mapping} +import play.api.mvc.{PathBindable, QueryStringBindable} -import scala.util.{ Success, Try } +import scala.util.{Success, Try} -/** - * For binding CSV query params without stomping on binding typeclasses for [[Seq]] - */ +/** For binding CSV query params without stomping on binding typeclasses for + * [[Seq]] + */ case class Csv[+A](toSeq: A*) -/** - * Companion object for [[Csv]] that holds useful implicits and helper methods - */ +/** Companion object for [[Csv]] that holds useful implicits and helper methods + */ object Csv { - import StringEscapeUtils.{ escapeCsv, unescapeCsv } - import StringUtils.{ trim, removeStart, split } + import StringEscapeUtils.{escapeCsv, unescapeCsv} + import StringUtils.{trim, removeStart, split} - /** - * Empty [[Csv]] - */ + /** Empty [[Csv]] */ val Empty = Csv[Nothing]() - /** - * Given a mapping for a Play Form, returns one that works with [[Csv]] - * - * Pretty useless..just stick with [[seq]] unless if you really want to have a [[Csv]] - * - * Example: - * {{{ - * Form("hello" -> Csv.mapping(number)) - * }}} - */ - def mapping[A](mapping: Mapping[A]): Mapping[Csv[A]] = Forms.of(formatter(mapping)) - - /** - * Implicit for binding [[Csv]] from query params - */ - implicit def queryStringBindable[A: QueryStringBindable]: QueryStringBindable[Csv[A]] = new QueryStringBindable[Csv[A]] { - - def bind(key: String, params: Map[String, Seq[String]]): Option[Either[String, Csv[A]]] = { + /** Given a mapping for a Play Form, returns one that works with [[Csv]] + * + * Pretty useless..just stick with [[seq]] unless if you really want to have + * a [[Csv]] + * + * Example: + * {{{ + * Form("hello" -> Csv.mapping(number)) + * }}} + */ + def mapping[A](mapping: Mapping[A]): Mapping[Csv[A]] = + Forms.of(formatter(mapping)) + + /** Implicit for binding [[Csv]] from query params */ + implicit def queryStringBindable[A: QueryStringBindable] + : QueryStringBindable[Csv[A]] = new QueryStringBindable[Csv[A]] { + + def bind( + key: String, + params: Map[String, Seq[String]] + ): Option[Either[String, Csv[A]]] = { if (params.get(key).isEmpty) { None } else { @@ -52,10 +52,13 @@ object Csv { strings <- params.get(key).toSeq string <- strings rawValue <- split(string, ',') - bound <- implicitly[QueryStringBindable[A]].bind(key, Map(key -> Seq(unescapeCsv(trim(rawValue))))) + bound <- implicitly[QueryStringBindable[A]] + .bind(key, Map(key -> Seq(unescapeCsv(trim(rawValue))))) } yield bound } - Some(transformOrElse(s"Failed to bind all of ${params.get(key)}")(tryBinds)) + Some( + transformOrElse(s"Failed to bind all of ${params.get(key)}")(tryBinds) + ) } } @@ -68,52 +71,66 @@ object Csv { } } - /** - * Implicit for binding [[Csv]] from path params - */ - implicit def pathStringBindable[A: PathBindable]: PathBindable[Csv[A]] = new PathBindable[Csv[A]] { + /** Implicit for binding [[Csv]] from path params */ + implicit def pathStringBindable[A: PathBindable]: PathBindable[Csv[A]] = + new PathBindable[Csv[A]] { - def bind(key: String, value: String): Either[String, Csv[A]] = { - val tryBinds = Try { split(value, ',').toSeq map (raw => implicitly[PathBindable[A]].bind(key, unescapeCsv(trim(raw)))) } - transformOrElse(s"Could not bind $value into a Csv")(tryBinds) - } + def bind(key: String, value: String): Either[String, Csv[A]] = { + val tryBinds = Try { + split(value, ',').toSeq map (raw => + implicitly[PathBindable[A]].bind(key, unescapeCsv(trim(raw))) + ) + } + transformOrElse(s"Could not bind $value into a Csv")(tryBinds) + } - def unbind(key: String, value: Csv[A]): String = { - val elemStrings = value.toSeq.map { v => - escapeCsv(implicitly[PathBindable[A]].unbind(key, v)) + def unbind(key: String, value: Csv[A]): String = { + val elemStrings = value.toSeq.map { v => + escapeCsv(implicitly[PathBindable[A]].unbind(key, v)) + } + elemStrings.mkString(",") } - elemStrings.mkString(",") } - } - - private[this] def formatter[A](mapping: Mapping[A]): Formatter[Csv[A]] = new Formatter[Csv[A]] { - def bind(key: String, data: Map[String, String]): Either[Seq[FormError], Csv[A]] = { - val elemBinder = mapping.withPrefix(key) - val tryEitherSeqEitherBinds = Try { - Formats.stringFormat.bind(key, data).map { s => - split(s, ',').toSeq map { p => elemBinder.bind(Map(key -> unescapeCsv(trim(p)))) } + private[this] def formatter[A](mapping: Mapping[A]): Formatter[Csv[A]] = + new Formatter[Csv[A]] { + + def bind( + key: String, + data: Map[String, String] + ): Either[Seq[FormError], Csv[A]] = { + val elemBinder = mapping.withPrefix(key) + val tryEitherSeqEitherBinds = Try { + Formats.stringFormat.bind(key, data).map { s => + split(s, ',').toSeq map { p => + elemBinder.bind(Map(key -> unescapeCsv(trim(p)))) + } + } } - } - tryEitherSeqEitherBinds match { - case Success(Right(seqEitherBinds)) if seqEitherBinds.forall(_.isRight) => { - transformOrElse(Seq(FormError(key, "Could not bind Csv", Nil)))(Success(seqEitherBinds)) + tryEitherSeqEitherBinds match { + case Success(Right(seqEitherBinds)) + if seqEitherBinds.forall(_.isRight) => { + transformOrElse(Seq(FormError(key, "Could not bind Csv", Nil)))( + Success(seqEitherBinds) + ) + } + case _ => Left(Seq(FormError(key, "Could not bind Csv", Nil))) } - case _ => Left(Seq(FormError(key, "Could not bind Csv", Nil))) } - } - def unbind(key: String, value: Csv[A]): Map[String, String] = { - val elemStrings = for { - v <- value.toSeq - vString <- mapping.unbind(v).values - } yield escapeCsv(vString) - Map(key -> elemStrings.mkString(",")) + def unbind(key: String, value: Csv[A]): Map[String, String] = { + val elemStrings = for { + v <- value.toSeq + vString <- mapping.unbind(v).values + } yield escapeCsv(vString) + Map(key -> elemStrings.mkString(",")) + } } - } // The orElse comes first so we can let the compiler infer types - private[this] def transformOrElse[A, B](orElse: => A)(tryBinds: Try[Seq[Either[A, B]]]): Either[A, Csv[B]] = { + private[this] def transformOrElse[A, B]( + orElse: => A + )(tryBinds: Try[Seq[Either[A, B]]]): Either[A, Csv[B]] = { tryBinds match { case Success(seqEithers) if seqEithers.forall(_.isRight) => { val seq = for { @@ -126,4 +143,4 @@ object Csv { } } -} \ No newline at end of file +} diff --git a/src/test/scala/com/beachape/play/CsvSpec.scala b/src/test/scala/com/beachape/play/CsvSpec.scala index 5b3e2f5..6b49286 100644 --- a/src/test/scala/com/beachape/play/CsvSpec.scala +++ b/src/test/scala/com/beachape/play/CsvSpec.scala @@ -13,13 +13,31 @@ class CsvSpec extends AnyFunSpec with Matchers { val subject = Csv.queryStringBindable[Int] - it("should create a binder that can bind strings corresponding the proper type") { - subject.bind("hello", Map("hello" -> Seq("1,2,3"))).value.value shouldBe Csv(1, 2, 3) - subject.bind("hello", Map("hello" -> Seq("1"))).value.value shouldBe Csv(1) + it( + "should create a binder that can bind strings corresponding the proper type" + ) { + subject + .bind("hello", Map("hello" -> Seq("1,2,3"))) + .value + .value shouldBe Csv(1, 2, 3) + subject.bind("hello", Map("hello" -> Seq("1"))).value.value shouldBe Csv( + 1 + ) } - it("should create a binder that cannot bind strings that don't correspond to the proper type") { - subject.bind("hello", Map("hello" -> Seq("this is the song that never ends, yes 1t goes on and on my friend"))).value should be(Symbol("left")) + it( + "should create a binder that cannot bind strings that don't correspond to the proper type" + ) { + subject + .bind( + "hello", + Map( + "hello" -> Seq( + "this is the song that never ends, yes 1t goes on and on my friend" + ) + ) + ) + .value should be(Symbol("left")) subject.bind("hello", Map("helloz" -> Seq("1.2, 3.4"))) shouldBe None } @@ -34,13 +52,22 @@ class CsvSpec extends AnyFunSpec with Matchers { val subject = Csv.pathStringBindable[Int] - it("should create a binder that can bind strings corresponding the proper type") { + it( + "should create a binder that can bind strings corresponding the proper type" + ) { subject.bind("hello", "1,2,3").value shouldBe Csv(1, 2, 3) subject.bind("hello", "1").value shouldBe Csv(1) } - it("should create a binder that cannot bind strings that don't correspond to the proper type") { - subject.bind("hello", "this is the song that never ends, yes 1t goes on and on my friend").isLeft shouldBe true + it( + "should create a binder that cannot bind strings that don't correspond to the proper type" + ) { + subject + .bind( + "hello", + "this is the song that never ends, yes 1t goes on and on my friend" + ) + .isLeft shouldBe true subject.bind("hello", "1.2, 3.4").isLeft shouldBe true } @@ -63,7 +90,8 @@ class CsvSpec extends AnyFunSpec with Matchers { val r = Seq( subject.bind(Map("hello" -> "AARSE")).value, subject.bind(Map("hello" -> "1,A,B")).value, - subject.bind(Map("hello" -> "99.9, 3, 33")).value) + subject.bind(Map("hello" -> "99.9, 3, 33")).value + ) r.forall(_ == None) shouldBe true }