From 25f953aec50b5fb6c9bd1b021688104e41d29d91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Chantepie?= Date: Wed, 17 Aug 2022 21:25:15 +0200 Subject: [PATCH] WIP: Migration RowParserImpl --- core/src/main/scala-2/anorm/Macro.scala | 2 +- .../anorm/macros/SealedRowParserImpl.scala | 6 +- core/src/main/scala-3/anorm/Macro.scala | 66 ++++-- .../scala-3/anorm/macros/RowParserImpl.scala | 200 ++++++++++++++++++ .../anorm/macros/SealedRowParserImpl.scala | 25 +-- .../anorm/macros/ToParameterListImpl.scala | 26 +-- core/src/main/scala/anorm/RowParser.scala | 180 ++++++++++++++++ core/src/main/scala/anorm/SqlParser.scala | 179 ---------------- core/src/test/scala/anorm/MacroSpec.scala | 163 ++++++++------ 9 files changed, 548 insertions(+), 299 deletions(-) create mode 100644 core/src/main/scala-3/anorm/macros/RowParserImpl.scala create mode 100644 core/src/main/scala/anorm/RowParser.scala diff --git a/core/src/main/scala-2/anorm/Macro.scala b/core/src/main/scala-2/anorm/Macro.scala index 4ceabc3a..f6ab11ee 100644 --- a/core/src/main/scala-2/anorm/Macro.scala +++ b/core/src/main/scala-2/anorm/Macro.scala @@ -66,7 +66,7 @@ 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-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/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..410d66e0 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,13 @@ 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] @@ -65,10 +69,10 @@ final class MacroSpec extends org.specs2.mutable.Specification { "be successful for Foo[Int]" >> { def spec(parser1: RowParser[Foo[Int]], parser2: RowParser[Foo[Int]])(implicit c: Connection) = { val expected = List( - Foo(1.2f, "str1")(1, Some(2L))(Some(true)), - Foo(2.3f, "str2")(4, None)(None), - Foo(3.4f, "str3")(5, Some(3L))(None), - Foo(5.6f, "str4")(6, None)(Some(false)) + Foo(1.2F, "str1")(1, Some(2L))(Some(true)), + Foo(2.3F, "str2")(4, None)(None), + Foo(3.4F, "str3")(5, Some(3L))(None), + Foo(5.6F, "str4")(6, None)(Some(false)) ) (SQL"TEST".as(parser1.*) must_=== expected).and(SQL("TEST").as(parser2.*) must_=== expected) @@ -76,10 +80,10 @@ final class MacroSpec extends org.specs2.mutable.Specification { "using the default column naming" in withQueryResult( fooRow1 - .append(1.2f, "str1", 1, 2L, true) - .append(2.3f, "str2", 4, nullLong, nullBoolean) - .append(3.4f, "str3", 5, 3L, nullBoolean) - .append(5.6f, "str4", 6, nullLong, false) + .append(1.2F, "str1", 1, 2L, true) + .append(2.3F, "str2", 4, nullLong, nullBoolean) + .append(3.4F, "str3", 5, 3L, nullBoolean) + .append(5.6F, "str4", 6, nullLong, false) ) { implicit con => spec(Macro.namedParser[Foo[Int]], Macro.parser[Foo[Int]]("r", "bar", "loremIpsum", "opt", "x")) @@ -88,10 +92,10 @@ final class MacroSpec extends org.specs2.mutable.Specification { "using the snake case naming" in withQueryResult( fooRow2 - .append(1.2f, "str1", 1, 2L, true) - .append(2.3f, "str2", 4, nullLong, nullBoolean) - .append(3.4f, "str3", 5, 3L, nullBoolean) - .append(5.6f, "str4", 6, nullLong, false) + .append(1.2F, "str1", 1, 2L, true) + .append(2.3F, "str2", 4, nullLong, nullBoolean) + .append(3.4F, "str3", 5, 3L, nullBoolean) + .append(5.6F, "str4", 6, nullLong, false) ) { implicit con => spec( @@ -115,21 +119,25 @@ final class MacroSpec extends org.specs2.mutable.Specification { classOf[Int] -> "v" ) - withQueryResult(row.append(1.2f, "str1", 1, 2L, true, 6)) { implicit c => - SQL"TEST".as(fooBar.singleOpt) must beSome(Foo(1.2f, "str1")(Bar(6), Some(2))(Some(true))) + withQueryResult(row.append(1.2F, "str1", 1, 2L, true, 6)) { implicit c => + SQL"TEST".as(fooBar.singleOpt) must beSome(Foo(1.2F, "str1")(Bar(6), Some(2))(Some(true))) } } "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)) @@ -137,25 +145,28 @@ final class MacroSpec extends org.specs2.mutable.Specification { "be successful for Foo[Int]" in withQueryResult( fooRow1 - .append(1.2f, "str1", 1, 2L, true) - .append(2.3f, "str2", 4, nullLong, nullBoolean) - .append(3.4f, "str3", 5, 3L, nullBoolean) - .append(5.6f, "str4", 6, nullLong, false) + .append(1.2F, "str1", 1, 2L, true) + .append(2.3F, "str2", 4, nullLong, nullBoolean) + .append(3.4F, "str3", 5, 3L, nullBoolean) + .append(5.6F, "str4", 6, nullLong, false) ) { implicit con => val parser: RowParser[Foo[Int]] = Macro.indexedParser[Foo[Int]] SQL"TEST".as(parser.*) must_=== List( - Foo(1.2f, "str1")(1, Some(2L))(Some(true)), - Foo(2.3f, "str2")(4, None)(None), - Foo(3.4f, "str3")(5, Some(3L))(None), - Foo(5.6f, "str4")(6, None)(Some(false)) + Foo(1.2F, "str1")(1, Some(2L))(Some(true)), + Foo(2.3F, "str2")(4, None)(None), + Foo(3.4F, "str3")(5, Some(3L))(None), + Foo(5.6F, "str4")(6, None)(Some(false)) ) } } "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)) @@ -163,27 +174,27 @@ final class MacroSpec extends org.specs2.mutable.Specification { "be successful for Foo[Int]" in withQueryResult( fooRow1 - .append(1.2f, "str1", 1, 2L, true) - .append(2.3f, "str2", 4, nullLong, nullBoolean) - .append(3.4f, "str3", 5, 3L, nullBoolean) - .append(5.6f, "str4", 6, nullLong, false) + .append(1.2F, "str1", 1, 2L, true) + .append(2.3F, "str2", 4, nullLong, nullBoolean) + .append(3.4F, "str3", 5, 3L, nullBoolean) + .append(5.6F, "str4", 6, nullLong, false) ) { implicit con => val parser: RowParser[Foo[Int]] = Macro.offsetParser[Foo[Int]](0) SQL"TEST".as(parser.*) must_=== List( - Foo(1.2f, "str1")(1, Some(2L))(Some(true)), - Foo(2.3f, "str2")(4, None)(None), - Foo(3.4f, "str3")(5, Some(3L))(None), - Foo(5.6f, "str4")(6, None)(Some(false)) + Foo(1.2F, "str1")(1, Some(2L))(Some(true)), + Foo(2.3F, "str2")(4, None)(None), + Foo(3.4F, "str3")(5, Some(3L))(None), + Foo(5.6F, "str4")(6, None)(Some(false)) ) } "be successful for Goo[T] with offset = 2" in withQueryResult( fooRow1 - .append(1.2f, "str1", 1, 2L, true) - .append(2.3f, "str2", 4, nullLong, nullBoolean) - .append(3.4f, "str3", 5, 3L, nullBoolean) - .append(5.6f, "str4", 6, nullLong, false) + .append(1.2F, "str1", 1, 2L, true) + .append(2.3F, "str2", 4, nullLong, nullBoolean) + .append(3.4F, "str3", 5, 3L, nullBoolean) + .append(5.6F, "str4", 6, nullLong, false) ) { implicit con => val parser: RowParser[Goo[Int]] = Macro.offsetParser[Goo[Int]](2) @@ -223,15 +234,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 +255,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 +272,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 +289,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 +303,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 + } } } @@ -320,7 +340,9 @@ final class MacroSpec extends org.specs2.mutable.Specification { } Fragments.foreach( - Seq[(Family, List[NamedParameter])](Bar(1) -> List(named("v" -> 1)), CaseObj -> List.empty[NamedParameter]) + 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 @@ -353,12 +375,13 @@ final class MacroSpec extends org.specs2.mutable.Specification { } } } -*/ "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 +394,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)