diff --git a/core/src/main/scala/magnolia1/interface.scala b/core/src/main/scala/magnolia1/interface.scala index dfab9869..4dd63691 100644 --- a/core/src/main/scala/magnolia1/interface.scala +++ b/core/src/main/scala/magnolia1/interface.scala @@ -478,6 +478,16 @@ final class debug(typeNamePart: String = "") extends scala.annotation.StaticAnno private[magnolia1] final case class EarlyExit[E](e: E) extends Exception with util.control.NoStackTrace +trait Config { + type Proxy <: Singleton { type Typeclass[A] } + type Ignore <: annotation.Annotation + val readOnly: Boolean + val minFields: Int + val maxFields: Int + val minCases: Int + val maxCases: Int +} + object MagnoliaUtil { final def checkParamLengths(fieldValues: Seq[Any], paramsLength: Int, typeName: String): Unit = diff --git a/core/src/main/scala/magnolia1/magnolia.scala b/core/src/main/scala/magnolia1/magnolia.scala index 32db02f6..66169bda 100644 --- a/core/src/main/scala/magnolia1/magnolia.scala +++ b/core/src/main/scala/magnolia1/magnolia.scala @@ -7,6 +7,16 @@ import scala.collection.mutable import scala.language.higherKinds import scala.reflect.macros._ +private case class MagnoliaConfig[ProxyType, IgnoreType]( + proxyType: ProxyType, + ignoreType: IgnoreType, + readOnly: Boolean = false, + minFields: Int = -1, + maxFields: Int = Int.MaxValue, + minCases: Int = -1, + maxCases: Int = Int.MaxValue +) + /** the object which defines the Magnolia macro */ object Magnolia { import CompileTimeState._ @@ -42,19 +52,39 @@ object Magnolia { * split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] = ... will suffice, however the qualifications regarding * additional type parameters and implicit parameters apply equally to `split` as to `join`. */ - def gen[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = Stack.withContext(c) { (stack, depth) => + def genWith[T: c.WeakTypeTag, C <: Config with Singleton: c.WeakTypeTag](c: whitebox.Context): c.Tree = { + import c.universe._ + + val weakConfig = weakTypeOf[C] + val proxyType: c.Symbol = weakConfig.decl(TypeName("Proxy")) + val ignoreType: c.Type = weakConfig.decl(TypeName("Ignore")).info + val NullaryMethodType(ConstantType(Constant(readOnly: Boolean))) = weakConfig.decl(TermName("readOnly")).info + val NullaryMethodType(ConstantType(Constant(minFields: Int))) = weakConfig.decl(TermName("minFields")).info + val NullaryMethodType(ConstantType(Constant(maxFields: Int))) = weakConfig.decl(TermName("maxFields")).info + val NullaryMethodType(ConstantType(Constant(minCases: Int))) = weakConfig.decl(TermName("minCases")).info + val NullaryMethodType(ConstantType(Constant(maxCases: Int))) = weakConfig.decl(TermName("maxCases")).info + + genMacro[T, c.Symbol, c.Type](c, Some(MagnoliaConfig(proxyType, ignoreType, readOnly, minFields, maxFields, minCases, maxCases))) + } + + def gen[T: c.WeakTypeTag](c: whitebox.Context): c.Tree = { + genMacro(c, None) + } + + private def genMacro[T: c.WeakTypeTag, ProxyType, IgnoreType](c: whitebox.Context, config: Option[MagnoliaConfig[ProxyType, IgnoreType]]): c.Tree = Stack.withContext(c) { (stack, depth) => import c.internal._ import c.universe._ import definitions._ val genericType = weakTypeOf[T] + val genericSymbol = genericType.typeSymbol def error(message: => String): Nothing = c.abort(c.enclosingPosition, if (depth > 1) "" else s"magnolia: $message") def warning(message: String): Unit = c.warning(c.enclosingPosition, s"magnolia: $message") val prefixType = c.prefix.tree.tpe - val prefixObject = prefixType.typeSymbol + val prefixObject = config.map(_.proxyType.asInstanceOf[c.Symbol]).getOrElse(prefixType.typeSymbol) val prefixName = prefixObject.name.decodedName val TypeClassNme = TypeName("Typeclass") @@ -97,6 +127,34 @@ object Magnolia { val SubtypeTpe = typeOf[Subtype[Any, Any]].typeConstructor val TypeNameObj = reify(magnolia1.TypeName).tree + def assertFieldsLimits(caseClassParameters: List[TermSymbol]): Unit = { + val minLimit = config.map(_.minFields).getOrElse(-1) + val maxLimit = config.map(_.maxFields).getOrElse(-1) + val fieldsNumber = caseClassParameters.size + + if (minLimit > -1 && fieldsNumber < minLimit) { + error(s"Case class ${genericSymbol.name} has $fieldsNumber fields which is less than required minimum: $minLimit") + } + + if (maxLimit > -1 && fieldsNumber > maxLimit) { + error(s"Case class ${genericSymbol.name} has $fieldsNumber fields which is above the required maximum: $maxLimit") + } + } + + def assertCasesLimits(subtypes: List[Type]): Unit = { + val minLimit = config.map(_.minCases).getOrElse(-1) + val maxLimit = config.map(_.maxCases).getOrElse(-1) + val casesNumber = subtypes.size + + if (minLimit > -1 && casesNumber < minLimit) { + error(s"Sealed trait ${genericSymbol.name} has $casesNumber subtypes which is less than required minimum: $minLimit") + } + + if (maxLimit > -1 && casesNumber > maxLimit) { + error(s"Sealed trait ${genericSymbol.name} has $casesNumber fields which is above the required maximum: $maxLimit") + } + } + val debug = c.macroApplication.symbol.annotations .find(_.tree.tpe <:< DebugTpe) .flatMap(_.tree.children.tail.collectFirst { @@ -318,6 +376,12 @@ object Magnolia { val resultType = appliedType(typeConstructor, genericType) val typeName = c.freshName(TermName("typeName")) + def isIgnored(termSymbol: c.universe.TermSymbol): Boolean = { + config.map(_.ignoreType).exists { ignoreType => + termSymbol.annotations.map(_.tree.symbol).contains(ignoreType) + } + } + def typeNameOf(tpe: Type): Tree = { val symbol = tpe.typeSymbol val typeArgNames = for (typeArg <- tpe.typeArgs) yield typeNameOf(typeArg) @@ -383,10 +447,15 @@ object Magnolia { .map(_.map(_.asTerm)) val caseClassParameters = genericType.decls.sorted.collect( - if (isValueClass) { case p: TermSymbol if p.isParamAccessor && p.isMethod => p } - else { case p: TermSymbol if p.isCaseAccessor && !p.isMethod => p } + if (isValueClass) { + case p: TermSymbol if p.isParamAccessor && p.isMethod => p + } else { + case p: TermSymbol if p.isCaseAccessor && !p.isMethod && !isIgnored(p) => p + } ) + assertFieldsLimits(caseClassParameters) + val (factoryObject, factoryMethod) = { if (isReadOnly && isValueClass) ReadOnlyParamObj -> TermName("valueParam") else if (isReadOnly) ReadOnlyParamObj -> TermName("apply") @@ -567,6 +636,8 @@ object Magnolia { if (applied <:< genericType) existentialAbstraction(typeParams, applied) :: Nil else Nil } + assertCasesLimits(subtypes) + if (subtypes.isEmpty) { error(s"could not find any direct subtypes of $typeSymbol") } diff --git a/examples/src/main/scala/magnolia1/examples/csv.scala b/examples/src/main/scala/magnolia1/examples/csv.scala index 2bc74de2..3e003947 100644 --- a/examples/src/main/scala/magnolia1/examples/csv.scala +++ b/examples/src/main/scala/magnolia1/examples/csv.scala @@ -1,6 +1,6 @@ package magnolia1.examples -import magnolia1.{CaseClass, Magnolia, SealedTrait} +import magnolia1._ import scala.language.experimental.macros @@ -8,6 +8,16 @@ trait Csv[A] { def apply(a: A): List[String] } +object CsvConfig extends Config { + type Proxy = Csv.type + type Ignore = transient + final val readOnly = true + final val minFields = -1 + final val maxFields = -1 + final val minCases = -1 + final val maxCases = -1 +} + object Csv { type Typeclass[A] = Csv[A] @@ -22,9 +32,13 @@ object Csv { def apply(a: A): List[String] = ctx.split(a)(sub => sub.typeclass(sub.cast(a))) } - implicit def deriveCsv[A]: Csv[A] = macro Magnolia.gen[A] + @transient + val ignoreMe: String = "ignored value" + + implicit def deriveCsv[A]: Csv[A] = macro Magnolia.genWith[A, CsvConfig.type] implicit val csvStr: Csv[String] = new Csv[String] { def apply(a: String): List[String] = List(a) } + } diff --git a/test/src/test/scala/magnolia1/tests/tests.scala b/test/src/test/scala/magnolia1/tests/tests.scala index 1cd5f820..f2c10e2e 100644 --- a/test/src/test/scala/magnolia1/tests/tests.scala +++ b/test/src/test/scala/magnolia1/tests/tests.scala @@ -52,7 +52,10 @@ sealed trait AttributeParent case class `%%`(`/`: Int, `#`: String) -case class Param(a: String, b: String) +case class Param(a: String, b: String) { + @transient + val daad = "Test" +} case class TestEntry(param: Param) object TestEntry { def apply(): TestEntry = TestEntry(Param("", ""))