diff --git a/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala b/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala index 53ede01d..62793776 100644 --- a/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala +++ b/smt-common/src/main/scala/org/bitlap/common/CaseClassExtractor.scala @@ -21,9 +21,8 @@ package org.bitlap.common -import java.time.ZonedDateTime -import java.time.format.DateTimeFormatter -import scala.reflect.macros.whitebox +import org.bitlap.common.internal.CaseClassExtractorMacro + import scala.reflect.ClassTag import scala.reflect.runtime.{ universe => ru } import scala.reflect.runtime.universe._ @@ -38,34 +37,7 @@ object CaseClassExtractor { /** Using the characteristics of the product type to get the field value should force the conversion externally * (safely). */ - def ofValue[T <: Product](t: T, field: CaseClassField): Option[Any] = macro macroImpl[T] - - def macroImpl[T: c.WeakTypeTag]( - c: whitebox.Context - )(t: c.Expr[T], field: c.Expr[CaseClassField]): c.Expr[Option[Any]] = { - import c.universe._ - // scalafmt: { maxColumn = 400 } - val tree = - q""" - if ($t == null) None else { - val _field = $field - _field.${TermName(CaseClassField.fieldNamesTermName)}.find(kv => kv._2 == _field.${TermName(CaseClassField.stringifyTermName)}) - .map(kv => $t.productElement(kv._1)) - } - """ - exprPrintTree[Option[Any]](c)(tree) - - } - - def exprPrintTree[Field: c.WeakTypeTag](c: whitebox.Context)(resTree: c.Tree): c.Expr[Field] = { - c.info( - c.enclosingPosition, - s"\n###### Time: ${ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)} Expanded macro start ######\n" + resTree - .toString() + "\n###### Expanded macro end ######\n", - force = false - ) - c.Expr[Field](resTree) - } + def ofValue[T <: Product](t: T, field: CaseClassField): Option[Any] = macro CaseClassExtractorMacro.macroImpl[T] /** Using scala reflect to get the field value (safely). */ diff --git a/smt-common/src/main/scala/org/bitlap/common/CaseClassField.scala b/smt-common/src/main/scala/org/bitlap/common/CaseClassField.scala index 044aa73f..442e24ee 100644 --- a/smt-common/src/main/scala/org/bitlap/common/CaseClassField.scala +++ b/smt-common/src/main/scala/org/bitlap/common/CaseClassField.scala @@ -21,10 +21,7 @@ package org.bitlap.common -import org.bitlap.common.CaseClassExtractor.exprPrintTree - -import scala.reflect.macros.whitebox -import scala.collection.Seq +import org.bitlap.common.internal.CaseClassFieldMacro trait CaseClassField { @@ -44,61 +41,5 @@ object CaseClassField { final val fieldTermName = "Field" final val fieldNamesTermName = "fieldIndexNames" - def apply[T <: Product](field: T => Any): CaseClassField = macro selectFieldMacroImpl[T] - - def selectFieldMacroImpl[T: c.WeakTypeTag]( - c: whitebox.Context - )(field: c.Expr[T => Any]): c.Expr[CaseClassField] = { - import c.universe._ - val packageName = q"_root_.org.bitlap.common" - val Function(_, Select(_, termName)) = field.tree - val caseClassParams = getCaseClassParams[T](c) - val fieldName = termName.decodedName.toString - val searchField = - caseClassParams.find(_.name.toTermName.decodedName.toString == fieldName) - val fieldType = searchField.map(f => c.typecheck(tq"$f", c.TYPEmode).tpe) - if (searchField.isEmpty || fieldType.isEmpty) { - c.abort( - c.enclosingPosition, - s"""Field name is invalid, "${c.weakTypeOf[T].resultType}" does not have a field named $fieldName! - |Please consider using "CaseClassField[T]($fieldName)" instead of "CaseClassField($fieldName)" """.stripMargin - ) - } - - val genericType = fieldType.get match { - case t if t <:< typeOf[Option[_]] => - val genericType = t.dealias.typeArgs.head - tq"_root_.scala.Option[$genericType]" - case t if t <:< typeOf[Seq[_]] => - val genericType = t.dealias.typeArgs.head - tq"_root_.scala.Seq[$genericType]" - case t if t <:< typeOf[List[_]] => - val genericType = t.dealias.typeArgs.head - tq"_root_.scala.List[$genericType]" - case t => tq"$t" - } - - val fieldNameTypeName = TermName(s"${CaseClassField.classNameTermName}$$$fieldName") - val res = - q""" - case object $fieldNameTypeName extends $packageName.${TypeName(CaseClassField.classNameTermName)} { - override def ${TermName(CaseClassField.stringifyTermName)}: String = $fieldName - override type ${TypeName(CaseClassField.fieldTermName)} = $genericType - override val ${TermName(CaseClassField.fieldNamesTermName)} = - (${caseClassParams.indices.toList} zip ${caseClassParams.map(_.name.decodedName.toString)}).toMap - } - $fieldNameTypeName - """ - exprPrintTree[CaseClassField](c)(res) - } - - def getCaseClassParams[T: c.WeakTypeTag](c: whitebox.Context): List[c.Symbol] = { - import c.universe._ - val parameters = c.weakTypeOf[T].resultType.member(TermName("")).typeSignature.paramLists - if (parameters.size > 1) { - c.abort(c.enclosingPosition, "The constructor of case class has currying!") - } - parameters.flatten - } - + def apply[T <: Product](field: T => Any): CaseClassField = macro CaseClassFieldMacro.selectFieldMacroImpl[T] } diff --git a/smt-common/src/main/scala/org/bitlap/common/MacroCache.scala b/smt-common/src/main/scala/org/bitlap/common/MacroCache.scala index 501362a5..0faa0293 100644 --- a/smt-common/src/main/scala/org/bitlap/common/MacroCache.scala +++ b/smt-common/src/main/scala/org/bitlap/common/MacroCache.scala @@ -46,4 +46,6 @@ object MacroCache { lazy val classFieldTypeMapping: mutable.Map[Int, mutable.Map[String, Any]] = mutable.Map.empty lazy val classFieldDefaultValueMapping: mutable.Map[Int, mutable.Map[String, Any]] = mutable.Map.empty + + lazy val transformerOptionsMapping: mutable.Map[Int, mutable.Set[Options]] = mutable.Map.empty } diff --git a/smt-common/src/main/scala/org/bitlap/common/Options.scala b/smt-common/src/main/scala/org/bitlap/common/Options.scala new file mode 100644 index 00000000..dc29dc4b --- /dev/null +++ b/smt-common/src/main/scala/org/bitlap/common/Options.scala @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 bitlap + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.bitlap.common + +/** @author + * 梦境迷离 + * @version 1.0,6/27/22 + */ +sealed trait Options + +object Options { + + case object enableOptionDefaultsToNone extends Options + + case object enableCollectionDefaultsToEmpty extends Options + + case object disableCollectionDefaultsToEmpty extends Options + + case object disableOptionDefaultsToNone extends Options + +} diff --git a/smt-common/src/main/scala/org/bitlap/common/Transformable.scala b/smt-common/src/main/scala/org/bitlap/common/Transformable.scala index b4084d08..1b7fb744 100644 --- a/smt-common/src/main/scala/org/bitlap/common/Transformable.scala +++ b/smt-common/src/main/scala/org/bitlap/common/Transformable.scala @@ -20,6 +20,7 @@ */ package org.bitlap.common +import org.bitlap.common.internal.TransformerMacro /** @author * 梦境迷离 @@ -27,12 +28,15 @@ package org.bitlap.common */ class Transformable[From, To] { - /** @param selectFromField + /** Sets the `From` to `To` mapping relationship of the field type. + * + * When the map function returns a known constant value, it means that the type mapping becomes a set value. + * + * @param selectFromField * Select the name of the field to be mapped in the `From` class. * @param map * Specify the type mapping of the field, which must be provided when the type is incompatible, or else attempt to * search for an implicit `Transformer[FromField, ToField]` (a failed search will result in a compile failure). - * * @tparam FromField * field type * @tparam ToField @@ -47,7 +51,9 @@ class Transformable[From, To] { ): Transformable[From, To] = macro TransformerMacro.mapTypeImpl[From, To, FromField, ToField] - /** @param selectFromField + /** Sets the `From` to `To` mapping relationship of the field name. + * + * @param selectFromField * Select the name of the field to be mapped in the `From` class. * @param selectToField * Select the name of the field to be mapped in the `To` class. @@ -66,14 +72,36 @@ class Transformable[From, To] { ): Transformable[From, To] = macro TransformerMacro.mapNameImpl[From, To, FromField, ToField] - /** Defines default value for missing field to successfully create `To` object. This method has the lowest priority. + /** Defines a default value for missing field to successfully create `To` object. This method has a higher priority + * than `enableOptionDefaultsToNone` or `enableCollectionDefaultsToEmpty`. * - * Only the `selectToField` field does not have the same name found in the `From` and is not in the name mapping. + * So, even if `enableCollectionDefaultsToEmpty` or `enableCollectionDefaultsToEmpty`, you can also use + * `setDefaultValue` method to set the initial value for a single field. */ - @unchecked def setDefaultValue[ToField](selectToField: To => ToField, defaultValue: ToField): Transformable[From, To] = macro TransformerMacro.setDefaultValueImpl[From, To, ToField] + /** Sets target value of optional fields to `None` if field is missing from source type `From`. + */ + def enableOptionDefaultsToNone: Transformable[From, To] = + macro TransformerMacro.enableOptionDefaultsToNoneImpl[From, To] + + /** Sets target value of collection fields to `empty` if field is missing from source type `From`. + */ + def enableCollectionDefaultsToEmpty: Transformable[From, To] = + macro TransformerMacro.enableCollectionDefaultsToEmptyImpl[From, To] + + /** Disable `None` fallback value for optional fields in `To`. This is the default configuration option. + */ + def disableOptionDefaultsToNone: Transformable[From, To] = + macro TransformerMacro.disableOptionDefaultsToNoneImpl[From, To] + + /** Disable `empty` fallback value for collection fields in `To`. Support List, Seq, Vector, Set. This is the default + * configuration option. + */ + def disableCollectionDefaultsToEmpty: Transformable[From, To] = + macro TransformerMacro.disableCollectionDefaultsToEmptyImpl[From, To] + def instance: Transformer[From, To] = macro TransformerMacro.instanceImpl[From, To] } @@ -82,6 +110,7 @@ object Transformable { /** Automatically derive `Transformable[From, To]` for case classes only, for non-case classes you should use the * `setType` method to configure the mapping relationship. + * * @tparam From * @tparam To * @return diff --git a/smt-common/src/main/scala/org/bitlap/common/AbstractMacroProcessor.scala b/smt-common/src/main/scala/org/bitlap/common/internal/AbstractMacroProcessor.scala similarity index 78% rename from smt-common/src/main/scala/org/bitlap/common/AbstractMacroProcessor.scala rename to smt-common/src/main/scala/org/bitlap/common/internal/AbstractMacroProcessor.scala index 425377f2..418c9622 100644 --- a/smt-common/src/main/scala/org/bitlap/common/AbstractMacroProcessor.scala +++ b/smt-common/src/main/scala/org/bitlap/common/internal/AbstractMacroProcessor.scala @@ -19,7 +19,7 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package org.bitlap.common +package org.bitlap.common.internal import java.time.ZonedDateTime import java.time.format.DateTimeFormatter @@ -44,7 +44,11 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { isOption: Boolean = false, isVector: Boolean = false, isSet: Boolean = false - ) + ) { + def isCollection: Boolean = isSeq || isList || isOption || isVector || isSet + + def isStrictCollection: Boolean = isSeq || isList || isVector || isSet + } final case class FieldTreeInformation( index: Int, @@ -59,7 +63,9 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { fieldName: String, fieldType: Type, collectionFlags: CollectionFlags, - genericType: List[Type] = Nil + genericType: List[Type] = Nil, + hasDefaultValue: Boolean, + zeroValue: Tree ) def tryGetOrElse(tree: Tree, default: Tree): Tree = @@ -82,7 +88,7 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { */ def checkGetFieldTreeInformationList[T: WeakTypeTag](columnsFunc: TermName): List[FieldTreeInformation] = { val idxColumn = (i: Int) => q"$columnsFunc()($i)" - val params = getCaseClassFieldInfo[T]() + val params = getCaseClassFieldInfoList[T]() val paramsSize = params.size val types = params.map(_.fieldType) val indexColumns = (0 until paramsSize).toList.map(i => i -> idxColumn(i)) @@ -91,46 +97,65 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { } indexColumns zip types map { kv => - val (isOption, isSeq, isList, isVector, isSet) = isWrapType(kv._2) - val typed = c.typecheck(tq"${kv._2}", c.TYPEmode).tpe - var genericType: List[Type] = Nil - if (isList || isSeq || isOption || isVector || isSet) { + val collectionFlag = isWrapType(kv._2) + val typed = c.typecheck(tq"${kv._2}", c.TYPEmode).tpe + var genericType: List[Type] = Nil + if (collectionFlag.isCollection) { genericType = typed.dealias.typeArgs ::: genericType } FieldTreeInformation( kv._1._1, kv._1._2, kv._2, - getDefaultValue(kv._2), - CollectionFlags(isSeq, isList, isOption, isVector, isSet), + getZeroValue(kv._2), + collectionFlag, genericType ) } } + def getFieldDefaultValueMap[T: WeakTypeTag](init: MethodSymbol): Map[String, Tree] = { + val classSym = weakTypeOf[T].typeSymbol + init.paramLists.head + .map(_.asTerm) + .zipWithIndex + .flatMap { case (p, i) => + if (!p.isParamWithDefault) None + else { + val getterName = TermName("apply$default$" + (i + 1)) + Some(p.name.decodedName.toString -> q"${classSym.name.toTermName}.$getterName") // moduleSym is none + } + } + .toMap + } + /** Get only the symbol of the case class constructor parameters. * * @tparam T * Type of the case class. * @return */ - def getCaseClassFieldInfo[T: WeakTypeTag](): List[FieldInformation] = { - val parameters = resolveParameters[T] + def getCaseClassFieldInfoList[T: WeakTypeTag](): List[FieldInformation] = { + val init = c.weakTypeOf[T].resultType.member(TermName("")).asMethod + val defaultValuesTerm = getFieldDefaultValueMap[T](init) + val parameters = init.typeSignature.paramLists if (parameters.size > 1) { c.abort(c.enclosingPosition, "The constructor of case class has currying!") } parameters.flatten.map { p => - val typed = c.typecheck(tq"$p", c.TYPEmode).tpe - var genericType: List[Type] = Nil - val (isOption, isSeq, isList, isVector, isSet) = isWrapType(typed) - if (isList || isSeq || isOption || isVector || isSet) { + val typed = c.typecheck(tq"$p", c.TYPEmode).tpe + var genericType: List[Type] = Nil + val collectionFlags = isWrapType(typed) + if (collectionFlags.isCollection) { genericType = typed.dealias.typeArgs ::: genericType } FieldInformation( p.name.decodedName.toString, typed, - CollectionFlags(isSeq, isList, isOption, isVector, isSet), - genericType + collectionFlags, + genericType, + defaultValuesTerm.contains(p.name.decodedName.toString), + getZeroValue(typed) ) } } @@ -152,16 +177,6 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { c.Expr[T](resTree) } - /** Get the constructor symbol of the case class. - * - * @tparam T - * Type of the case class. - * @return - * The parameters may be currying, so it's a two-level list. - */ - def resolveParameters[T: WeakTypeTag]: List[List[Symbol]] = - c.weakTypeOf[T].resultType.member(TermName("")).typeSignature.paramLists - /** Get the `TypeName` of the class. * * @tparam T @@ -179,7 +194,7 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { * @return */ def checkGetFieldZipInformation[T: WeakTypeTag]: FieldZipInformation = { - val params = getCaseClassFieldInfo[T]() + val params = getCaseClassFieldInfoList[T]() val paramsSize = params.size val names = params.map(_.fieldName) FieldZipInformation( @@ -196,7 +211,7 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { def getBuilderId(annoBuilderPrefix: String): Int = c.prefix.actualType.toString.replace(annoBuilderPrefix, "").toInt - private def getDefaultValue(typ: Type): Tree = + def getZeroValue(typ: Type): Tree = typ match { case t if t =:= typeOf[Int] => q"0" @@ -226,9 +241,7 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { q"null" } - private type OptionSeqListVectorSet = (Boolean, Boolean, Boolean, Boolean, Boolean) - - private def isWrapType(typed: Type): OptionSeqListVectorSet = { + private def isWrapType(typed: Type): CollectionFlags = { var isList: Boolean = false var isSeq: Boolean = false var isOption: Boolean = false @@ -247,7 +260,7 @@ abstract class AbstractMacroProcessor(val c: blackbox.Context) { isSeq = true case _ => } - Tuple5(isOption, isSeq, isList, isVector, isSet) + CollectionFlags(isSeq, isList, isOption, isVector, isSet) } } diff --git a/smt-common/src/main/scala/org/bitlap/common/internal/CaseClassExtractorMacro.scala b/smt-common/src/main/scala/org/bitlap/common/internal/CaseClassExtractorMacro.scala new file mode 100644 index 00000000..765075d1 --- /dev/null +++ b/smt-common/src/main/scala/org/bitlap/common/internal/CaseClassExtractorMacro.scala @@ -0,0 +1,57 @@ +/* + * Copyright (c) 2022 bitlap + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.bitlap.common.internal + +import org.bitlap.common.CaseClassField + +import java.time.ZonedDateTime +import java.time.format.DateTimeFormatter +import scala.reflect.macros.whitebox + +/** @author + * 梦境迷离 + * @version 1.0,6/27/22 + */ +object CaseClassExtractorMacro { + + def macroImpl[T]( + c: whitebox.Context + )(t: c.Expr[T], field: c.Expr[CaseClassField]): c.Expr[Option[Any]] = { + import c.universe._ + // scalafmt: { maxColumn = 400 } + val tree = + q""" + if ($t == null) None else { + val _field = $field + _field.${TermName(CaseClassField.fieldNamesTermName)}.find(kv => kv._2 == _field.${TermName(CaseClassField.stringifyTermName)}) + .map(kv => $t.productElement(kv._1)) + } + """ + c.info( + c.enclosingPosition, + s"\n###### Time: ${ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)} Expanded macro start ######\n" + tree + .toString() + "\n###### Expanded macro end ######\n", + force = false + ) + c.Expr[Option[Any]](tree) + } +} diff --git a/smt-common/src/main/scala/org/bitlap/common/internal/CaseClassFieldMacro.scala b/smt-common/src/main/scala/org/bitlap/common/internal/CaseClassFieldMacro.scala new file mode 100644 index 00000000..e7770575 --- /dev/null +++ b/smt-common/src/main/scala/org/bitlap/common/internal/CaseClassFieldMacro.scala @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2022 bitlap + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of + * the Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER + * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ + +package org.bitlap.common.internal +import org.bitlap.common.CaseClassField + +import scala.collection.Seq +import scala.reflect.macros.whitebox +import java.time.format.DateTimeFormatter +import java.time.ZonedDateTime + +/** @author + * 梦境迷离 + * @version 1.0,6/27/22 + */ +object CaseClassFieldMacro { + + def selectFieldMacroImpl[T: c.WeakTypeTag]( + c: whitebox.Context + )(field: c.Expr[T => Any]): c.Expr[CaseClassField] = { + import c.universe._ + val packageName = q"_root_.org.bitlap.common" + val Function(_, Select(_, termName)) = field.tree + val caseClassParams = getCaseClassParams[T](c) + val fieldName = termName.decodedName.toString + val searchField = + caseClassParams.find(_.name.toTermName.decodedName.toString == fieldName) + val fieldType = searchField.map(f => c.typecheck(tq"$f", c.TYPEmode).tpe) + if (searchField.isEmpty || fieldType.isEmpty) { + c.abort( + c.enclosingPosition, + s"""Field name is invalid, "${c.weakTypeOf[T].resultType}" does not have a field named $fieldName! + |Please consider using "CaseClassField[T]($fieldName)" instead of "CaseClassField($fieldName)" """.stripMargin + ) + } + + val genericType = fieldType.get match { + case t if t <:< typeOf[Option[_]] => + val genericType = t.dealias.typeArgs.head + tq"_root_.scala.Option[$genericType]" + case t if t <:< typeOf[Seq[_]] => + val genericType = t.dealias.typeArgs.head + tq"_root_.scala.Seq[$genericType]" + case t if t <:< typeOf[List[_]] => + val genericType = t.dealias.typeArgs.head + tq"_root_.scala.List[$genericType]" + case t => tq"$t" + } + + val fieldNameTypeName = TermName(s"${CaseClassField.classNameTermName}$$$fieldName") + val res = + q""" + case object $fieldNameTypeName extends $packageName.${TypeName(CaseClassField.classNameTermName)} { + override def ${TermName(CaseClassField.stringifyTermName)}: String = $fieldName + override type ${TypeName(CaseClassField.fieldTermName)} = $genericType + override val ${TermName(CaseClassField.fieldNamesTermName)} = + (${caseClassParams.indices.toList} zip ${caseClassParams.map(_.name.decodedName.toString)}).toMap + } + $fieldNameTypeName + """ + c.info( + c.enclosingPosition, + s"\n###### Time: ${ZonedDateTime.now().format(DateTimeFormatter.ISO_ZONED_DATE_TIME)} Expanded macro start ######\n" + res + .toString() + "\n###### Expanded macro end ######\n", + force = false + ) + c.Expr[CaseClassField](res) + } + + def getCaseClassParams[T: c.WeakTypeTag](c: whitebox.Context): List[c.Symbol] = { + import c.universe._ + val parameters = c.weakTypeOf[T].resultType.member(TermName("")).typeSignature.paramLists + if (parameters.size > 1) { + c.abort(c.enclosingPosition, "The constructor of case class has currying!") + } + parameters.flatten + } + +} diff --git a/smt-common/src/main/scala/org/bitlap/common/TransformerMacro.scala b/smt-common/src/main/scala/org/bitlap/common/internal/TransformerMacro.scala similarity index 63% rename from smt-common/src/main/scala/org/bitlap/common/TransformerMacro.scala rename to smt-common/src/main/scala/org/bitlap/common/internal/TransformerMacro.scala index 65a4afb7..ebfb9cc5 100644 --- a/smt-common/src/main/scala/org/bitlap/common/TransformerMacro.scala +++ b/smt-common/src/main/scala/org/bitlap/common/internal/TransformerMacro.scala @@ -19,9 +19,10 @@ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ -package org.bitlap.common +package org.bitlap.common.internal + +import org.bitlap.common.{ MacroCache, Options, Transformable, Transformer => BitlapTransformer } -import org.bitlap.common.{ Transformer => BitlapTransformer } import scala.collection.mutable import scala.reflect.macros.whitebox @@ -33,8 +34,6 @@ class TransformerMacro(override val c: whitebox.Context) extends AbstractMacroPr import c.universe._ - import scala.collection.immutable - protected val packageName = q"_root_.org.bitlap.common" private val builderFunctionPrefix = "_TransformableFunction$" private val builderDefaultValuePrefix$ = "_TransformableDefaultValue$" @@ -77,7 +76,54 @@ class TransformerMacro(override val c: whitebox.Context) extends AbstractMacroPr MacroCache.classFieldNameMapping .getOrElseUpdate(builderId, mutable.Map.empty) .update(toName.decodedName.toString, fromName.decodedName.toString) + val tree = q"new ${c.prefix.actualType}" + exprPrintTree[Transformable[From, To]](force = false, tree) + } + + def enableOptionDefaultsToNoneImpl[From, To] = + setOptions[From, To]( + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .add(Options.enableOptionDefaultsToNone), + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .remove(Options.disableOptionDefaultsToNone) + ) + + def enableCollectionDefaultsToEmptyImpl[From, To] = + setOptions[From, To]( + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .add(Options.enableCollectionDefaultsToEmpty), + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .remove(Options.disableCollectionDefaultsToEmpty) + ) + def disableOptionDefaultsToNoneImpl[From, To] = + setOptions[From, To]( + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .add(Options.disableOptionDefaultsToNone), + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .remove(Options.enableCollectionDefaultsToEmpty) + ) + + def disableCollectionDefaultsToEmptyImpl[From, To] = + setOptions[From, To]( + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .add(Options.disableCollectionDefaultsToEmpty), + MacroCache.transformerOptionsMapping + .getOrElseUpdate(_, mutable.Set.empty) + .remove(Options.enableCollectionDefaultsToEmpty) + ) + + private def setOptions[From, To](enable: Int => Unit, disable: Int => Unit): Expr[Transformable[From, To]] = { + val builderId = getBuilderId(annoBuilderPrefix) + enable(builderId) + disable(builderId) val tree = q"new ${c.prefix.actualType}" exprPrintTree[Transformable[From, To]](force = false, tree) } @@ -140,35 +186,13 @@ class TransformerMacro(override val c: whitebox.Context) extends AbstractMacroPr private def getTransformBody[From: WeakTypeTag, To: WeakTypeTag]: Tree = { val toClassName = resolveClassTypeName[To] - val fromClassName = resolveClassTypeName[From] - val toClassInfo = getCaseClassFieldInfo[To]() - val fromClassInfo = getCaseClassFieldInfo[From]() - val customDefaultValueMapping = - MacroCache.classFieldDefaultValueMapping.getOrElse(getBuilderId(annoBuilderPrefix), mutable.Map.empty) + val toClassInfo = getCaseClassFieldInfoList[To]() + val fromClassInfo = getCaseClassFieldInfoList[From]() val customFieldNameMapping = MacroCache.classFieldNameMapping.getOrElse(getBuilderId(annoBuilderPrefix), mutable.Map.empty) val customFieldTypeMapping = MacroCache.classFieldTypeMapping.getOrElse(getBuilderId(annoBuilderPrefix), mutable.Map.empty) - c.info(c.enclosingPosition, s"Field default value mapping: $customDefaultValueMapping", force = true) - c.info(c.enclosingPosition, s"Field name mapping: $customFieldNameMapping", force = true) - c.info(c.enclosingPosition, s"Field type mapping: $customFieldTypeMapping", force = true) - - val missingFields = toClassInfo.map(_.fieldName).filterNot(fromClassInfo.map(_.fieldName).contains) - val missingExcludeMappingName = missingFields.filterNot(customFieldNameMapping.contains) - if (missingExcludeMappingName.nonEmpty) { - val noDefaultValueFields = missingExcludeMappingName.filterNot(customDefaultValueMapping.keySet.contains) - if (noDefaultValueFields.nonEmpty) { - c.abort( - c.enclosingPosition, - s"From type: `$fromClassName` has fewer fields than To type: `$toClassName` and cannot be transformed!" + - s"\nMissing field mapping: `$fromClassName`.? => `$toClassName`.`${missingExcludeMappingName.mkString(",")}`." + - s"\nPlease consider using `setName` or `setDefaultValue` method for `$toClassName`.${missingExcludeMappingName - .mkString(",")}!" - ) - } - } - val fields = toClassInfo.map { field => val fromFieldName = customFieldNameMapping.get(field.fieldName) val realToFieldName = fromFieldName.fold(field.fieldName)(x => x) @@ -179,14 +203,14 @@ class TransformerMacro(override val c: whitebox.Context) extends AbstractMacroPr case None if customFieldTypeMapping.contains(field.fieldName) => q"""${TermName(field.fieldName)} = ${TermName(builderFunctionPrefix + field.fieldName)}.apply(${q"$fromTermName.${TermName(realToFieldName)}"})""" case _ => - checkFieldGetFieldTerm[From]( + checkForNoMappingField[From, To]( realToFieldName, fromClassInfo.find(_.fieldName == realToFieldName), field, - customDefaultValueMapping + customFieldNameMapping ) } - } + }.filterNot(_ == EmptyTree) q""" ${toClassName.toTermName}.apply( ..$fields @@ -194,37 +218,57 @@ class TransformerMacro(override val c: whitebox.Context) extends AbstractMacroPr """ } - private def checkFieldGetFieldTerm[From: WeakTypeTag]( + private def checkForNoMappingField[From: WeakTypeTag, To: WeakTypeTag]( realFromFieldName: String, fromFieldOpt: Option[FieldInformation], toField: FieldInformation, - customDefaultValueMapping: mutable.Map[String, Any] + customFieldNameMapping: mutable.Map[String, String] ): Tree = { + val toClassInfo = getCaseClassFieldInfoList[To]() + val fromClassInfo = getCaseClassFieldInfoList[From]() + + val customOptionsMapping = + MacroCache.transformerOptionsMapping.getOrElse(getBuilderId(annoBuilderPrefix), mutable.Set.empty) + val customDefaultValueMapping = + MacroCache.classFieldDefaultValueMapping.getOrElse(getBuilderId(annoBuilderPrefix), mutable.Map.empty) + val fromFieldTerm = q"$fromTermName.${TermName(realFromFieldName)}" - val fromClassName = resolveClassTypeName[From] fromFieldOpt match { case Some(fromField) if !(fromField.fieldType weak_<:< toField.fieldType) => - tryForWrapType(fromFieldTerm, fromField, toField) + tryForCollectionType(fromFieldTerm, fromField, toField) case Some(fromField) if fromField.fieldType weak_<:< toField.fieldType => q"${TermName(toField.fieldName)} = $fromFieldTerm" - case None if !customDefaultValueMapping.keySet.contains(toField.fieldName) => - c.abort( - c.enclosingPosition, - s"The value `$realFromFieldName` is not a member of `$fromClassName`!" + - s"\nPlease consider using `setDefaultValue` method!" - ) - case _ => + case None if customDefaultValueMapping.keySet.contains(toField.fieldName) => val value = q"""${TermName(builderDefaultValuePrefix$ + toField.fieldName)}""" q"${TermName(toField.fieldName)} = $value" + case _ => + val isStrictCollection = toField.collectionFlags.isStrictCollection && customOptionsMapping.contains(Options.enableCollectionDefaultsToEmpty) + val isOption = toField.collectionFlags.isOption && customOptionsMapping.contains(Options.enableOptionDefaultsToNone) + if (isStrictCollection || isOption) { + q"${TermName(toField.fieldName)} = ${getZeroValue(toField.fieldType)}" + } else { + if (!toField.hasDefaultValue) { + checkMissingFields[From, To]( + fromClassInfo, + toClassInfo, + customDefaultValueMapping, + customFieldNameMapping, + customOptionsMapping + ) + EmptyTree + } else { + EmptyTree + } + } } } - private def tryForWrapType(fromFieldTerm: Tree, fromField: FieldInformation, toField: FieldInformation): Tree = + private def tryForCollectionType(fromFieldTerm: Tree, fromField: FieldInformation, toField: FieldInformation): Tree = (fromField, toField) match { case ( - FieldInformation(_, fromFieldType, collectionsFlags1, genericType1), - FieldInformation(_, toFieldType, collectionsFlags2, genericType2) + FieldInformation(_, fromFieldType, collectionsFlags1, genericType1, _, _), + FieldInformation(_, toFieldType, collectionsFlags2, genericType2, _, _) ) if ((collectionsFlags1.isSeq && collectionsFlags2.isSeq) || (collectionsFlags1.isList && collectionsFlags2.isList) || @@ -246,4 +290,36 @@ class TransformerMacro(override val c: whitebox.Context) extends AbstractMacroPr q"""${TermName(toField.fieldName)} = $packageName.Transformer[${information1.fieldType}, ${information2.fieldType}].transform($fromFieldTerm)""" } + private def checkMissingFields[From: WeakTypeTag, To: WeakTypeTag]( + fromClassInfo: List[FieldInformation], + toClassInfo: List[FieldInformation], + customDefaultValueMapping: mutable.Map[String, Any], + customFieldNameMapping: mutable.Map[String, String], + customOptionsMapping: mutable.Set[Options] + ) = { + val toClassName = resolveClassTypeName[To] + val fromClassName = resolveClassTypeName[From] + val missingFields = toClassInfo.filterNot(t => fromClassInfo.map(_.fieldName).contains(t.fieldName)) + val missingExcludeMappingName = missingFields.filterNot(m => customFieldNameMapping.contains(m.fieldName)) + if (missingExcludeMappingName.nonEmpty) { + val noDefaultValueFields = missingExcludeMappingName + .filterNot(m => customDefaultValueMapping.keySet.contains(m.fieldName)) + .filterNot(_.hasDefaultValue) + if (noDefaultValueFields.nonEmpty) { + // scalafmt: { maxColumn = 400 } + val needHandleFields = (if (customOptionsMapping.contains(Options.enableOptionDefaultsToNone)) { + noDefaultValueFields.filterNot(_.collectionFlags.isOption) + } else if (customOptionsMapping.contains(Options.enableCollectionDefaultsToEmpty)) { + noDefaultValueFields.filterNot(_.collectionFlags.isStrictCollection) + } else { + noDefaultValueFields + }).map(_.fieldName) + c.abort( + c.enclosingPosition, + s"Missing field mapping: `$fromClassName`.? => `$toClassName`.[${needHandleFields.mkString(",")}]." + + s"\nPlease consider using `setName`、`setDefaultValue` or `enable*` methods for `$toClassName`.[${needHandleFields.mkString(",")}]!" + ) + } + } + } } diff --git a/smt-common/src/test/scala/org/bitlap/common/TransformableTest.scala b/smt-common/src/test/scala/org/bitlap/common/TransformableTest.scala index 5a8857f9..4d30603f 100644 --- a/smt-common/src/test/scala/org/bitlap/common/TransformableTest.scala +++ b/smt-common/src/test/scala/org/bitlap/common/TransformableTest.scala @@ -384,4 +384,128 @@ class TransformableTest extends AnyFlatSpec with Matchers { | .instance |""".stripMargin shouldNot compile } + + "TransformableTest enable* method" should "ok" in { + case class A1(a: String, b: Int, cc: Long) + case class A2( + a: String, + b: Int, + c: Int, + d: Option[String], + e: List[String], + f: Seq[String], + g: Set[String], + h: Vector[String] + ) + + val a = A1("hello", 1, 2) + + implicit val b: Transformer[A1, A2] = Transformable[A1, A2] + .setName(_.cc, _.c) + .setType[Long, Int](_.cc, fromField => fromField.toInt) + .enableOptionDefaultsToNone + .enableCollectionDefaultsToEmpty + .instance + + a.transform[A2](b).toString shouldEqual "A2(hello,1,2,None,List(),List(),Set(),Vector())" + + implicit val b2: Transformer[A1, A2] = Transformable[A1, A2] + .setName(_.cc, _.c) + .setType[Long, Int](_.cc, fromField => fromField.toInt) + .setDefaultValue[Vector[String]]( + _.h, + Vector("Hello world") + ) // Higher priority than enableCollectionDefaultsToEmpty + .enableCollectionDefaultsToEmpty + .enableOptionDefaultsToNone + .instance + + a.transform[A2](b2).toString shouldEqual "A2(hello,1,2,None,List(),List(),Set(),Vector(Hello world))" + + implicit val b3: Transformer[A1, A2] = Transformable[A1, A2] + .setName(_.cc, _.c) + .setType[Long, Int](_.cc, fromField => fromField.toInt) + .setDefaultValue[Option[String]](_.d, Option("Hello world")) // Higher priority than enableOptionDefaultsToNone + .enableCollectionDefaultsToEmpty + .instance + + a.transform[A2](b3).toString shouldEqual "A2(hello,1,2,Some(Hello world),List(),List(),Set(),Vector())" + + implicit val b4: Transformer[A1, A2] = Transformable[A1, A2] + .setName(_.cc, _.c) + .setType[Long, Int](_.cc, fromField => fromField.toInt) + .setDefaultValue[Vector[String]]( + _.h, + Vector("Hello world1") + ) // Higher priority than enableCollectionDefaultsToEmpty + .setDefaultValue[Option[String]](_.d, Option("Hello world2")) // Higher priority than enableOptionDefaultsToNone + .enableOptionDefaultsToNone + .enableCollectionDefaultsToEmpty + .instance + + a.transform[A2](b4).toString shouldEqual "A2(hello,1,2,Some(Hello world2),List(),List(),Set(),Vector(Hello world1))" + + } + + "TransformableTest disable* method" should "ok" in { + case class A1(d: Option[String]) + case class A2( + d: Option[String], + e: Option[String] = Some("option"), + f: Option[String] = None, + h: List[String] = List("list"), + i: List[String] = List.empty + ) + + val a = A1(Some("hello a")) + implicit val b1: Transformer[A1, A2] = + Transformable[A1, A2].enableCollectionDefaultsToEmpty.enableOptionDefaultsToNone.instance + + a.transform[A2](b1).toString shouldEqual "A2(Some(hello a),None,None,List(),List())" + + implicit val b2: Transformer[A1, A2] = + Transformable[A1, A2] + // This method has a higher priority + .setDefaultValue(_.f, Option("1")) + .disableCollectionDefaultsToEmpty // use default value, not None + .disableOptionDefaultsToNone // use default value, not Empty + .instance + + a.transform[A2](b2).toString shouldEqual "A2(Some(hello a),Some(option),Some(1),List(list),List())" + + } + + "TransformableTest disable* is ok" should "compile ok" in { + case class A1(d: Option[String]) + case class A2( + d: Option[String], + e: Option[String] = Some("option"), + f: Option[String] = None, + h: List[String] = List("list"), + i: List[String] = List.empty + ) + + val a = A1(Some("hello a")) + implicit val b1: Transformer[A1, A2] = Transformable[A1, A2].instance + + a.transform[A2](b1).toString shouldEqual "A2(Some(hello a),Some(option),None,List(list),List())" + } + + "TransformableTest disable* is ok if no default value" should "compile failed" in { + """ + | case class A1(d: Option[String]) + | case class A2( + | d: Option[String], + | e: Option[String], + | f: Option[String] = None, + | h: List[String] = List("list"), + | i: List[String] = List.empty, + | ) + | + | val a = A1(Some("hello a")) + | implicit val b1: Transformer[A1, A2] = Transformable[A1, A2].instance + | + | a.transform[A2](b1) + |""".stripMargin shouldNot compile + } } diff --git a/smt-csv-derive/src/main/scala/org/bitlap/csv/derive/DeriveCsvConverter.scala b/smt-csv-derive/src/main/scala/org/bitlap/csv/derive/DeriveCsvConverter.scala index 199e2f24..7e994302 100644 --- a/smt-csv-derive/src/main/scala/org/bitlap/csv/derive/DeriveCsvConverter.scala +++ b/smt-csv-derive/src/main/scala/org/bitlap/csv/derive/DeriveCsvConverter.scala @@ -21,8 +21,8 @@ package org.bitlap.csv.derive +import org.bitlap.common.internal.AbstractMacroProcessor import scala.reflect.macros.blackbox -import org.bitlap.common.AbstractMacroProcessor import org.bitlap.csv.{ Converter, CsvFormat } /** This is a tool macro for automatic derivation of the base CSV converter. diff --git a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveCsvableBuilder.scala b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveCsvableBuilder.scala index 09180d8f..9fc44178 100644 --- a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveCsvableBuilder.scala +++ b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveCsvableBuilder.scala @@ -21,7 +21,8 @@ package org.bitlap.csv.macros -import org.bitlap.common.{ AbstractMacroProcessor, MacroCache } +import org.bitlap.common.MacroCache +import org.bitlap.common.internal.AbstractMacroProcessor import org.bitlap.csv.{ CsvFormat, CsvableBuilder } import java.io.File diff --git a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveScalableBuilder.scala b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveScalableBuilder.scala index 6cc2f682..5b84fa68 100644 --- a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveScalableBuilder.scala +++ b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveScalableBuilder.scala @@ -21,7 +21,8 @@ package org.bitlap.csv.macros -import org.bitlap.common.{ AbstractMacroProcessor, MacroCache } +import org.bitlap.common.MacroCache +import org.bitlap.common.internal.AbstractMacroProcessor import org.bitlap.csv.{ CsvFormat, ScalableBuilder } import java.io.InputStream @@ -160,7 +161,7 @@ class DeriveScalableBuilder(override val c: whitebox.Context) extends AbstractMa // scalafmt: { maxColumn = 400 } private def scalableBody[T: WeakTypeTag](clazzName: TypeName, innerFuncTermName: TermName): Tree = { val customTrees = MacroCache.builderFunctionTrees.getOrElse(getBuilderId(annoBuilderPrefix), mutable.Map.empty) - val params = getCaseClassFieldInfo[T]() + val params = getCaseClassFieldInfoList[T]() val fieldNames = params.map(_.fieldName) val fields = checkGetFieldTreeInformationList[T](innerFuncTermName).map { fieldTreeInformation => val idx = fieldTreeInformation.index diff --git a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToCaseClass.scala b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToCaseClass.scala index 917bb48f..ccb69d53 100644 --- a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToCaseClass.scala +++ b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToCaseClass.scala @@ -21,7 +21,7 @@ package org.bitlap.csv.macros -import org.bitlap.common.AbstractMacroProcessor +import org.bitlap.common.internal.AbstractMacroProcessor import org.bitlap.csv.CsvFormat import scala.reflect.macros.blackbox diff --git a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToString.scala b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToString.scala index 3c1eeebd..372e5116 100644 --- a/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToString.scala +++ b/smt-csv/src/main/scala/org/bitlap/csv/macros/DeriveToString.scala @@ -21,7 +21,7 @@ package org.bitlap.csv.macros -import org.bitlap.common.AbstractMacroProcessor +import org.bitlap.common.internal.AbstractMacroProcessor import org.bitlap.csv.CsvFormat import scala.reflect.macros.blackbox