All Downloads are FREE. Search and download functionalities are using the official Maven repository.

arrow.optics.OpticsProcessor.kt Maven / Gradle / Ivy

package arrow.optics

import arrow.common.utils.AbstractProcessor
import arrow.common.utils.isSealed
import arrow.common.utils.knownError
import arrow.common.utils.removeBackticks
import arrow.optics.OpticsProcessor.ClassType.DATA_CLASS
import arrow.optics.OpticsProcessor.ClassType.OTHER
import arrow.optics.OpticsProcessor.ClassType.SEALED_CLASS
import arrow.optics.OpticsTarget.DSL
import arrow.optics.OpticsTarget.ISO
import arrow.optics.OpticsTarget.LENS
import arrow.optics.OpticsTarget.OPTIONAL
import arrow.optics.OpticsTarget.PRISM
import com.google.auto.service.AutoService
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import java.io.File
import javax.annotation.processing.Processor
import javax.annotation.processing.RoundEnvironment
import javax.lang.model.SourceVersion
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement

@AutoService(Processor::class)
class OpticsProcessor : AbstractProcessor() {

  private val annotatedEles = mutableListOf()

  override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()

  override fun getSupportedAnnotationTypes() = setOf(opticsAnnotationClass.canonicalName)

  override fun onProcess(annotations: Set, roundEnv: RoundEnvironment) {
    roundEnv
      .getElementsAnnotatedWith(opticsAnnotationClass)
      .forEach { element ->
        if (element.classType == OTHER) knownError(element.otherClassTypeErrorMessage, element)
        if (element.hasNoCompanion) knownError("@optics annotated class $element needs to declare companion object.")

        val targets: List = element.normalizedTargets().map { target ->
          when (target) {
            LENS -> evalAnnotatedDataClass(element, element.lensErrorMessage).let(::LensTarget)
            OPTIONAL -> evalAnnotatedDataClass(element, element.optionalErrorMessage).let(::OptionalTarget)
            ISO -> evalAnnotatedIsoElement(element).let(::IsoTarget)
            PRISM -> evalAnnotatedPrismElement(element).let(::PrismTarget)
            DSL -> evalAnnotatedDslElement(element)
          }
        }

        annotatedEles.add(AnnotatedElement(element as TypeElement, element.getClassData(), targets))
      }

    if (roundEnv.processingOver()) {
      annotatedEles
        .forEach { ele ->
          ele.snippets()
            .groupBy(Snippet::fqName)
            .values
            .map {
              it.reduce { acc, snippet ->
                acc.copy(
                  imports = acc.imports + snippet.imports,
                  content = "${acc.content}\n${snippet.content}"
                )
              }
            }.forEach {
              val generatedDir = File("$generatedDir/${it.`package`.removeBackticks().replace(".", "/")}").also { it.mkdirs() }
              File(generatedDir, "${it.name.removeBackticks()}\$\$optics.kt").writeText(it.asFileText())
            }
        }
    }
  }

  private fun AnnotatedElement.snippets(): List = this.targets.map {
    when (it) {
      is IsoTarget -> generateIsos(this, it)
      is PrismTarget -> generatePrisms(this, it)
      is LensTarget -> generateLenses(this, it)
      is OptionalTarget -> generateOptionals(this, it)
      is SealedClassDsl -> generatePrismDsl(this, it)
      is DataClassDsl -> generateOptionalDsl(this, it) + generateLensDsl(this, it)
    }
  }

  private fun Element.normalizedTargets(): List = with(getAnnotation(opticsAnnotationClass).targets) {
    when {
      isEmpty() -> if (classType == SEALED_CLASS) listOf(PRISM, DSL) else listOf(ISO, LENS, OPTIONAL, DSL)
      else -> toList()
    }
  }

  private fun evalAnnotatedDataClass(element: Element, errorMessage: String): List = when (element.classType) {
    DATA_CLASS -> element.getConstructorTypesNames().zip(element.getConstructorParamNames(), Focus.Companion::invoke)
    else -> knownError(errorMessage, element)
  }

  private fun evalAnnotatedPrismElement(element: Element): List = when (element.classType) {
    SEALED_CLASS -> element.kotlinMetadata.let { it as KotlinClassMetadata }.data.let { (nameResolver, classProto) ->
      classProto.sealedSubclassFqNameList
        .map(nameResolver::getString)
        .map { it.replace('/', '.') }
        .map { Focus(it, it.substringAfterLast(".").decapitalize()) }
    }

    else -> knownError(element.prismErrorMessage, element)
  }

  private fun evalAnnotatedDslElement(element: Element): Target = when (element.classType) {
    DATA_CLASS -> DataClassDsl(element.getConstructorTypesNames().zip(element.getConstructorParamNames(), Focus.Companion::invoke))
    SEALED_CLASS -> SealedClassDsl(evalAnnotatedPrismElement(element))
    OTHER -> knownError(element.dslErrorMessage, element)
  }

  private fun evalAnnotatedIsoElement(element: Element): List = when (element.classType) {
    DATA_CLASS -> element.getConstructorTypesNames().zip(element.getConstructorParamNames(), Focus.Companion::invoke)
      .takeIf { it.size <= 22 } ?: knownError(element.isoTooBigErrorMessage, element)
    else -> knownError(element.isoErrorMessage, element)
  }

  private enum class ClassType {
    DATA_CLASS,
    SEALED_CLASS,
    OTHER;
  }

  private val Element.classType: ClassType
    get() = when {
      (kotlinMetadata as? KotlinClassMetadata)?.data?.classProto?.isDataClass == true -> DATA_CLASS
      (kotlinMetadata as? KotlinClassMetadata)?.data?.classProto?.isSealed == true -> SEALED_CLASS
      else -> OTHER
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy