Skip to content

Instantly share code, notes, and snippets.

@ghostdogpr
Last active January 10, 2025 01:38
Show Gist options
  • Save ghostdogpr/3b5bd33dd3356e16434db42595924bf4 to your computer and use it in GitHub Desktop.
Save ghostdogpr/3b5bd33dd3356e16434db42595924bf4 to your computer and use it in GitHub Desktop.
Code generation to create lenses
Compile / sourceGenerators / fileInputs ++= Seq(
baseDirectory.value.toGlob / "inputDir"
),
Compile / sourceGenerators ++= Seq(
Def.task {
val source: File = baseDirectory.value / "inputDir"
val lenses = CodeGenerator.generateLenses(source.toPath)
val file = (Compile / sourceManaged).value / "codegen" / "Lenses.scala"
IO.write(file, lenses)
Seq(file)
}.taskValue
)
import java.nio.file.Path
import scala.meta.*
object CodeGenerator {
import dialects.Scala213Source3
private val emptyTree = "".parse[Source].get
def generateLenses(path: Path): String = {
val files = os.walk(os.Path(path)).toList.filterNot(os.isDir)
val lenses: List[Defn.Object] =
files.flatMap { file =>
val input = Input.VirtualFile(path.toString, os.read(file))
val tree = input.parse[Source].getOrElse(emptyTree)
def getCaseClasses(tree: Tree, parentObject: Option[Term.Ref]): List[(Type, List[(Name, Type)])] =
tree match {
case i: Source => i.stats.flatMap(stat => getCaseClasses(stat, parentObject))
case i: Pkg => i.stats.flatMap(stat => getCaseClasses(stat, parentObject))
case i: Defn.Class if i.mods.exists(_.syntax.contains("@lenses")) =>
val className = Type.Name(i.name.value)
List(
(
parentObject.fold[Type](className)(parent => Type.Select(parent, className)),
i.ctor.paramClauses.toList.flatMap(_.values.flatMap(param => param.decltpe.map(param.name -> _)))
)
)
case i: Defn.Object =>
val objectName = Term.Name(i.name.value)
i.templ.stats.flatMap(stat =>
getCaseClasses(stat, Some(parentObject.fold[Term.Ref](objectName)(parent => Term.Select(parent, objectName))))
)
case _ => Nil
}
val caseClasses: List[(Type, List[(Name, Type)])] = getCaseClasses(tree, None)
caseClasses.map { case (parentType, params) =>
val paramLenses = params.map { case (paramName, paramType) =>
val lensName = Term.Name(paramName.value)
q"val ${Pat.Var(lensName)}: Lens[$parentType, $paramType] = GenLens[$parentType](_.$lensName)"
}
val name = parentType.toString().split('.').mkString
q"""
object ${Term.Name(s"${name}Lens")} {
..$paramLenses
}
"""
}
}
val output = q"""
package com.example
// add needed imports here
import monocle.macros.GenLens
import monocle.Lens
..$lenses
"""
attachComment(output.syntax, path.toString)
}
def attachComment(syntax: String, source: String): String =
s"""// Generated by project/CodeGenerator.scala.
|// Source: $source
|// Do not edit!
|
|$syntax""".stripMargin
}
class lenses extends scala.annotation.StaticAnnotation
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment