Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implements magnolia config #353

Draft
wants to merge 4 commits into
base: scala2
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions core/src/main/scala/magnolia1/interface.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
79 changes: 75 additions & 4 deletions core/src/main/scala/magnolia1/magnolia.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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._
Expand Down Expand Up @@ -42,19 +52,39 @@ object Magnolia {
* split[T](sealedTrait: SealedTrait[Typeclass, T]): Typeclass[T] = ... </pre> 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")
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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")
}
Expand Down
18 changes: 16 additions & 2 deletions examples/src/main/scala/magnolia1/examples/csv.scala
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
package magnolia1.examples

import magnolia1.{CaseClass, Magnolia, SealedTrait}
import magnolia1._

import scala.language.experimental.macros

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]

Expand All @@ -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)
}

}
5 changes: 4 additions & 1 deletion test/src/test/scala/magnolia1/tests/tests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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("", ""))
Expand Down