Skip to content

Commit

Permalink
add member annotation reflection and create the full annotation struc…
Browse files Browse the repository at this point in the history
…tures
  • Loading branch information
Grifs committed Nov 7, 2024
1 parent b5bdee4 commit 343ecde
Show file tree
Hide file tree
Showing 3 changed files with 98 additions and 136 deletions.
72 changes: 72 additions & 0 deletions src/main/scala/io/viash/helpers/Mirroring.scala
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ inline def deprecatedFieldsOf[T]: Vector[(String, String, String, String)] = ${
inline def removedFieldsOf[T]: Vector[(String, String, String, String)] = ${ removedFieldsOfImpl[T] }
inline def annotationsOf[T]: List[(String, List[String])] = ${ annotationsOfImpl[T] }
inline def membersOf[T]: List[String] = ${ membersOfImpl[T] }
inline def memberAnnotationsOf[T]: List[(String, List[(String, List[String])])] = ${ memberAnnotationsOfImpl[T] }
inline def historyOf[T]: List[String] = ${ historyOfImpl[T] }

def typeOfImpl[T: Type](using Quotes): Expr[String] =
import quotes.reflect.*
Expand Down Expand Up @@ -191,3 +193,73 @@ def membersOfImpl[T: Type](using Quotes): Expr[List[String]] = {
val memberSymbols = tpe.fieldMembers.map(_.name)
Expr(memberSymbols)
}

def memberAnnotationsOfImpl[T: Type](using Quotes): Expr[List[(String, List[(String, List[String])])]] = {
import quotes.reflect.*
val tpe = TypeRepr.of[T].typeSymbol
val memberSymbols = tpe.fieldMembers

def unfinishedStringStripMargin(s: String, marginChar: Char = '|'): String = {
s.replaceAll("\\\\n", "\n").stripMargin(marginChar)
}

def mapTreeList(l: List[Tree], marginChar: Char = '|'): String = {
l.map(i => i match {
// case Literal(Constant(value: String)) =>
// unfinishedStringStripMargin(value, marginChar)
case Literal(value) =>
unfinishedStringStripMargin(value.show(using Printer.ConstantCode), marginChar)
case _ =>
"unmatched in mapTreeList: " + i.toString()
}).mkString
}

// Traverse tree information and extract values or lists of values
def annotationToStrings(ann: Term): List[String] = {
// val name = ann.tree.tpe.toString()
val values = ann match {
case Apply(c, args: List[Tree]) =>
args.collect({
case i: Tree =>
i match {
// Here 'Apply' contains lists
// While 'Select' has a single element
// case Literal(Constant(value: String)) =>
// value
case Literal(value) =>
value.show(using Printer.ConstantCode)
// case Select(Select(a, b), stripMargin) =>
// unfinishedStringStripMargin(b)
case Select(Apply(a, a2), b) if b.toString == "stripMargin" =>
mapTreeList(a2)
case Apply(Select(Apply(a, a2), b), stripMargin) if b.toString == "stripMargin" =>
val stripper = stripMargin.head.toString.charAt(1)
mapTreeList(a2, stripper)
case _ =>
"unmatched in annotationToStrings: " + i.toString()
}
})
}
values
}

val annots =
memberSymbols
.map{ case m =>
val name = m.name
val n: Symbol = m
val annotations = m.annotations
.filter(_.tpe.typeSymbol.fullName.startsWith("io.viash"))
.map(ann => (ann.tpe.typeSymbol.name, annotationToStrings(ann)))
(name, annotations)
}

Expr(annots)
}

def historyOfImpl[T: Type](using Quotes): Expr[List[String]] = {
import quotes.reflect.*
val baseClasses = TypeRepr.of[T].baseClasses.map(_.fullName).filter(_.startsWith("io.viash"))

Expr(baseClasses)
}
95 changes: 14 additions & 81 deletions src/main/scala/io/viash/schemas/CollectedSchemas.scala
Original file line number Diff line number Diff line change
Expand Up @@ -84,62 +84,19 @@ object CollectedSchemas {
private implicit val encodeDeprecatedOrRemoved: Encoder.AsObject[DeprecatedOrRemovedSchema] = deriveConfiguredEncoder
private implicit val encodeExample: Encoder.AsObject[ExampleSchema] = deriveConfiguredEncoder

private inline final def summonLabels[T <: Tuple]: List[String] =
inline erasedValue[T] match
case _: EmptyTuple => Nil
case _: (t *: ts) => constValue[t].asInstanceOf[String] :: summonLabels[ts]

private inline def getMembers[T /*TypeTag*/]/*(using mirror: Mirror.Of[T])*/(): (Map[String,List[MemberInfo]], List[Symbol]) = {

// val name: String = constValue[mirror.MirroredLabel]
val name: String = typeOf[T]

// Get all members and filter for constructors, first one should be the best (most complete) one
// Traits don't have constructors
// Get all parameters and store their short name
// val constructorMembers = typeOf[T].members.filter(_.isConstructor).headOption.map(_.asMethod.paramLists.head.map(_.shortName)).getOrElse(List.empty[String])
// val constructorMembers = summonLabels[mirror.MirroredElemLabels]
val constructorMembers = fieldsOf[T]

// val baseClasses = typeOf[T].baseClasses
// .filter(_.fullName.startsWith("io.viash"))

// // If we're only getting a abstract class/trait, not a final implementation, use these definitions (otherwise we're left with nothing).
// val documentFully =
// baseClasses.length == 1 &&
// baseClasses.head.isAbstract &&
// baseClasses.head.annotations.exists(a => a.tree.tpe =:= typeOf[documentFully])

// val memberNames = typeOf[T].members
// .filter(!_.isMethod || documentFully)
// .map(_.shortName)
// .toSeq
val memberNames = membersOf[T]

// val allMembers = baseClasses
// .zipWithIndex
// .flatMap{ case (baseClass, index) =>
// baseClass.info.members
// .filter(_.fullName.startsWith("io.viash"))
// .filter(m => memberNames.contains(m.shortName))
// .filter(m => !m.info.getClass.toString.endsWith("NullaryMethodType") || index != 0 || documentFully) // Only regular members if base class, otherwise all members
// .map(y => MemberInfo(y, (constructorMembers.contains(y.shortName)), baseClass.fullName, index))
// }
// .groupBy(k => k.shortName)

// (allMembers, baseClasses)

// println(s"name: $name")
// println(s"constructorMembers: $constructorMembers")
// println(s"baseClasses: $baseClasses")
// println(s"documentFully: $documentFully")
// println(s"memberNames: $memberNames")
// println(s"allMembers: $allMembers")

(Map.empty, Nil)
private def getMembers[T](): List[ParameterSchema] = {
val tpe = typeOf[T]
val history = historyOf[T]
val annotations = annotationsOf[T]
val thisMembers = ParameterSchema("__this__", tpe, history, annotations)

val memberAnnotations = memberAnnotationsOf[T].map({ case (memberName, memberAnns) =>
ParameterSchema(memberName, tpe, Nil, memberAnns)
})
thisMembers +: memberAnnotations
}

lazy val schemaClasses = List(
lazy val fullData = List(
getMembers[Config](),
getMembers[PackageConfig](),
getMembers[BuildInfo](),
Expand Down Expand Up @@ -223,32 +180,7 @@ object CollectedSchemas {
.replaceAll("""(\w*)\[[\w\.]*?(\w*),[\w\.]*?(\w*)\]""", "$1[$2,$3]")
}

private def annotationsOf(members: (Map[String,List[MemberInfo]]), classes: List[Symbol]): List[(String, String, List[String], List[Annotation])] = {
// val annMembers = members
// .map{ case (memberName, memberInfo) => {
// val h = memberInfo.head
// val annotations = memberInfo.flatMap(_.symbol.annotations)
// (h.fullName, h.symbol.info.toString, annotations, h.className, h.inheritanceIndex, Nil)
// } }
// .filter(_._3.length > 0)
// val annThis = ("__this__", classes.head.name.toString(), classes.head.annotations, "", 0, classes.map(_.fullName))
// val allAnnotations = annThis :: annMembers.toList
// allAnnotations
// .map({case (name, tpe, annotations, d, e, hierarchy) => (name, trimTypeName(tpe), hierarchy, annotations)}) // TODO this ignores where the annotation was defined, ie. top level class or super class
Nil
}

private val getSchema: ((Map[String,List[MemberInfo]], List[Symbol])) => List[ParameterSchema] = (t: (Map[String,List[MemberInfo]], List[Symbol])) => t match {
case (members, classes) => {
annotationsOf(members, classes).map{ case (name, tpe, hierarchy, annotations) => ParameterSchema(name, tpe, hierarchy, annotations) }
}
}

// get all parameters for a given type, including parent class annotations
// def getParameters[T: TypeTag](): List[ParameterSchema] = getSchema(getMembers[T]())

// Main call for documentation output
lazy val fullData: List[List[ParameterSchema]] = schemaClasses.map{ v => getSchema(v)}
lazy val data: List[List[ParameterSchema]] = fullData.map(_.filter(p => !p.hasUndocumented && !p.hasInternalFunctionality))

def getKeyFromParamList(data: List[ParameterSchema]): String = data.find(p => p.name == "__this__").get.`type`
Expand All @@ -272,8 +204,9 @@ object CollectedSchemas {

// Main call for checking whether all arguments are annotated
// Add extra non-annotated value so we can always somewhat check the code is functional
def getAllNonAnnotated: Map[String, String] = (schemaClasses :+ getMembers[CollectedSchemas]()).flatMap {
v => getNonAnnotated(v._1, v._2).map((getMemberName(v._1, v._2), _))
def getAllNonAnnotated: Map[String, String] = (fullData :+ getMembers[CollectedSchemas]()).flatMap {
// v => getNonAnnotated(v._1, v._2).map((getMemberName(v._1, v._2), _))
_ => Nil
}.toMap

def getAllDeprecations: Map[String, DeprecatedOrRemovedSchema] = {
Expand Down
67 changes: 12 additions & 55 deletions src/main/scala/io/viash/schemas/ParameterSchema.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,49 +40,8 @@ final case class ParameterSchema(
)

object ParameterSchema {
// Aid processing `augmentString` strings
private def unfinishedStringStripMargin(s: String, marginChar: Char = '|'): String = {
s.replaceAll("\\\\n", "\n").stripMargin(marginChar)
}

// private def mapTreeList(l: List[Tree], marginChar: Char = '|'): String = {
// l.map(i => i match {
// case Literal(Constant(value: String)) =>
// unfinishedStringStripMargin(value, marginChar)
// case _ =>
// "unmatched in mapTreeList: " + i.toString()
// }).mkString
// }

// Traverse tree information and extract values or lists of values
// private def annotationToStrings(ann: Annotation):(String, List[String]) = {
// val name = ann.tree.tpe.toString()
// val values = ann.tree match {
// case Apply(c, args: List[Tree]) =>
// args.collect({
// case i: Tree =>
// i match {
// // Here 'Apply' contains lists
// // While 'Select' has a single element
// case Literal(Constant(value: String)) =>
// value
// // case Select(Select(a, b), stripMargin) =>
// // unfinishedStringStripMargin(b)
// case Select(Apply(a, a2), b) if b.toString == "stripMargin" =>
// mapTreeList(a2)
// case Apply(Select(Apply(a, a2), b), stripMargin) if b.toString == "stripMargin" =>
// val stripper = stripMargin.head.toString.charAt(1)
// mapTreeList(a2, stripper)
// case _ =>
// "unmatched in annotationToStrings: " + i.toString()
// }
// })
// }
// (name, values)
// }

def apply(name: String, `type`: String, hierarchy: List[String], annotations: List[Annotation]): ParameterSchema = {
println(s"ParameterSchema: $name, ${`type`}, $hierarchy, $annotations")
def apply(name: String, `type`: String, hierarchy: List[String], annotations: List[(String, List[String])]): ParameterSchema = {

def beautifyTypeName(s: String): String = {

Expand Down Expand Up @@ -120,8 +79,6 @@ object ParameterSchema {
}
}

// val annStrings = annotations.map(annotationToStrings(_))
val annStrings = List[(String, List[String])]() // TODO
val hierarchyOption = hierarchy match {
case l if l.length > 0 => Some(l)
case _ => None
Expand All @@ -130,7 +87,7 @@ object ParameterSchema {
// name is e.g. "io.viash.config.Config.name", only keep "name"
// name can also be "__this__"
// Use the name defined from the class, *unless* the 'nameOverride' annotation is set. Then use the override, unless the name is '__this__'.
val nameOverride = annStrings.collectFirst({case (name, value) if name.endsWith("nameOverride") => value.head})
val nameOverride = annotations.collectFirst({case (name, value) if name.endsWith("nameOverride") => value.head})
val nameFromClass = name.split('.').last
val name_ = (nameOverride, nameFromClass) match {
case (Some(_), "__this__") => "__this__"
Expand All @@ -143,26 +100,26 @@ object ParameterSchema {
case (typeName, _, _) => typeName
}

val description = annStrings.collectFirst({case (name, value) if name.endsWith("description") => value.head})
val example = annStrings.collect({case (name, value) if name.endsWith("example") => value}).map(ExampleSchema(_))
val exampleWithDescription = annStrings.collect({case (name, value) if name.endsWith("exampleWithDescription") => value}).map(ExampleSchema(_))
val description = annotations.collectFirst({case (name, value) if name.endsWith("description") => value.head})
val example = annotations.collect({case (name, value) if name.endsWith("example") => value}).map(ExampleSchema(_))
val exampleWithDescription = annotations.collect({case (name, value) if name.endsWith("exampleWithDescription") => value}).map(ExampleSchema(_))
val examples = example ::: exampleWithDescription match {
case l if l.length > 0 => Some(l)
case _ => None
}
val since = annStrings.collectFirst({case (name, value) if name.endsWith("since") => value.head})
val deprecated = annStrings.collectFirst({case (name, value) if name.endsWith("deprecated") => value}).map(DeprecatedOrRemovedSchema(_))
val removed = annStrings.collectFirst({case (name, value) if name.endsWith("removed") => value}).map(DeprecatedOrRemovedSchema(_))
val defaultFromAnnotation = annStrings.collectFirst({case (name, value) if name.endsWith("default") => value.head})
val since = annotations.collectFirst({case (name, value) if name.endsWith("since") => value.head})
val deprecated = annotations.collectFirst({case (name, value) if name.endsWith("deprecated") => value}).map(DeprecatedOrRemovedSchema(_))
val removed = annotations.collectFirst({case (name, value) if name.endsWith("removed") => value}).map(DeprecatedOrRemovedSchema(_))
val defaultFromAnnotation = annotations.collectFirst({case (name, value) if name.endsWith("default") => value.head})
val defaultFromType = Option.when(typeName.startsWith("Option["))("Empty")
val default = defaultFromAnnotation orElse defaultFromType
val subclass = annStrings.collect{ case (name, value) if name.endsWith("subclass") => value.head } match {
val subclass = annotations.collect{ case (name, value) if name.endsWith("subclass") => value.head } match {
case l if l.nonEmpty => Some(l)
case _ => None
}

val undocumented = annStrings.exists{ case (name, value) => name.endsWith("undocumented")}
val internalFunctionality = annStrings.exists{ case (name, value) => name.endsWith("internalFunctionality")}
val undocumented = annotations.exists{ case (name, value) => name.endsWith("undocumented")}
val internalFunctionality = annotations.exists{ case (name, value) => name.endsWith("internalFunctionality")}

ParameterSchema(name_, typeName, beautifyTypeName(typeName), hierarchyOption, description, examples, since, deprecated, removed, default, subclass, undocumented, internalFunctionality)
}
Expand Down

0 comments on commit 343ecde

Please sign in to comment.