Skip to content

Commit

Permalink
Improve optics processor (#852)
Browse files Browse the repository at this point in the history
* fix AP tests

* Add OpticsProcessor check to verify unnamed companion object

* Generate single file per annotated element

* Re-enabled grained control for DSL generation

* Small clean up Optics processors

* Remove unused reflection usage

* Update formatting generated code Lens & Iso

* Fix tests

* Update DSLTest with new syntax

* Update Snippet so it belongs to an annotated target

* Generate file per annotated target in corresponding directory

* Test code generation in correct folder structure
  • Loading branch information
nomisRev authored and raulraja committed Jun 10, 2018
1 parent d7c1931 commit 5cae954
Show file tree
Hide file tree
Showing 16 changed files with 334 additions and 401 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,85 @@ import arrow.common.utils.fullName
import me.eugeniomarletti.kotlin.metadata.escapedClassName
import javax.lang.model.element.TypeElement

data class AnnotatedOptic(val type: TypeElement, val classData: ClassOrPackageDataWrapper.Class, val targets: List<Target>) {
data class AnnotatedElement(val type: TypeElement, val classData: ClassOrPackageDataWrapper.Class, val targets: List<Target>) {
val sourceClassName = classData.fullName.escapedClassName
val sourceName = type.simpleName.toString().decapitalize()
val targetNames = targets.map(Target::fullName)
val hasTupleFocus: Boolean = targets.size > 1
val focusSize: Int = targets.size
val packageName = classData.`package`.escapedClassName

operator fun Snippet.plus(snippet: Snippet): Snippet = copy(
imports = imports + snippet.imports,
content = "$content\n${snippet.content}"
)
}

typealias IsoTarget = Target.Iso
typealias PrismTarget = Target.Prism
typealias LensTarget = Target.Lens
typealias OptionalTarget = Target.Optional
typealias SealedClassDsl = Target.SealedClassDsl
typealias DataClassDsl = Target.DataClassDsl

sealed class Target {
abstract val foci: List<Focus>

data class Iso(override val foci: List<Focus>) : Target()
data class Prism(override val foci: List<Focus>) : Target()
data class Lens(override val foci: List<Focus>) : Target()
data class Optional(override val foci: List<Focus>) : Target()
data class SealedClassDsl(override val foci: List<Focus>) : Target()
data class DataClassDsl(override val foci: List<Focus>) : Target()
}

typealias NonNullFocus = Focus.NonNull
typealias OptionFocus = Focus.Option
typealias NullableFocus = Focus.Nullable

sealed class Focus {

companion object {
operator fun invoke(fullName: String, paramName: String): Target = when {
fullName.endsWith("?") -> NullableTarget(fullName, paramName)
fullName.startsWith("`arrow`.`core`.`Option`") -> OptionTarget(fullName, paramName)
else -> NonNullTarget(fullName, paramName)
operator fun invoke(fullName: String, paramName: String): Focus = when {
fullName.endsWith("?") -> Nullable(fullName, paramName)
fullName.startsWith("`arrow`.`core`.`Option`") -> Option(fullName, paramName)
else -> NonNull(fullName, paramName)
}
}

abstract val fullName: String
abstract val className: String
abstract val paramName: String

data class NullableTarget(override val fullName: String, override val paramName: String) : Target() {
val nonNullFullName = fullName.dropLast(1)
data class Nullable(override val className: String, override val paramName: String) : Focus() {
val nonNullClassName = className.dropLast(1)
}

data class OptionTarget(override val fullName: String, override val paramName: String) : Target() {
val nestedFullName = Regex("`arrow`.`core`.`Option`<(.*)>$").matchEntire(fullName)!!.groupValues[1]
data class Option(override val className: String, override val paramName: String) : Focus() {
val nestedClassName = Regex("`arrow`.`core`.`Option`<(.*)>$").matchEntire(className)!!.groupValues[1]
}

data class NonNullTarget(override val fullName: String, override val paramName: String) : Target()
data class NonNull(override val className: String, override val paramName: String) : Focus()

}

const val Lens = "arrow.optics.Lens"
const val Iso = "arrow.optics.Iso"
const val Optional = "arrow.optics.Optional"
const val Prism = "arrow.optics.Prism"
const val Getter = "arrow.optics.Getter"
const val Setter = "arrow.optics.Setter"
const val Traversal = "arrow.optics.Traversal"
const val Fold = "arrow.optics.Fold"
const val Tuple = "arrow.core.Tuple"

data class Snippet(
val `package`: String,
val name: String,
val imports: Set<String> = emptySet(),
val content: String
) {
val fqName = "$`package`.$name"
}

val Lens = "arrow.optics.Lens"
val Iso = "arrow.optics.Iso"
val Optional = "arrow.optics.Optional"
val Prism = "arrow.optics.Prism"
val Getter = "arrow.optics.Getter"
val Setter = "arrow.optics.Setter"
val Traversal = "arrow.optics.Traversal"
val Fold = "arrow.optics.Fold"
fun Snippet.asFileText(): String = """
|package $`package`
|${imports.joinToString(prefix = "\n", separator = "\n", postfix = "\n")}
|$content
""".trimMargin()
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,3 @@ package arrow.optics

val opticsAnnotationKClass = optics::class
val opticsAnnotationClass = opticsAnnotationKClass.java
val opticsAnnotationName = "@" + opticsAnnotationKClass.simpleName
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package arrow.optics

import arrow.common.utils.simpleName

fun generateLensDsl(ele: AnnotatedElement, optic: DataClassDsl) = Snippet(
`package` = ele.packageName,
name = ele.classData.simpleName,
content = processLensSyntax(ele, optic.foci)
)

fun generateOptionalDsl(ele: AnnotatedElement, optic: DataClassDsl) = Snippet(
`package` = ele.packageName,
name = ele.classData.simpleName,
content = processOptionalSyntax(ele, optic)
)

fun generatePrismDsl(ele: AnnotatedElement, isoOptic: SealedClassDsl) = Snippet(
`package` = ele.packageName,
name = ele.classData.simpleName,
content = processPrismSyntax(ele, isoOptic)
)

private fun processLensSyntax(ele: AnnotatedElement, foci: List<Focus>): String = foci.joinToString(separator = "\n") { focus ->
"""
|inline val <S> $Iso<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Lens<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|inline val <S> $Lens<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Lens<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|inline val <S> $Optional<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Optional<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|inline val <S> $Prism<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Optional<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|inline val <S> $Getter<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Getter<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|inline val <S> $Setter<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Setter<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|inline val <S> $Traversal<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Traversal<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|inline val <S> $Fold<S, ${ele.sourceClassName}>.${focus.lensParamName()}: $Fold<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.lensParamName()}
|""".trimMargin()
}

private fun processOptionalSyntax(ele: AnnotatedElement, optic: DataClassDsl) = optic.foci.filterNot { it is NonNullFocus }.joinToString(separator = "\n") { focus ->
val targetClassName = when (focus) {
is NullableFocus -> focus.nonNullClassName
is OptionFocus -> focus.nestedClassName
is NonNullFocus -> ""
}

"""
|inline val <S> $Iso<S, ${ele.sourceClassName}>.${focus.paramName}: $Optional<S, $targetClassName> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Lens<S, ${ele.sourceClassName}>.${focus.paramName}: $Optional<S, $targetClassName> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Optional<S, ${ele.sourceClassName}>.${focus.paramName}: $Optional<S, $targetClassName> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Prism<S, ${ele.sourceClassName}>.${focus.paramName}: $Optional<S, $targetClassName> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Setter<S, ${ele.sourceClassName}>.${focus.paramName}: $Setter<S, $targetClassName> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Traversal<S, ${ele.sourceClassName}>.${focus.paramName}: $Traversal<S, $targetClassName> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Fold<S, ${ele.sourceClassName}>.${focus.paramName}: $Fold<S, $targetClassName> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|""".trimMargin()
}

private fun processPrismSyntax(ele: AnnotatedElement, dsl: SealedClassDsl): String = dsl.foci.joinToString(separator = "\n\n") { focus ->
"""
|inline val <S> $Iso<S, ${ele.sourceClassName}>.${focus.paramName}: $Prism<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Lens<S, ${ele.sourceClassName}>.${focus.paramName}: $Optional<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Optional<S, ${ele.sourceClassName}>.${focus.paramName}: $Optional<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Prism<S, ${ele.sourceClassName}>.${focus.paramName}: $Prism<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Setter<S, ${ele.sourceClassName}>.${focus.paramName}: $Setter<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Traversal<S, ${ele.sourceClassName}>.${focus.paramName}: $Traversal<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|inline val <S> $Fold<S, ${ele.sourceClassName}>.${focus.paramName}: $Fold<S, ${focus.className}> inline get() = this + ${ele.sourceClassName}.${focus.paramName}
|""".trimMargin()
}
Original file line number Diff line number Diff line change
@@ -1,52 +1,38 @@
package arrow.optics

import me.eugeniomarletti.kotlin.metadata.escapedClassName
import arrow.common.utils.simpleName
import me.eugeniomarletti.kotlin.metadata.plusIfNotBlank
import java.io.File

class IsosFileGenerator(
private val annotatedList: Collection<AnnotatedOptic>,
private val generatedDir: File
) {

private val filePrefix = "isos"
private val tuple = "arrow.core.Tuple"
private val letters = ('a'..'v').toList()

fun generate() = buildIsos(annotatedList)

private fun buildIsos(optics: Collection<AnnotatedOptic>) =
optics.map(this::processElement)
.forEach { (element, funString) ->
File(generatedDir, "$filePrefix.${element.classData.`package`}.${element.sourceName}.kt").printWriter().use { w ->
w.println(funString)
}
}

private fun processElement(iso: AnnotatedOptic): Pair<AnnotatedOptic, String> = iso to """
|package ${iso.classData.`package`.escapedClassName}
|
|inline val ${iso.sourceClassName}.Companion.iso: $Iso<${iso.sourceClassName}, ${focusType(iso)}> get()= $Iso(
| get = { ${iso.sourceName}: ${iso.sourceClassName} -> ${getFunction(iso)} },
| reverseGet = { ${reverseGetFunction(iso)} }
|)""".trimMargin()

private fun getFunction(iso: AnnotatedOptic) =
if (iso.hasTupleFocus) tupleConstructor(iso)
else "${iso.sourceName}.${iso.targets.first().paramName}"

private fun reverseGetFunction(iso: AnnotatedOptic) =
if (iso.hasTupleFocus) "tuple: ${focusType(iso)} -> ${classConstructorFromTuple(iso.sourceClassName, iso.focusSize)}"
else "${iso.sourceClassName}(it)"

private fun tupleConstructor(iso: AnnotatedOptic) =
iso.targets.joinToString(prefix = "$tuple${iso.focusSize}(", postfix = ")", transform = { "${iso.sourceName}.${it.paramName.plusIfNotBlank(prefix = "`", postfix = "`")}" })

private fun focusType(iso: AnnotatedOptic) =
if (iso.hasTupleFocus) iso.targetNames.joinToString(prefix = "$tuple${iso.targets.size}<", postfix = ">")
else iso.targetNames.first()

private fun classConstructorFromTuple(sourceClassName: String, propertiesSize: Int) =

fun generateIsos(ele: AnnotatedElement, target: IsoTarget) = Snippet(
`package` = ele.packageName,
name = ele.classData.simpleName,
content = processElement(ele, target)
)

inline val Target.targetNames inline get() = foci.map(Focus::className)

private fun processElement(iso: AnnotatedElement, target: Target): String {
val foci = target.foci
val hasTupleFocus = foci.size > 1
val letters = ('a'..'v').toList()

fun tupleConstructor() =
foci.joinToString(prefix = "$Tuple${foci.size}(", postfix = ")", transform = { "${iso.sourceName}.${it.paramName.plusIfNotBlank(prefix = "`", postfix = "`")}" })

fun focusType() =
if (hasTupleFocus) target.targetNames.joinToString(prefix = "$Tuple${foci.size}<", postfix = ">")
else target.targetNames.first()

fun classConstructorFromTuple(sourceClassName: String, propertiesSize: Int) =
(0 until propertiesSize).joinToString(prefix = "$sourceClassName(", postfix = ")", transform = { "tuple.${letters[it]}" })

}
val get = if (hasTupleFocus) tupleConstructor() else "${iso.sourceName}.${foci.first().paramName}"
val reverseGet = if (hasTupleFocus) "tuple: ${focusType()} -> ${classConstructorFromTuple(iso.sourceClassName, foci.size)}" else "${iso.sourceClassName}(it)"

return """
|inline val ${iso.sourceClassName}.Companion.iso: $Iso<${iso.sourceClassName}, ${focusType()}> inline get()= $Iso(
| get = { ${iso.sourceName}: ${iso.sourceClassName} -> $get },
| reverseGet = { $reverseGet }
|)
|""".trimMargin()
}
Original file line number Diff line number Diff line change
@@ -1,56 +1,31 @@
package arrow.optics

import arrow.common.utils.fullName
import me.eugeniomarletti.kotlin.metadata.escapedClassName
import arrow.common.utils.simpleName
import me.eugeniomarletti.kotlin.metadata.plusIfNotBlank
import java.io.File

class LensesFileGenerator(
private val annotatedList: Collection<AnnotatedOptic>,
private val generatedDir: File
) {

private val filePrefix = "lenses"

fun generate() = annotatedList.map(this::processElement)
.map { (element, funs) ->
"$filePrefix.${element.classData.`package`}.${element.type.simpleName.toString().toLowerCase()}.kt" to
funs.joinToString(prefix = "package ${element.classData.`package`.escapedClassName}\n\n", separator = "\n")
}.forEach { (name, fileString) -> File(generatedDir, name).writeText(fileString) }

private fun String.toUpperCamelCase(): String = split(" ").joinToString("", transform = String::capitalize)

private fun processElement(annotatedOptic: AnnotatedOptic): Pair<AnnotatedOptic, List<String>> =
annotatedOptic to annotatedOptic.targets.map { variable ->
val sourceClassName = annotatedOptic.classData.fullName.escapedClassName
val sourceName = annotatedOptic.type.simpleName.toString().decapitalize()
val targetClassName = variable.fullName
val targetName = variable.paramName
val lensType = when (variable) {
is Target.NullableTarget -> "nullable${targetName.toUpperCamelCase()}"
is Target.OptionTarget -> "option${targetName.toUpperCamelCase()}"
is Target.NonNullTarget -> targetName
}

"""
|inline val $sourceClassName.Companion.$lensType: $Lens<$sourceClassName, $targetClassName> get()= $Lens(
| get = { $sourceName: $sourceClassName -> $sourceName.${targetName.plusIfNotBlank(prefix = "`", postfix = "`")} },
| set = { value: $targetClassName ->
| { $sourceName: $sourceClassName ->
| $sourceName.copy(${targetName.plusIfNotBlank(prefix = "`", postfix = "`")} = value)
| }
| }
|)
|
|inline val <S> $Iso<S, $sourceClassName>.$lensType: $Lens<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|inline val <S> $Lens<S, $sourceClassName>.$lensType: $Lens<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|inline val <S> $Optional<S, $sourceClassName>.$lensType: $Optional<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|inline val <S> $Prism<S, $sourceClassName>.$lensType: $Optional<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|inline val <S> $Getter<S, $sourceClassName>.$lensType: $Getter<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|inline val <S> $Setter<S, $sourceClassName>.$lensType: $Setter<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|inline val <S> $Traversal<S, $sourceClassName>.$lensType: $Traversal<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|inline val <S> $Fold<S, $sourceClassName>.$lensType: $Fold<S, $targetClassName> inline get() = this + $sourceClassName.$lensType
|""".trimMargin()
}
fun generateLenses(ele: AnnotatedElement, target: LensTarget) = Snippet(
`package` = ele.packageName,
name = ele.classData.simpleName,
content = processElement(ele, target.foci)
)

private fun String.toUpperCamelCase(): String = split(" ").joinToString("", transform = String::capitalize)

private fun processElement(ele: AnnotatedElement, foci: List<Focus>): String = foci.joinToString(separator = "\n") { focus ->
"""
|inline val ${ele.sourceClassName}.Companion.${focus.lensParamName()}: $Lens<${ele.sourceClassName}, ${focus.className}> inline get()= $Lens(
| get = { ${ele.sourceName}: ${ele.sourceClassName} -> ${ele.sourceName}.${focus.paramName.plusIfNotBlank(prefix = "`", postfix = "`")} },
| set = { value: ${focus.className} ->
| { ${ele.sourceName}: ${ele.sourceClassName} ->
| ${ele.sourceName}.copy(${focus.paramName.plusIfNotBlank(prefix = "`", postfix = "`")} = value)
| }
| }
|)
|""".trimMargin()
}

fun Focus.lensParamName(): String = when (this) {
is NullableFocus -> "nullable${paramName.toUpperCamelCase()}"
is OptionFocus -> "option${paramName.toUpperCamelCase()}"
is NonNullFocus -> paramName
}

0 comments on commit 5cae954

Please sign in to comment.