diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 83bc17d3..e1f89f4f 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -28,7 +28,7 @@ jobs: uses: playframework/.github/.github/workflows/cmd.yml@v2 with: java: 11, 8 - scala: 2.11.12, 2.12.16, 2.13.8, 3.1.3 + scala: 2.11.12, 2.12.16, 2.13.8 # TODO: 3.1.3 (other play modules not ok) cmd: sbt ++$MATRIX_SCALA docs/test tests: @@ -41,7 +41,7 @@ jobs: with: java: 11, 8 scala: 2.11.12, 2.12.16, 2.13.8, 3.1.3 - cmd: sbt ++$MATRIX_SCALA publishLocal test # scapegoat + cmd: sbt ++$MATRIX_SCALA publishLocal test finish: name: Finish diff --git a/akka/src/test/scala/anorm/AkkaStreamSpec.scala b/akka/src/test/scala/anorm/AkkaStreamSpec.scala index ed77f777..daa61aca 100644 --- a/akka/src/test/scala/anorm/AkkaStreamSpec.scala +++ b/akka/src/test/scala/anorm/AkkaStreamSpec.scala @@ -66,8 +66,9 @@ final class AkkaStreamSpec(implicit ee: ExecutionEnv) extends org.specs2.mutable "on success" in assertAllStagesStopped { withQueryResult(stringList :+ "A" :+ "B" :+ "C") { implicit con => - runAsync(Sink.seq[String]) must beLike[ResultSet] { case rs => - (rs.isClosed must beTrue).and(rs.getStatement.isClosed must beTrue).and(con.isClosed must beFalse) + runAsync(Sink.seq[String]) must beLike[ResultSet] { + case rs => + (rs.isClosed must beTrue).and(rs.getStatement.isClosed must beTrue).and(con.isClosed must beFalse) }.await(0, 5.seconds) } } diff --git a/build.sbt b/build.sbt index a9ca90ea..3958e7d9 100644 --- a/build.sbt +++ b/build.sbt @@ -7,11 +7,10 @@ import com.typesafe.tools.mima.plugin.MimaKeys.{ mimaBinaryIssueFilters, mimaPre // Scalafix inThisBuild( List( - //scalaVersion := "2.13.3", + // scalaVersion := "2.13.3", semanticdbEnabled := true, semanticdbVersion := scalafixSemanticdb.revision, - scalafixDependencies ++= Seq( - "com.github.liancheng" %% "organize-imports" % "0.5.0") + scalafixDependencies ++= Seq("com.github.liancheng" %% "organize-imports" % "0.5.0") ) ) @@ -19,7 +18,7 @@ val specs2Test = Seq( "specs2-core", "specs2-junit", "specs2-matcher-extra" -).map("org.specs2" %% _ % "4.10.6" % Test cross(CrossVersion.for3Use2_13)) +).map("org.specs2" %% _ % "4.10.6" % Test cross (CrossVersion.for3Use2_13)) lazy val acolyteVersion = "1.2.1" lazy val acolyte = "org.eu.acolyte" %% "jdbc-scala" % acolyteVersion % Test @@ -50,9 +49,8 @@ lazy val `anorm-tokenizer` = project // --- val armShading = Seq( - libraryDependencies += ("com.jsuereth" %% "scala-arm" % "2.1-SNAPSHOT"). - cross(CrossVersion.for3Use2_13), - assembly / test := {}, + libraryDependencies += ("com.jsuereth" %% "scala-arm" % "2.1-SNAPSHOT").cross(CrossVersion.for3Use2_13), + assembly / test := {}, assembly / assemblyOption ~= { _.withIncludeScala(false) // java libraries shouldn't include scala }, @@ -109,6 +107,14 @@ lazy val coreMimaFilter: ProblemFilter = { case _ => true } +lazy val xmlVer = Def.setting[String] { + if (scalaBinaryVersion.value == "2.11") { + "1.3.0" + } else { + "2.1.0" + } +} + lazy val `anorm-core` = project .in(file("core")) .settings( @@ -120,7 +126,9 @@ lazy val `anorm-core` = project scalacOptions ++= { if (scalaBinaryVersion.value == "3") { Seq.empty - Seq("-Wconf:cat=deprecation&msg=.*(reflectiveSelectableFromLangReflectiveCalls|DeprecatedSqlParser|missing .*ToSql).*:s") + Seq( + "-Wconf:cat=deprecation&msg=.*(reflectiveSelectableFromLangReflectiveCalls|DeprecatedSqlParser|missing .*ToSql).*:s" + ) } else { Seq( "-Xlog-free-terms", @@ -169,6 +177,7 @@ lazy val `anorm-core` = project ProblemFilters.exclude[DirectMissingMethodProblem]( // deprecated 2.3.8 "anorm.SqlQuery.statement" ), + incoRet("anorm.ParameterValue.apply"), // private: ProblemFilters.exclude[DirectMissingMethodProblem]( // private "anorm.Sql.asTry" @@ -183,8 +192,8 @@ lazy val `anorm-core` = project "joda-time" % "joda-time" % "2.11.0", "org.joda" % "joda-convert" % "2.2.2", "org.scala-lang.modules" %% "scala-parser-combinators" % parserCombinatorsVer.value, - "org.scala-lang.modules" %% "scala-xml" % "2.1.0" % Test, - "com.h2database" % "h2" % "2.1.214" % Test, + "org.scala-lang.modules" %% "scala-xml" % xmlVer.value % Test, + "com.h2database" % "h2" % "2.1.214" % Test, acolyte ) ++ specs2Test.map(_.exclude("org.scala-lang.modules", "*")), ) ++ armShading @@ -321,6 +330,6 @@ addCommandAlias( "validateCode", List( "scalafmtSbtCheck", - "scalafmtCheckAll", + "+scalafmtCheckAll", ).mkString(";") ) diff --git a/core/src/main/scala-2/anorm/Macro.scala b/core/src/main/scala-2/anorm/Macro.scala index 4ceabc3a..1fd95b8e 100644 --- a/core/src/main/scala-2/anorm/Macro.scala +++ b/core/src/main/scala-2/anorm/Macro.scala @@ -66,7 +66,10 @@ object Macro extends MacroOptions { def psz = params.size if (names.size < psz) { - c.abort(c.enclosingPosition, s"no column name for parameters: ${show(names)} < $params") + c.abort( + c.enclosingPosition, + s"no column name for parameters: ${names.map(n => show(n)).mkString(", ")} < ${params.map(_.name).mkString(", ")}" + ) } else { parserImpl[T](c) { (t, _, i) => diff --git a/core/src/main/scala-2/anorm/macros/SealedRowParserImpl.scala b/core/src/main/scala-2/anorm/macros/SealedRowParserImpl.scala index 6f5ab01e..eaaae489 100644 --- a/core/src/main/scala-2/anorm/macros/SealedRowParserImpl.scala +++ b/core/src/main/scala-2/anorm/macros/SealedRowParserImpl.scala @@ -57,12 +57,8 @@ private[anorm] object SealedRowParserImpl { val caseName = TermName(c.freshName("discriminated")) val key = q"$discriminate(${subclass.typeSymbol.fullName})" val caseDecl = q"val $caseName = $key" - val subtype = { // TODO: typeParams is not supported anyway - if (subclass.typeSymbol.asClass.typeParams.isEmpty) subclass - else subclass.erasure - } - (key, caseDecl, cq"`$caseName` => implicitly[anorm.RowParser[$subtype]]") + (key, caseDecl, cq"`$caseName` => implicitly[anorm.RowParser[$subclass]]") } lazy val supported = q"List(..${cases.map(_._1)})" diff --git a/core/src/main/scala-2/anorm/macros/ToParameterListImpl.scala b/core/src/main/scala-2/anorm/macros/ToParameterListImpl.scala index cf772ebf..ba92f526 100644 --- a/core/src/main/scala-2/anorm/macros/ToParameterListImpl.scala +++ b/core/src/main/scala-2/anorm/macros/ToParameterListImpl.scala @@ -28,10 +28,9 @@ private[anorm] object ToParameterListImpl { c.inferImplicitValue(ptype) match { case EmptyTree => - c.abort( - c.enclosingPosition, s"Missing ToParameterList[${subcls}]") + c.abort(c.enclosingPosition, s"Missing ToParameterList[${subcls}]") - case toParams => + case toParams => cq"v: ${subcls} => $toParams(v)" } } diff --git a/core/src/main/scala-3/anorm/Macro.scala b/core/src/main/scala-3/anorm/Macro.scala index 98deab51..bfd8fde4 100644 --- a/core/src/main/scala-3/anorm/Macro.scala +++ b/core/src/main/scala-3/anorm/Macro.scala @@ -265,21 +265,28 @@ object Macro extends MacroOptions with macros.ValueColumn with macros.ValueToSta // --- - private def namedParserImpl[T](using Quotes, Type[T]): Expr[RowParser[T]] = - withColumn[T] { col => - parserImpl[T] { (n, _) => - '{ anorm.SqlParser.get[T](${ Expr(n) })($col) } + private def namedParserImpl[T](using q: Quotes, tpe: Type[T]): Expr[RowParser[T]] = + parserImpl[T](q) { (tpr, n, _) => + tpr.asType match { + case '[t] => + withColumn[t] { col => + '{ SqlParser.get[t](${ Expr(n) })($col) } + } } } private def namedParserImpl1[T]( naming: Expr[ColumnNaming] - )(using Quotes, Type[T], Type[ColumnNaming]): Expr[RowParser[T]] = - withColumn[T] { col => - parserImpl[T] { (n, _) => - '{ anorm.SqlParser.get[T]($naming(${ Expr(n) }))($col) } + )(using q: Quotes, tpe: Type[T], colNme: Type[ColumnNaming]): Expr[RowParser[T]] = { + parserImpl[T](q) { (tpr, n, _) => + tpr.asType match { + case '[t] => + withColumn[t] { col => + '{ SqlParser.get[t]($naming(${ Expr(n) }))($col) } + } } } + } private def namedParserImpl2[T]( naming: Expr[ColumnNaming], @@ -316,13 +323,16 @@ object Macro extends MacroOptions with macros.ValueColumn with macros.ValueToSta report.errorAndAbort(s"no column name for parameters: ${ns.mkString(", ")} < $params") } else { - parserImpl[T] { (_, i) => + parserImpl[T](q) { (tpr, _, i) => ns.lift(i) match { case Some(n) => - withColumn[T] { col => - val cn = naming(Expr(n)) + tpr.asType match { + case '[t] => + withColumn[t] { col => + val cn = naming(Expr(n)) - '{ SqlParser.get[T]($cn)($col) } + '{ SqlParser.get[t]($cn)($col) } + } } case _ => @@ -332,10 +342,13 @@ object Macro extends MacroOptions with macros.ValueColumn with macros.ValueToSta } } - private def offsetParserImpl[T](offset: Expr[Int])(using Quotes, Type[T]): Expr[RowParser[T]] = - withColumn[T] { col => - parserImpl[T] { (_, i) => - '{ anorm.SqlParser.get[T]($offset + ${ Expr(i + 1) })($col) } + private def offsetParserImpl[T](offset: Expr[Int])(using q: Quotes, tpe: Type[T]): Expr[RowParser[T]] = + parserImpl[T](q) { (tpr, _, i) => + tpr.asType match { + case '[t] => + withColumn[t] { col => + '{ SqlParser.get[t]($offset + ${ Expr(i + 1) })($col) } + } } } @@ -361,9 +374,22 @@ object Macro extends MacroOptions with macros.ValueColumn with macros.ValueToSta ): Expr[RowParser[T]] = macros.SealedRowParserImpl[T](naming, discriminate) - private def parserImpl[T](genGet: (String, Int) => Expr[RowParser[T]])(using Quotes, Type[T]): Expr[RowParser[T]] = { - // TODO: anorm.macros.RowParserImpl[T](c)(genGet) - '{ ??? } + inline private def withParser[T](f: RowParser[T] => (Row => SqlResult[T])): RowParser[T] = new RowParser[T] { self => + lazy val underlying = f(self) + + def apply(row: Row): SqlResult[T] = underlying(row) + } + + private def parserImpl[T]( + q: Quotes + )(genGet: (q.reflect.TypeRepr, String, Int) => Expr[RowParser[_]])(using Type[T]): Expr[RowParser[T]] = { + given quotes: Quotes = q + + '{ + withParser { self => + ${ macros.RowParserImpl[T](q, 'self)(genGet) } + } + } } // --- @@ -516,7 +542,7 @@ object Macro extends MacroOptions with macros.ValueColumn with macros.ValueToSta } } - inline private[anorm] def withSelfToParameterList[T]( + inline private def withSelfToParameterList[T]( f: ToParameterList[T] => (T => List[NamedParameter]) ): ToParameterList[T] = new ToParameterList[T] { self => lazy val underlying = f(self) diff --git a/core/src/main/scala-3/anorm/macros/RowParserImpl.scala b/core/src/main/scala-3/anorm/macros/RowParserImpl.scala new file mode 100644 index 00000000..80b07434 --- /dev/null +++ b/core/src/main/scala-3/anorm/macros/RowParserImpl.scala @@ -0,0 +1,200 @@ +package anorm.macros + +import scala.quoted.{ Expr, Quotes, Type } + +import anorm.{ Column, Row, RowParser, SqlResult } +import anorm.Macro.debugEnabled + +private[anorm] object RowParserImpl { + def apply[A]( + q: Quotes, + forwardExpr: Expr[RowParser[A]] + )( + genGet: (q.reflect.TypeRepr, String, Int) => Expr[RowParser[_]] + )(using tpe: Type[A], parserTpe: Type[RowParser]): Expr[Row => SqlResult[A]] = { + given quotes: Quotes = q + + import q.reflect.* + + val (repr, aTArgs) = TypeRepr.of[A](using tpe) match { + case tpr @ AppliedType(_, args) => + tpr -> args.collect { + case repr: TypeRepr => + repr + } + + case tpr => + tpr -> List.empty[TypeRepr] + } + + @inline def abort(msg: String) = report.errorAndAbort(msg) + + val tpeSym = repr.typeSymbol + + if (!tpeSym.isClassDef || !tpeSym.flags.is(Flags.Case)) { + abort(s"case class expected: ${repr.show}") + } + + // --- + + val ctor = tpeSym.primaryConstructor + + val (boundTypes, properties) = ctor.paramSymss match { + case targs :: params :: Nil if targs.forall(_.isType) => { + val boundTps = targs.zip(aTArgs).toMap + + boundTps -> params + } + + case params :: Nil => + Map.empty[Symbol, TypeRepr] -> params + + case params :: _ => { + report.info( + s"${repr.show} constructor has multiple list of parameters. As for unapply, only for the first one will be considered" + ) + + Map.empty[Symbol, TypeRepr] -> params + } + + case _ => + report.errorAndAbort(s"${repr.show} constructor has no parameter") + } + + if (properties.isEmpty) { + abort(s"parsed data cannot be passed as parameter: $ctor") + } + + /* TODO + val colTpe = c.weakTypeTag[Column[_]].tpe + val parserTpe = c.weakTypeTag[RowParser[_]].tpe + + val boundTypes: Map[String, Type] = Inspect.boundTypes(c)(tpe) + val forwardName = TermName(c.freshName("forward")) + */ + + val debug = { + if (debugEnabled) report.info(_: String) + else (_: String) => {} + } + + val resolv = ImplicitResolver[A](q).resolver(forwardExpr, Map.empty, debug)(parserTpe) + + /* TODO + + val resolveImplicit: (Name, Type, Type) => Implicit[Type, Name, Tree] = + ImplicitResolver(c)(tpe, boundTypes, forwardName) + + // --- + + val (x, m, body, _, hasSelfRef) = + ctor.paramLists.foldLeft[(Tree, Tree, Tree, Int, Boolean)]((EmptyTree, EmptyTree, EmptyTree, 0, false)) { + case ((xa, ma, bs, ia, sr), pss) => + val (xb, mb, vs, ib, selfRef) = + pss.foldLeft((xa, ma, List.empty[Tree], ia, sr)) { + case ((xtr, mp, ps, pi, sref), term: TermSymbol) => { + val tn = term.name.toString + val tt = { + val t = term.typeSignature + + boundTypes.getOrElse(t.typeSymbol.fullName, t) + // TODO: term.isParamWithDefault + } + + // Try to resolve `Column[tt]` + resolveImplicit(term.name, tt, colTpe) match { + case Implicit.Unresolved() => // No `Column[tt]` ... + // ... try to resolve `RowParser[tt]` + resolveImplicit(term.name, tt, parserTpe) match { + case Implicit.Unresolved() => + abort(s"cannot find $colTpe nor $parserTpe for ${term.name}:$tt in $ctor") + + case Implicit(_, _, pr, _, s) => { + // Use an existing `RowParser[T]` as part + pq"${term.name}" match { + case b @ Bind(bn, _) => { + val bt = q"${bn.toTermName}" + + xtr match { + case EmptyTree => + (pr, b, List[Tree](bt), pi + 1, s || sref) + + case _ => (q"$xtr ~ $pr", pq"anorm.~($mp, $b)", bt :: ps, pi + 1, s || sref) + + } + } + + case _ => + abort(s"unsupported $colTpe nor $parserTpe for ${term.name}:$tt in $ctor") + } + } + } + + case Implicit(_, _, itree, _, _) => { + // Generate a `get` for the `Column[T]` + val get = genGet(tt, tn, pi) + + pq"${term.name}" match { + case b @ Bind(bn, _) => { + val bt = q"${bn.toTermName}" + + xtr match { + case EmptyTree => + (get, b, List[Tree](bt), pi + 1, sref) + + case _ => (q"$xtr ~ $get($itree)", pq"anorm.~($mp, $b)", bt :: ps, pi + 1, sref) + + } + } + + case _ => + abort(s"unsupported $colTpe nor $parserTpe for ${term.name}:$tt: ${show(itree)}") + } + } + } + } + + case (state, sym) => { + c.warning(c.enclosingPosition, s"unexpected symbol: $sym") + state + } + } + + val by = bs match { + case EmptyTree => q"new $tpe(..${vs.reverse})" + case xs => q"$xs(..${vs.reverse})" + } + + (xb, mb, by, ib, selfRef) + } + + val caseDef = cq"$m => { $body }" + val patMat = q"$x.map[$tpe] { _ match { case $caseDef } }" + val parser = + if (!hasSelfRef) patMat + else { + val generated = TypeName(c.freshName("Generated")) + val rowParser = TermName(c.freshName("rowParser")) + + q"""{ + final class $generated() { + val ${forwardName} = + anorm.RowParser[$tpe]($rowParser) + + def $rowParser: anorm.RowParser[$tpe] = $patMat + } + + new $generated().$rowParser + }""" + } + + if (debugEnabled) { + c.echo(c.enclosingPosition, s"row parser generated for $tpe: ${parser.show}") + } + + c.Expr[RowParser[T]](c.typecheck(parser)) + */ + + '{ ??? } + } +} diff --git a/core/src/main/scala-3/anorm/macros/SealedRowParserImpl.scala b/core/src/main/scala-3/anorm/macros/SealedRowParserImpl.scala index e1c21485..5b0e6784 100644 --- a/core/src/main/scala-3/anorm/macros/SealedRowParserImpl.scala +++ b/core/src/main/scala-3/anorm/macros/SealedRowParserImpl.scala @@ -7,8 +7,8 @@ import anorm.{ Error, RowParser, SqlMappingError, SqlParser } private[anorm] object SealedRowParserImpl { def apply[A]( - naming: Expr[DiscriminatorNaming], - discriminate: Expr[Discriminate] + naming: Expr[DiscriminatorNaming], + discriminate: Expr[Discriminate] )(using q: Quotes, tpe: Type[A]): Expr[RowParser[A]] = { import q.reflect.* @@ -48,26 +48,26 @@ private[anorm] object SealedRowParserImpl { } if (missing.nonEmpty) { - def details = missing.map { subcls => + def details = missing + .map { subcls => s"- cannot find anorm.RowParser[${subcls.show}] in the implicit scope" } .mkString(",\r\n") - report.errorAndAbort( - s"fails to generate sealed parser: ${repr.show};\r\n$details") + report.errorAndAbort(s"fails to generate sealed parser: ${repr.show};\r\n$details") } // --- - val cases: List[(String, CaseDef)] = subParsers.result().map { + val cases: List[(String, CaseDef)] = subParsers.result().map { case (subcls, subParser) => val tpeSym = subcls.typeSymbol val tpeName = { - if (tpeSym.flags is Flags.Module) tpeSym.fullName.stripSuffix(f"$$") + if (tpeSym.flags.is(Flags.Module)) tpeSym.fullName.stripSuffix(f"$$") else tpeSym.fullName } - val key = '{ $discriminate(${Expr(tpeName)}) } + val key = '{ $discriminate(${ Expr(tpeName) }) } val bind = Symbol.newBind( @@ -82,7 +82,8 @@ private[anorm] object SealedRowParserImpl { tpeSym.fullName -> CaseDef( Bind(bind, Wildcard()), guard = Some('{ $ref == $key }.asTerm), - rhs = subParser.asTerm) + rhs = subParser.asTerm + ) } def fallbackCase: CaseDef = { @@ -100,8 +101,8 @@ private[anorm] object SealedRowParserImpl { Bind(fallbackBind, Wildcard()), guard = None, rhs = '{ - val msg = "unexpected row type '%s'; expected: %s". - format($fallbackVal, ${Expr(cases.map(_._1))}.mkString(", ")) + val msg = + "unexpected row type '%s'; expected: %s".format($fallbackVal, ${ Expr(cases.map(_._1)) }.mkString(", ")) RowParser.failed[A](Error(SqlMappingError(msg))) }.asTerm @@ -115,7 +116,7 @@ private[anorm] object SealedRowParserImpl { ).asExprOf[RowParser[A]] val parser: Expr[RowParser[A]] = '{ - val discriminatorCol = $naming(${Expr(repr.typeSymbol.fullName)}) + val discriminatorCol = $naming(${ Expr(repr.typeSymbol.fullName) }) SqlParser.str(discriminatorCol).flatMap { (discriminatorVal: String) => ${ body('discriminatorVal) } diff --git a/core/src/main/scala-3/anorm/macros/ToParameterListImpl.scala b/core/src/main/scala-3/anorm/macros/ToParameterListImpl.scala index 621827cc..beffaa28 100644 --- a/core/src/main/scala-3/anorm/macros/ToParameterListImpl.scala +++ b/core/src/main/scala-3/anorm/macros/ToParameterListImpl.scala @@ -92,25 +92,12 @@ private[anorm] object ToParameterListImpl { @inline def abort(msg: String) = report.errorAndAbort(msg) - val debug = { - if (debugEnabled) report.info(_: String) - else (_: String) => {} - } - if (!tpeSym.isClassDef || !tpeSym.flags.is(Flags.Case)) { abort(s"Case class expected: $tpe") } val ctor = tpeSym.primaryConstructor - if (ctor.paramSymss.isEmpty) { - abort("parsed data cannot be passed as constructor parameters") - } - - val resolv = ImplicitResolver[A](q).resolver(forwardExpr, Map.empty, debug)(tsTpe) - - // --- - val (boundTypes, properties) = ctor.paramSymss match { case targs :: params :: Nil if targs.forall(_.isType) => { val boundTps = targs.zip(aTArgs).toMap @@ -133,6 +120,19 @@ private[anorm] object ToParameterListImpl { report.errorAndAbort(s"${aTpr.show} constructor has no parameter") } + if (properties.isEmpty) { + abort("parsed data cannot be passed as constructor parameters") + } + + // --- + + val debug = { + if (debugEnabled) report.info(_: String) + else (_: String) => {} + } + + val resolv = ImplicitResolver[A](q).resolver(forwardExpr, Map.empty, debug)(tsTpe) + val compiledProjection: Seq[ParameterProjection] = { import _root_.anorm.Macro.parameterProjectionFromExpr diff --git a/core/src/main/scala/anorm/Anorm.scala b/core/src/main/scala/anorm/Anorm.scala index c329bb89..f9cad187 100644 --- a/core/src/main/scala/anorm/Anorm.scala +++ b/core/src/main/scala/anorm/Anorm.scala @@ -277,7 +277,6 @@ object Sql { // TODO: Rename to SQL private def toSql(ts: List[StatementToken], buf: StringBuilder): StringBuilder = ts.foldLeft(buf) { case (sql, StringToken(t)) => sql ++= t case (sql, PercentToken) => sql += '%' - // TODO: Remove; case (sql, _) => sql } @SuppressWarnings(Array("IncorrectlyNamedExceptions")) diff --git a/core/src/main/scala/anorm/RowParser.scala b/core/src/main/scala/anorm/RowParser.scala new file mode 100644 index 00000000..1da70904 --- /dev/null +++ b/core/src/main/scala/anorm/RowParser.scala @@ -0,0 +1,180 @@ +package anorm + +trait RowParser[+A] extends (Row => SqlResult[A]) { parent => + + /** + * Returns a parser that will apply given function `f` + * to the result of this first parser. If the current parser is not + * successful, the new one will return encountered [[Error]]. + * + * @param f Function applied on the successful parser result + * + * {{{ + * import anorm.{ RowParser, SqlParser } + * + * val parser: RowParser[Int] = SqlParser.str("col").map(_.length) + * // Prepares a parser that first get 'col' string value, + * // and then returns the length of that + * }}} + */ + def map[B](f: A => B): RowParser[B] = RowParser(parent.andThen(_.map(f))) + + /** + * Returns parser which collects information + * from already parsed row data using `f`. + * + * @param otherwise Message returned as error if nothing can be collected using `f`. + * @param f Collecting function + */ + def collect[B](otherwise: String)(f: PartialFunction[A, B]): RowParser[B] = + RowParser(parent(_).flatMap(f.lift(_).fold[SqlResult[B]](Error(SqlMappingError(otherwise)))(Success(_)))) + + def flatMap[B](k: A => RowParser[B]): RowParser[B] = + RowParser(row => parent(row).flatMap(k(_)(row))) + + /** + * Combines this parser on the left of the parser `p` given as argument. + * + * @param p Parser on the right + * + * {{{ + * import anorm._, SqlParser.{ int, str } + * + * def populations(implicit con: java.sql.Connection): List[String ~ Int] = + * SQL("SELECT * FROM Country").as((str("name") ~ int("population")).*) + * }}} + */ + def ~[B](p: RowParser[B]): RowParser[A ~ B] = + RowParser(row => parent(row).flatMap(a => p(row).map(new ~(a, _)))) + + /** + * Combines this current parser with the one given as argument `p`, + * if and only if the current parser can first/on left side successfully + * parse a row, without keeping these values in parsed result. + * + * {{{ + * import anorm._, SqlParser.{ int, str } + * + * def string(implicit con: java.sql.Connection) = SQL("SELECT * FROM test"). + * as((int("id") ~> str("val")).single) + * // row has to have an int column 'id' and a string 'val' one, + * // keeping only 'val' in result + * }}} + */ + def ~>[B](p: RowParser[B]): RowParser[B] = + RowParser(row => parent(row).flatMap(_ => p(row))) + + /** + * Combines this current parser with the one given as argument `p`, + * if and only if the current parser can first successfully + * parse a row, without keeping the values of the parser `p`. + * + * {{{ + * import anorm._, SqlParser.{ int, str } + * + * def i(implicit con: java.sql.Connection) = SQL("SELECT * FROM test"). + * as((int("id") <~ str("val")).single) + * // row has to have an int column 'id' and a string 'val' one, + * // keeping only 'id' in result + * }}} + */ + def <~[B](p: RowParser[B]): RowParser[A] = parent.~(p).map(_._1) + + // TODO: Scaladoc + def |[B >: A](p: RowParser[B]): RowParser[B] = RowParser { row => + parent(row) match { + case Error(_) => p(row) + case a => a + } + } + + /** + * Returns a row parser for optional column, + * that will turn missing or null column as None. + */ + def ? : RowParser[Option[A]] = RowParser { + parent(_) match { + case Success(a) => Success(Some(a)) + case Error(ColumnNotFound(_, _)) => + Success(None) + + case e @ Error(_) => e + } + } + + /** Alias for [[flatMap]] */ + def >>[B](f: A => RowParser[B]): RowParser[B] = flatMap(f) + + /** + * Returns possibly empty list parsed from result. + * + * {{{ + * import anorm._, SqlParser.scalar + * + * val price = 125 + * + * def foo(implicit con: java.sql.Connection) = + * SQL"SELECT name FROM item WHERE price < \\$price".as(scalar[String].*) + * }}} + */ + def * : ResultSetParser[List[A]] = ResultSetParser.list(parent) + + /** + * Returns non empty list parse from result, + * or raise error if there is no result. + * + * {{{ + * import anorm._, SqlParser.str + * + * def foo(implicit con: java.sql.Connection) = { + * val parser = str("title") ~ str("descr") + * SQL("SELECT title, descr FROM pages").as(parser.+) // at least 1 page + * } + * }}} + */ + def + : ResultSetParser[List[A]] = ResultSetParser.nonEmptyList(parent) + + /** + * Returns a result set parser expecting exactly one row to parse. + * + * {{{ + * import anorm._, SqlParser.scalar + * + * def b(implicit con: java.sql.Connection): Boolean = + * SQL("SELECT flag FROM Test WHERE id = :id"). + * on("id" -> 1).as(scalar[Boolean].single) + * }}} + * + * @see #singleOpt + */ + def single = ResultSetParser.single(parent) + + /** + * Returns a result set parser for none or one parsed row. + * + * {{{ + * import anorm._, SqlParser.scalar + * + * def name(implicit con: java.sql.Connection): Option[String] = + * SQL("SELECT name FROM Country WHERE lang = :lang") + * .on("lang" -> "notFound").as(scalar[String].singleOpt) + * }}} + */ + def singleOpt: ResultSetParser[Option[A]] = ResultSetParser.singleOpt(parent) + +} + +object RowParser { + def apply[A](f: Row => SqlResult[A]): RowParser[A] = new RowParser[A] { + def apply(row: Row): SqlResult[A] = f(row) + } + + /** Row parser that result in successfully unchanged row. */ + object successful extends RowParser[Row] { + def apply(row: Row): SqlResult[Row] = Success(row) + } + + def failed[A](error: => Error): RowParser[A] = new RowParser[A] { + def apply(row: Row): SqlResult[A] = error + } +} diff --git a/core/src/main/scala/anorm/SqlParser.scala b/core/src/main/scala/anorm/SqlParser.scala index dcc65690..472fbda1 100644 --- a/core/src/main/scala/anorm/SqlParser.scala +++ b/core/src/main/scala/anorm/SqlParser.scala @@ -567,185 +567,6 @@ sealed trait DeprecatedSqlParser { _parser: SqlParser.type => @SuppressWarnings(Array("ClassNames")) final case class ~[+A, +B](_1: A, _2: B) -object RowParser { - def apply[A](f: Row => SqlResult[A]): RowParser[A] = new RowParser[A] { - def apply(row: Row): SqlResult[A] = f(row) - } - - /** Row parser that result in successfully unchanged row. */ - object successful extends RowParser[Row] { - def apply(row: Row): SqlResult[Row] = Success(row) - } - - def failed[A](error: => Error): RowParser[A] = new RowParser[A] { - def apply(row: Row): SqlResult[A] = error - } -} - -trait RowParser[+A] extends (Row => SqlResult[A]) { parent => - - /** - * Returns a parser that will apply given function `f` - * to the result of this first parser. If the current parser is not - * successful, the new one will return encountered [[Error]]. - * - * @param f Function applied on the successful parser result - * - * {{{ - * import anorm.{ RowParser, SqlParser } - * - * val parser: RowParser[Int] = SqlParser.str("col").map(_.length) - * // Prepares a parser that first get 'col' string value, - * // and then returns the length of that - * }}} - */ - def map[B](f: A => B): RowParser[B] = RowParser(parent.andThen(_.map(f))) - - /** - * Returns parser which collects information - * from already parsed row data using `f`. - * - * @param otherwise Message returned as error if nothing can be collected using `f`. - * @param f Collecting function - */ - def collect[B](otherwise: String)(f: PartialFunction[A, B]): RowParser[B] = - RowParser(parent(_).flatMap(f.lift(_).fold[SqlResult[B]](Error(SqlMappingError(otherwise)))(Success(_)))) - - def flatMap[B](k: A => RowParser[B]): RowParser[B] = - RowParser(row => parent(row).flatMap(k(_)(row))) - - /** - * Combines this parser on the left of the parser `p` given as argument. - * - * @param p Parser on the right - * - * {{{ - * import anorm._, SqlParser.{ int, str } - * - * def populations(implicit con: java.sql.Connection): List[String ~ Int] = - * SQL("SELECT * FROM Country").as((str("name") ~ int("population")).*) - * }}} - */ - def ~[B](p: RowParser[B]): RowParser[A ~ B] = - RowParser(row => parent(row).flatMap(a => p(row).map(new ~(a, _)))) - - /** - * Combines this current parser with the one given as argument `p`, - * if and only if the current parser can first/on left side successfully - * parse a row, without keeping these values in parsed result. - * - * {{{ - * import anorm._, SqlParser.{ int, str } - * - * def string(implicit con: java.sql.Connection) = SQL("SELECT * FROM test"). - * as((int("id") ~> str("val")).single) - * // row has to have an int column 'id' and a string 'val' one, - * // keeping only 'val' in result - * }}} - */ - def ~>[B](p: RowParser[B]): RowParser[B] = - RowParser(row => parent(row).flatMap(_ => p(row))) - - /** - * Combines this current parser with the one given as argument `p`, - * if and only if the current parser can first successfully - * parse a row, without keeping the values of the parser `p`. - * - * {{{ - * import anorm._, SqlParser.{ int, str } - * - * def i(implicit con: java.sql.Connection) = SQL("SELECT * FROM test"). - * as((int("id") <~ str("val")).single) - * // row has to have an int column 'id' and a string 'val' one, - * // keeping only 'id' in result - * }}} - */ - def <~[B](p: RowParser[B]): RowParser[A] = parent.~(p).map(_._1) - - // TODO: Scaladoc - def |[B >: A](p: RowParser[B]): RowParser[B] = RowParser { row => - parent(row) match { - case Error(_) => p(row) - case a => a - } - } - - /** - * Returns a row parser for optional column, - * that will turn missing or null column as None. - */ - def ? : RowParser[Option[A]] = RowParser { - parent(_) match { - case Success(a) => Success(Some(a)) - case Error(ColumnNotFound(_, _)) => - Success(None) - - case e @ Error(_) => e - } - } - - /** Alias for [[flatMap]] */ - def >>[B](f: A => RowParser[B]): RowParser[B] = flatMap(f) - - /** - * Returns possibly empty list parsed from result. - * - * {{{ - * import anorm._, SqlParser.scalar - * - * val price = 125 - * - * def foo(implicit con: java.sql.Connection) = - * SQL"SELECT name FROM item WHERE price < \\$price".as(scalar[String].*) - * }}} - */ - def * : ResultSetParser[List[A]] = ResultSetParser.list(parent) - - /** - * Returns non empty list parse from result, - * or raise error if there is no result. - * - * {{{ - * import anorm._, SqlParser.str - * - * def foo(implicit con: java.sql.Connection) = { - * val parser = str("title") ~ str("descr") - * SQL("SELECT title, descr FROM pages").as(parser.+) // at least 1 page - * } - * }}} - */ - def + : ResultSetParser[List[A]] = ResultSetParser.nonEmptyList(parent) - - /** - * Returns a result set parser expecting exactly one row to parse. - * - * {{{ - * import anorm._, SqlParser.scalar - * - * def b(implicit con: java.sql.Connection): Boolean = - * SQL("SELECT flag FROM Test WHERE id = :id"). - * on("id" -> 1).as(scalar[Boolean].single) - * }}} - * - * @see #singleOpt - */ - def single = ResultSetParser.single(parent) - - /** - * Returns a result set parser for none or one parsed row. - * - * {{{ - * import anorm._, SqlParser.scalar - * - * def name(implicit con: java.sql.Connection): Option[String] = - * SQL("SELECT name FROM Country WHERE lang = :lang") - * .on("lang" -> "notFound").as(scalar[String].singleOpt) - * }}} - */ - def singleOpt: ResultSetParser[Option[A]] = ResultSetParser.singleOpt(parent) - -} - /** Parser for scalar row (row of one single column). */ sealed trait ScalarRowParser[+A] extends RowParser[A] { override def singleOpt: ResultSetParser[Option[A]] = ResultSetParser { diff --git a/core/src/test/scala/anorm/MacroSpec.scala b/core/src/test/scala/anorm/MacroSpec.scala index 78230fae..035ef82c 100644 --- a/core/src/test/scala/anorm/MacroSpec.scala +++ b/core/src/test/scala/anorm/MacroSpec.scala @@ -8,6 +8,7 @@ import acolyte.jdbc.AcolyteDSL.{ connection, handleStatement, withQueryResult } import acolyte.jdbc.Implicits._ import org.specs2.specification.core.Fragments +import org.specs2.matcher.TypecheckMatchers._ import com.github.ghik.silencer.silent @@ -17,6 +18,8 @@ import SqlParser.scalar final class MacroSpec extends org.specs2.mutable.Specification { "Macro".title + import TestUtils.typecheck + val barRow1 = RowLists.rowList1(classOf[Int] -> "v") val fooRow1 = RowLists.rowList5( @@ -35,7 +38,6 @@ final class MacroSpec extends org.specs2.mutable.Specification { classOf[JBool] -> "x" ) // java types to avoid conv - /* TODO "Column naming" should { import ColumnNaming._ @@ -49,11 +51,17 @@ final class MacroSpec extends org.specs2.mutable.Specification { } "Generated named parser" should { - // No Column[Bar] so compilation error is expected - shapeless.test.illTyped("anorm.Macro.namedParser[Foo[Bar]]") - - // Not enough column names for class parameters - shapeless.test.illTyped("""anorm.Macro.parser[Foo[Int]]("Foo", "Bar")""") + "not be resolved" in { + // No Column[Bar] so compilation error is expected + (typecheck("anorm.Macro.namedParser[Foo[Bar]]") must failWith( + ".*cannot find.* .*Column.* nor .*RowParser.* for .*loremIpsum.*Bar.*" + )).and { + // Not enough column names for class parameters + typecheck("""anorm.Macro.parser[Foo[Int]]("Foo", "Bar")""") must failWith( + ".*no column name for parameters.* .*Foo.* .*Bar.*" + ) + } + } "be successful for Bar" in withQueryResult(barRow1 :+ 1 :+ 3) { implicit c => val parser1 = Macro.namedParser[Bar] @@ -121,15 +129,20 @@ final class MacroSpec extends org.specs2.mutable.Specification { } "support self reference" in { - val _ = Macro.namedParser[Self] // check compile is ok + // check compile is ok + typecheck("Macro.namedParser[Self]") must not(beNull) - ok // TODO: Supports aliasing to make it really usable (see #124) + // TODO: Supports aliasing to make it really usable (see #124) } } "Generated indexed parser" should { - // No Column[Bar] so compilation error is expected - shapeless.test.illTyped("anorm.Macro.indexedParser[Foo[Bar]]") + "not be resolved" in { + // No Column[Bar] so compilation error is expected + typecheck("anorm.Macro.indexedParser[Foo[Bar]]") must failWith( + ".*cannot find .*Column.* nor .*RowParser.* .*loremIpsum.*Bar.*" + ) + } "be successful for Bar" in withQueryResult(RowLists.intList :+ 1 :+ 3) { implicit c => SQL"TEST".as(Macro.indexedParser[Bar].*) must_=== List(Bar(1), Bar(3)) @@ -154,8 +167,10 @@ final class MacroSpec extends org.specs2.mutable.Specification { } "Generated indexed parser (with an offset)" should { - // No Column[Bar] so compilation error is expected - shapeless.test.illTyped("anorm.Macro.offsetParser[Foo[Bar]]") + "not be resolved" in { + // No Column[Bar] so compilation error is expected + typecheck("Macro.offsetParser[Foo[Bar]]") must failWith(".*too few argument.*") + } "be successful for Bar" in withQueryResult(RowLists.intList :+ 1 :+ 3) { implicit c => SQL"TEST".as(Macro.offsetParser[Bar](0).*) must_=== List(Bar(1), Bar(3)) @@ -223,15 +238,18 @@ final class MacroSpec extends org.specs2.mutable.Specification { } "Sealed parser" should { - // No subclass - shapeless.test.illTyped("anorm.Macro.sealedParser[NoSubclass]") - - // Cannot find the RowParser instances for the subclasses, - // from the implicit scope - shapeless.test.illTyped("Macro.sealedParser[Family]") - - // No subclass - shapeless.test.illTyped("Macro.sealedParser[EmptyFamily]") + "not be resolved" in { + (typecheck("Macro.sealedParser[NoSubclass]") must failWith(".*cannot find any subclass.* .*NoSubclass.*")) + .and { + // Cannot find the RowParser instances for the subclasses, + // from the implicit scope + typecheck("Macro.sealedParser[Family]") must failWith(".*sealed.* .*Bar.* .*CaseObj.*") + } + .and { + // No subclass + typecheck("Macro.sealedParser[EmptyFamily]") must failWith(".*cannot find any subclass.* .*EmptyFamily.*") + } + } "be successful for the Family trait" >> { "with the default discrimination" in { @@ -241,7 +259,8 @@ final class MacroSpec extends org.specs2.mutable.Specification { implicit val caseObjParser = RowParser[CaseObj.type] { _ => Success(CaseObj) } - implicit val barParser = Macro.namedParser[Bar] + implicit val barParser: RowParser[Bar] = + SqlParser.int("v").map { Bar(_) } // cannot handle object anorm.MacroSpec.NotCase: no case accessor @silent def familyParser = Macro.sealedParser[Family] @@ -257,7 +276,8 @@ final class MacroSpec extends org.specs2.mutable.Specification { implicit val caseObjParser = RowParser[CaseObj.type] { _ => Success(CaseObj) } - implicit val barParser = Macro.namedParser[Bar] + implicit val barParser: RowParser[Bar] = + SqlParser.int("v").map { Bar(_) } // cannot handle object anorm.MacroSpec.NotCase: no case accessor @silent def familyParser = @@ -273,8 +293,11 @@ final class MacroSpec extends org.specs2.mutable.Specification { import Macro.{ ParameterProjection => proj } import NamedParameter.{ namedWithString => named } - // No ToParameterList[Bar] so compilation error is expected - shapeless.test.illTyped("anorm.Macro.toParameters[Goo[Bar]]") + "not be resolved" in { + typecheck("Macro.toParameters[Goo[Bar]]") must failWith( + ".*cannot find either .*ToParameterList.* or .*ToStatement.* for .*Bar.*" + ) + } "be successful for Bar" >> { val fixture = Bar(1) @@ -284,10 +307,11 @@ final class MacroSpec extends org.specs2.mutable.Specification { Macro.toParameters[Bar]() -> List(named("v" -> 1)), Macro.toParameters[Bar](proj("v", "w")) -> List(named("w" -> 1)) ).zipWithIndex - ) { case ((encoder, params), index) => - s"using encoder #${index}" in { - encoder(fixture) must_=== params - } + ) { + case ((encoder, params), index) => + s"using encoder #${index}" in { + encoder(fixture) must_=== params + } } } @@ -303,10 +327,11 @@ final class MacroSpec extends org.specs2.mutable.Specification { ), Macro.toParameters[Goo[Int]](proj("loremIpsum", "value")) -> List(named("value" -> 1)) ).zipWithIndex - ) { case ((encoder, params), index) => - s"using encoder #${index}" in { - encoder(fixture) must_=== params - } + ) { + case ((encoder, params), index) => + s"using encoder #${index}" in { + encoder(fixture) must_=== params + } } } @@ -321,10 +346,11 @@ final class MacroSpec extends org.specs2.mutable.Specification { Fragments.foreach( Seq[(Family, List[NamedParameter])](Bar(1) -> List(named("v" -> 1)), CaseObj -> List.empty[NamedParameter]) - ) { case (i, params) => - s"for $i" in { - ToParameterList.from(i) must_=== params - } + ) { + case (i, params) => + s"for $i" in { + ToParameterList.from(i) must_=== params + } } } @@ -346,19 +372,21 @@ final class MacroSpec extends org.specs2.mutable.Specification { named("x" -> Some(false)) ) ).zipWithIndex - ) { case ((encoder, params), index) => - s"using encoder #${index}" in { - encoder(fixture) must_=== params - } + ) { + case ((encoder, params), index) => + s"using encoder #${index}" in { + encoder(fixture) must_=== params + } } } } -*/ "Generated column" should { - shapeless.test.illTyped("anorm.Macro.valueColumn[Bar]") // case class - - shapeless.test.illTyped("anorm.Macro.valueColumn[InvalidValueClass]") + "not be resolved" in { + (typecheck("Macro.valueColumn[Bar]") must failWith(".*AnyVal.*")).and { + typecheck("Macro.valueColumn[InvalidValueClass]") must failWith(".*MacroSpec.*") + } + } "be generated for a supported ValueClass" in { implicit val generated: Column[ValidValueClass] = @@ -371,7 +399,9 @@ final class MacroSpec extends org.specs2.mutable.Specification { } "ToStatement" should { - shapeless.test.illTyped("anorm.Macro.valueToStatement[Bar]") // case class + "not be resolved" in { + typecheck("Macro.valueToStatement[Bar]") must failWith(".*AnyVal.*") + } val SqlDouble3s = ParamMeta.Double(23.456d) diff --git a/enumeratum/src/main/scala/values/ValueEnumColumn.scala b/enumeratum/src/main/scala/values/ValueEnumColumn.scala index af2d0764..be284d3e 100644 --- a/enumeratum/src/main/scala/values/ValueEnumColumn.scala +++ b/enumeratum/src/main/scala/values/ValueEnumColumn.scala @@ -6,16 +6,17 @@ import anorm.{ Column, TypeDoesNotMatch } private[values] object ValueEnumColumn { def apply[ValueType, EntryType <: ValueEnumEntry[ValueType]](e: ValueEnum[ValueType, EntryType])(implicit baseColumn: Column[ValueType] - ): Column[EntryType] = Column.nonNull[EntryType] { case (value, meta) => - baseColumn(value, meta) match { - case Left(err) => - Left(err) + ): Column[EntryType] = Column.nonNull[EntryType] { + case (value, meta) => + baseColumn(value, meta) match { + case Left(err) => + Left(err) - case Right(s) => - e.withValueOpt(s) match { - case Some(obj) => Right(obj) - case None => Left(TypeDoesNotMatch(s"Invalid value: $s")) - } - } + case Right(s) => + e.withValueOpt(s) match { + case Some(obj) => Right(obj) + case None => Left(TypeDoesNotMatch(s"Invalid value: $s")) + } + } } } diff --git a/enumeratum/src/test/scala/EnumColumnSpec.scala b/enumeratum/src/test/scala/EnumColumnSpec.scala index 6b95e73f..d53c2f62 100644 --- a/enumeratum/src/test/scala/EnumColumnSpec.scala +++ b/enumeratum/src/test/scala/EnumColumnSpec.scala @@ -28,8 +28,9 @@ final class EnumColumnSpec extends org.specs2.mutable.Specification { "not be parsed as Column from invalid String representation" >> { def spec(title: String, repr: String) = title in withQueryResult(RowLists.stringList :+ repr) { implicit con => - SQL("SELECT v").as(scalar[Dummy].single) must throwA[Exception].like { case NonFatal(cause) => - cause must_=== AnormException(TypeDoesNotMatch(s"Invalid value: $repr").message) + SQL("SELECT v").as(scalar[Dummy].single) must throwA[Exception].like { + case NonFatal(cause) => + cause must_=== AnormException(TypeDoesNotMatch(s"Invalid value: $repr").message) } } @@ -41,8 +42,11 @@ final class EnumColumnSpec extends org.specs2.mutable.Specification { "not be parsed as Column from non-String values" >> { def spec(tpe: String, rowList: RowList[_]) = tpe in withQueryResult(rowList) { implicit con => - SQL("SELECT v").as(scalar[Dummy].single) must throwA[Exception].like { case NonFatal(cause) => - cause must_=== AnormException(TypeDoesNotMatch(s"Column '.null' expected to be String; Found $tpe").message) + SQL("SELECT v").as(scalar[Dummy].single) must throwA[Exception].like { + case NonFatal(cause) => + cause must_=== AnormException( + TypeDoesNotMatch(s"Column '.null' expected to be String; Found $tpe").message + ) } } diff --git a/enumeratum/src/test/scala/values/ValueEnumColumnSpec.scala b/enumeratum/src/test/scala/values/ValueEnumColumnSpec.scala index a4cba5cb..0ca54f2d 100644 --- a/enumeratum/src/test/scala/values/ValueEnumColumnSpec.scala +++ b/enumeratum/src/test/scala/values/ValueEnumColumnSpec.scala @@ -28,8 +28,9 @@ final class ValueEnumColumnSpec extends org.specs2.mutable.Specification { "not be parsed as Column from invalid Short representation" in { withQueryResult(RowLists.shortList :+ 0.toShort) { implicit con => - SQL("SELECT v").as(scalar[Drink].single) must throwA[Exception].like { case NonFatal(cause) => - cause mustEqual AnormException(TypeDoesNotMatch(s"Invalid value: 0").message) + SQL("SELECT v").as(scalar[Drink].single) must throwA[Exception].like { + case NonFatal(cause) => + cause mustEqual AnormException(TypeDoesNotMatch(s"Invalid value: 0").message) } } } diff --git a/project/Common.scala b/project/Common.scala index 73f79abd..e53aaa9b 100644 --- a/project/Common.scala +++ b/project/Common.scala @@ -31,18 +31,15 @@ object Common extends AutoPlugin { ("com.github.ghik" %% "silencer-plugin" % silencerVersion) .cross(CrossVersion.full) ), - ("com.github.ghik" %% "silencer-lib" % silencerVersion % Provided) - .cross(CrossVersion.full) + ("com.github.ghik" %% "silencer-lib" % silencerVersion % Provided) + .cross(CrossVersion.full) ) } else Seq.empty }, scalacOptions ++= Seq("-Xfatal-warnings"), scalacOptions ++= { if (scalaBinaryVersion.value != "3") { - Seq( - "-target:jvm-1.8", - "-Xlint", - "-g:vars") + Seq("-target:jvm-1.8", "-Xlint", "-g:vars") } else { Seq.empty } @@ -65,9 +62,7 @@ object Common extends AutoPlugin { "-Ywarn-macros:after" ) } else if (v == "2.11") { - Seq( - "-Xmax-classfile-name", "128", - "-Yopt:_", "-Ydead-code", "-Yclosure-elim", "-Yconst-opt") + Seq("-Xmax-classfile-name", "128", "-Yopt:_", "-Ydead-code", "-Yclosure-elim", "-Yconst-opt") } else { Seq( "-explaintypes", @@ -108,13 +103,16 @@ object Common extends AutoPlugin { def scalaUnmanaged(ver: String, base: File): Seq[File] = CrossVersion.partialVersion(ver) match { + case Some((2, 11)) => + Seq(base / "scala-2.12-", base / "scala-2.13-") + case Some((2, 12)) => Seq(base / "scala-2.12+", base / "scala-2.13-") case Some((3, _) | (2, 13)) => Seq(base / "scala-2.12+", base / "scala-2.13+") - case Some((_, minor)) => + case Some((_, minor)) => Seq(base / s"scala-2.${minor}-") case _ => diff --git a/tokenizer/src/main/scala-2/anorm/TokenizedStatement.scala b/tokenizer/src/main/scala-2/anorm/TokenizedStatement.scala index 011f189a..612fb870 100644 --- a/tokenizer/src/main/scala-2/anorm/TokenizedStatement.scala +++ b/tokenizer/src/main/scala-2/anorm/TokenizedStatement.scala @@ -90,8 +90,9 @@ private[anorm] object TokenizedStatement { val groups = (gs match { case TokenGroup(List(StringToken("")), None) :: tgs => tgs // trim end case _ => gs - }).collect { case TokenGroup(pr, pl) => - TokenGroup(pr.reverse, pl) + }).collect { + case TokenGroup(pr, pl) => + TokenGroup(pr.reverse, pl) }.reverse TokenizedStatement(groups, ns.reverse) -> m diff --git a/tokenizer/src/main/scala-3/anorm/TokenizedStatement.scala b/tokenizer/src/main/scala-3/anorm/TokenizedStatement.scala index dd4bcdb9..9e12dc0d 100644 --- a/tokenizer/src/main/scala-3/anorm/TokenizedStatement.scala +++ b/tokenizer/src/main/scala-3/anorm/TokenizedStatement.scala @@ -7,21 +7,24 @@ package anorm private[anorm] case class TokenizedStatement(tokens: Seq[TokenGroup], names: Seq[String]) private[anorm] object TokenizedStatement { - import scala.quoted.{Expr,FromExpr,Quotes,Type} + import scala.quoted.{ Expr, FromExpr, Quotes, Type } /** Returns empty tokenized statement. */ lazy val empty = TokenizedStatement(Nil, Nil) /** String interpolation to tokenize statement. */ - inline def stringInterpolation[T](inline parts: Seq[String], inline params: Seq[T & Show]): (TokenizedStatement, Map[String, T]) = ${tokenizeImpl[T]('parts, 'params)} + inline def stringInterpolation[T]( + inline parts: Seq[String], + inline params: Seq[T & Show] + ): (TokenizedStatement, Map[String, T]) = ${ tokenizeImpl[T]('parts, 'params) } /** Tokenization macro */ private def tokenizeImpl[T]( - parts: Expr[Seq[String]], - params: Expr[Seq[T & Show]] + parts: Expr[Seq[String]], + params: Expr[Seq[T & Show]] )(using Quotes, Type[T]): Expr[(TokenizedStatement, Map[String, T])] = '{ - val _parts = ${parts} - val _params = ${params} + val _parts = ${ parts } + val _params = ${ params } tokenize(Iterator[String](), Nil, _parts, _params, Nil, Nil, Map.empty[String, T]) } @@ -51,12 +54,12 @@ private[anorm] object TokenizedStatement { StringToken(str.dropRight(1)) :: gts } val ng = TokenGroup( - (tks ::: StringToken(v.show) :: - before), + tks ::: StringToken(v.show) :: + before, pl ) - tokenize(ti, tks.tail, parts, ps.tail, (ng :: groups), ns, m) + tokenize(ti, tks.tail, parts, ps.tail, ng :: groups, ns, m) case _ => val ng = TokenGroup(tks, None) @@ -66,8 +69,8 @@ private[anorm] object TokenizedStatement { tks.tail, parts, ps.tail, - (ng :: prev.copy(placeholder = Some(n)) :: groups), - (n +: ns), + ng :: prev.copy(placeholder = Some(n)) :: groups, + n +: ns, m + (n -> v) ) } @@ -89,8 +92,9 @@ private[anorm] object TokenizedStatement { val groups = (gs match { case TokenGroup(List(StringToken("")), None) :: tgs => tgs // trim end case _ => gs - }).collect { case TokenGroup(pr, pl) => - TokenGroup(pr.reverse, pl) + }).collect { + case TokenGroup(pr, pl) => + TokenGroup(pr.reverse, pl) }.reverse TokenizedStatement(groups, ns.reverse) -> m