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

Prism processor #267

Merged
merged 3 commits into from
Sep 11, 2017
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,9 @@ import javax.lang.model.element.VariableElement
sealed class AnnotatedLens {
data class Element(val type: TypeElement, val properties: Collection<VariableElement>) : AnnotatedLens()
data class InvalidElement(val reason: String) : AnnotatedLens()
}

sealed class AnnotatedPrism {
data class Element(val type: TypeElement, val subTypes: Collection<TypeElement>) : AnnotatedPrism()
data class InvalidElement(val reason: String) : AnnotatedPrism()
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,10 @@ package kategory.optics

val lensesAnnotationKClass = lenses::class
val lensesAnnotationClass = lensesAnnotationKClass.java
val lensesAnnotationName = "@" + lensesAnnotationClass.simpleName
val lensesAnnotationName = "@" + lensesAnnotationClass.simpleName
val lensesAnnotationTarget = "data class"

val prismsAnnotationKClass = prisms::class
val prismsAnnotationClass = prismsAnnotationKClass.java
val prismsAnnotationName = "@" + prismsAnnotationClass.simpleName
val prismsAnnotationTarget = "sealed class"
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package kategory.optics

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KotlinFile
import com.squareup.kotlinpoet.asClassName
import java.io.File

class LensesFileGenerator(
Expand All @@ -15,19 +17,19 @@ class LensesFileGenerator(

private fun buildLenses(elements: Collection<AnnotatedLens.Element>): List<KotlinFile> = elements.map(this::processElement)
.map { (name, funs) ->
funs.fold(KotlinFile.builder("kategory.optics", "optics.kategory.lens.$name").skipJavaLangImports(true), { builder, lensSpec ->
funs.fold(KotlinFile.builder(name.packageName(), "${name.simpleName().toLowerCase()}.lenses").skipJavaLangImports(true), { builder, lensSpec ->
builder.addFun(lensSpec)
}).build()
}

private fun processElement(annotatedLens: AnnotatedLens.Element): Pair<String, List<FunSpec>> =
annotatedLens.type.simpleName.toString().toLowerCase() to annotatedLens.properties.map { variable ->
private fun processElement(annotatedLens: AnnotatedLens.Element): Pair<ClassName, List<FunSpec>> =
annotatedLens.type.asClassName() to annotatedLens.properties.map { variable ->
val className = annotatedLens.type.simpleName.toString().toLowerCase()
val variableName = variable.simpleName

FunSpec.builder("$className${variableName.toString().capitalize()}")
.addStatement(
"""return Lens(
"""return kategory.optics.Lens(
| get = { $className: %T -> $className.$variableName },
| set = { $variableName: %T ->
| { $className: %T ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,54 +6,80 @@ import kategory.common.utils.knownError
import me.eugeniomarletti.kotlin.metadata.KotlinClassMetadata
import me.eugeniomarletti.kotlin.metadata.isDataClass
import me.eugeniomarletti.kotlin.metadata.kotlinMetadata
import me.eugeniomarletti.kotlin.metadata.modality
import org.jetbrains.kotlin.serialization.ProtoBuf
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.ElementKind
import javax.lang.model.element.ExecutableElement
import javax.lang.model.element.TypeElement
import javax.lang.model.element.VariableElement
import javax.lang.model.type.TypeKind

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

private val annotatedLenses = mutableListOf<AnnotatedLens.Element>()

private val annotatedPrisms = mutableListOf<AnnotatedPrism.Element>()

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

override fun getSupportedAnnotationTypes() = setOf(lensesAnnotationClass.canonicalName)
override fun getSupportedAnnotationTypes() = setOf(
lensesAnnotationClass.canonicalName,
prismsAnnotationClass.canonicalName
)

override fun onProcess(annotations: Set<TypeElement>, roundEnv: RoundEnvironment) {
annotatedLenses += roundEnv
.getElementsAnnotatedWith(lensesAnnotationClass)
.map(this::evalAnnotatedElement)
.map { annotatedLens ->
when (annotatedLens) {
is AnnotatedLens.InvalidElement -> knownError(annotatedLens.reason)
is AnnotatedLens.Element -> annotatedLens
}
}

annotatedPrisms += roundEnv
.getElementsAnnotatedWith(prismsAnnotationClass)
.map(this::evalAnnotatedPrismElement)

if (roundEnv.processingOver()) {
val generatedDir = File(this.generatedDir!!, "").also { it.mkdirs() }
LensesFileGenerator(annotatedLenses, generatedDir).generate()
PrismsFileGenerator(annotatedPrisms, generatedDir).generate()
}
}

fun evalAnnotatedElement(element: Element): AnnotatedLens = when {
element.kotlinMetadata !is KotlinClassMetadata -> AnnotatedLens.InvalidElement("""
|Cannot use @Lenses on ${element.enclosingElement}.${element.simpleName}.
|It can only be used on data classes.""".trimMargin())

(element.kotlinMetadata as KotlinClassMetadata).data.classProto.isDataClass ->
private fun evalAnnotatedElement(element: Element): AnnotatedLens.Element = when {
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isDataClass == true ->
AnnotatedLens.Element(
element as TypeElement,
element.enclosedElements
.filter { it.asType().kind == TypeKind.DECLARED }
.map { it as VariableElement })
element.enclosedElements.firstOrNull { it.kind == ElementKind.CONSTRUCTOR }
?.let { it as ExecutableElement }
?.parameters ?: emptyList()
)

else -> AnnotatedLens.InvalidElement("${element.enclosingElement}.${element.simpleName} cannot be annotated with @Lenses")
else -> knownError(opticsAnnotationError(element, lensesAnnotationName, lensesAnnotationTarget))
}

private fun evalAnnotatedPrismElement(element: Element): AnnotatedPrism.Element = when {
element.let { it.kotlinMetadata as? KotlinClassMetadata }?.data?.classProto?.isSealed == true -> {
val (nameResolver, classProto) = element.kotlinMetadata.let { it as KotlinClassMetadata }.data

AnnotatedPrism.Element(
element as TypeElement,
classProto.sealedSubclassFqNameList
.map(nameResolver::getString)
.map { it.replace('/', '.') }
.mapNotNull(elementUtils::getTypeElement)
)
}

else -> knownError(opticsAnnotationError(element, prismsAnnotationName, prismsAnnotationTarget))
}

private fun opticsAnnotationError(element: Element, annotationName: String, targetName: String): String = """
|Cannot use $annotationName on ${element.enclosingElement}.${element.simpleName}.
|It can only be used on $targetName.""".trimMargin()

private val ProtoBuf.Class.isSealed
get() = modality == ProtoBuf.Modality.SEALED

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package kategory.optics

import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KotlinFile
import com.squareup.kotlinpoet.asClassName
import java.io.File

class PrismsFileGenerator(
private val annotatedList: Collection<AnnotatedPrism.Element>,
private val generatedDir: File
) {

fun generate() = buildPrisms(annotatedList).forEach {
it.writeTo(generatedDir)
}

private fun buildPrisms(elements: Collection<AnnotatedPrism.Element>): List<KotlinFile> = elements.map(this::processElement)
.map { (name, funs) ->
funs.fold(KotlinFile.builder(name.packageName(), "${name.simpleName().toLowerCase()}.prisms").skipJavaLangImports(true), { builder, prismSpec ->
builder.addFun(prismSpec)
}).addStaticImport("kategory", "right", "left").build()
}

private fun processElement(annotatedPrism: AnnotatedPrism.Element): Pair<ClassName, List<FunSpec>> =
annotatedPrism.type.asClassName() to annotatedPrism.subTypes.map { subClass ->
val sealedClassName = annotatedPrism.type.simpleName.toString().toLowerCase()
val subTypeName = subClass.simpleName.toString()

FunSpec.builder("$sealedClassName$subTypeName")
.addStatement(
"""return kategory.optics.Prism(
| getOrModify = { $sealedClassName: %T ->
| when ($sealedClassName) {
| is %T -> $sealedClassName.right()
| else -> $sealedClassName.left()
| }
| },
| reverseGet = { it }
|)""".trimMargin(), annotatedPrism.type, subClass)
.build()
}

}
6 changes: 5 additions & 1 deletion kategory-annotations/src/main/java/kategory/optics.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,8 @@ import kotlin.annotation.AnnotationTarget.CLASS

@Retention(SOURCE)
@Target(CLASS)
annotation class lenses
annotation class lenses

@Retention(SOURCE)
@Target(CLASS)
annotation class prisms
1 change: 0 additions & 1 deletion kategory-optics/build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
dependencies {
compile project(':kategory-core')
compile project(':kategory-test')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
testCompile "io.kotlintest:kotlintest:$kotlinTestVersion"
testCompile project(':kategory-test')
Expand Down
7 changes: 7 additions & 0 deletions kategory-optics/src/main/kotlin/kategory/optics/Lens.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import kategory.HK
import kategory.Option
import kategory.Tuple2
import kategory.functor
import kategory.identity
import kategory.toT

/**
Expand All @@ -25,6 +26,12 @@ abstract class Lens<A, B> {
abstract fun set(b: B): (A) -> A

companion object {

fun <A> codiagonal() = Lens<Either<A, A>, A>(
get = { it.fold(::identity, ::identity) },
set = { a -> { it.bimap({ a }, { a }) } }
)

operator fun <A, B> invoke(get: (A) -> B, set: (B) -> (A) -> A) = object : Lens<A, B>() {
override fun get(a: A): B = get(a)

Expand Down
36 changes: 28 additions & 8 deletions kategory-optics/src/main/kotlin/kategory/optics/Prism.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ package kategory.optics

import kategory.Applicative
import kategory.Either
import kategory.Eq
import kategory.HK
import kategory.Option
import kategory.Tuple2
import kategory.compose
import kategory.eq
import kategory.flatMap
import kategory.identity
import kategory.left
Expand All @@ -31,11 +34,20 @@ abstract class Prism<A, B> {
abstract fun reverseGet(b: B): A

companion object {
operator fun <A,B> invoke(getOrModify: (A) -> Either<A, B>, reverseGet: (B) -> A) = object : Prism<A,B>() {
operator fun <A, B> invoke(getOrModify: (A) -> Either<A, B>, reverseGet: (B) -> A) = object : Prism<A, B>() {
override fun getOrModify(a: A): Either<A, B> = getOrModify(a)

override fun reverseGet(b: B): A = reverseGet(b)
}

/**
* a [Prism] that checks for equality with a given value
*/
inline fun <reified A> only(a: A, EQA: Eq<A> = eq()) = Prism<A, Unit>(
getOrModify = { a2 -> (if (EQA.eqv(a, a2)) a.left() else Unit.right()) },
reverseGet = { a }
)

}

/**
Expand Down Expand Up @@ -68,11 +80,6 @@ abstract class Prism<A, B> {
*/
fun set(b: B): (A) -> A = modify { b }

infix fun <C> composePrism(other: Prism<B, C>): Prism<A, C> = Prism(
{ a -> getOrModify(a).flatMap { b: B -> other.getOrModify(b).bimap({ set(it)(a) }, ::identity) } },
{ reverseGet(other.reverseGet(it)) }
)

/**
* Set the target of a [Prism] with a value
*/
Expand All @@ -81,12 +88,12 @@ abstract class Prism<A, B> {
/**
* Check if there is a target
*/
fun isNotEmpty(a: A): Boolean = getOption(a).isDefined
fun nonEmpty(a: A): Boolean = getOption(a).nonEmpty

/**
* Check if there is no target
*/
fun isEmpty(a: A): Boolean = !isNotEmpty(a)
fun isEmpty(a: A): Boolean = !nonEmpty(a)

/**
* Find if the target satisfies the predicate
Expand Down Expand Up @@ -119,6 +126,19 @@ abstract class Prism<A, B> {
{ (c, b) -> c toT reverseGet(b) }
)

/**
* Compose a [Prism] with another [Prism]
*/
infix fun <C> composePrism(other: Prism<B, C>): Prism<A, C> = Prism(
{ a -> getOrModify(a).flatMap { b: B -> other.getOrModify(b).bimap({ set(it)(a) }, ::identity) } },
this::reverseGet compose other::reverseGet
)

/**
* Plus operator overload to compose lenses
*/
operator fun <C> plus(other: Prism<B, C>): Prism<A, C> = composePrism(other)

}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.kotlintest.KTestJUnitRunner
import io.kotlintest.properties.Gen
import io.kotlintest.properties.forAll
import kategory.Eq
import kategory.LensLaws
import kategory.Option
import kategory.Tuple2
import kategory.UnitSpec
Expand Down
14 changes: 12 additions & 2 deletions kategory-optics/src/test/kotlin/kategory/optics/PrismTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import io.kotlintest.properties.forAll
import kategory.Eq
import kategory.NonEmptyList
import kategory.Option
import kategory.PrismLaws
import kategory.Try
import kategory.UnitSpec
import kategory.applicative
Expand Down Expand Up @@ -96,7 +97,16 @@ class PrismTest : UnitSpec() {

"Joining two prisms together with same target should yield same result" {
forAll(SumGen, { a ->
(sumPrism composePrism stringPrism).getOption(a) == sumPrism.getOption(a).flatMap(stringPrism::getOption)
(sumPrism composePrism stringPrism).getOption(a) == sumPrism.getOption(a).flatMap(stringPrism::getOption) &&
(sumPrism + stringPrism).getOption(a) == (sumPrism composePrism stringPrism).getOption(a)
})
}

"Checking if a prism exists with a target" {
forAll(SumGen, Gen.bool(), { a, bool ->
Prism.only(a, object : Eq<SumType> {
override fun eqv(a: SumType, b: SumType): Boolean = bool
}).isEmpty(a) == bool
})
}

Expand All @@ -108,7 +118,7 @@ class PrismTest : UnitSpec() {

"Checking if a target exists" {
forAll(SumGen, { sum ->
sumPrism.isNotEmpty(sum) == sum is SumType.A
sumPrism.nonEmpty(sum) == sum is SumType.A
})
}

Expand Down
1 change: 1 addition & 0 deletions kategory-test/build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
dependencies {
compile project(':kategory-core')
compile project(':kategory-optics')
compile "org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlinVersion"
compile "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion"
compile "io.kotlintest:kotlintest:$kotlinTestVersion"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
package kategory.optics
package kategory

import io.kotlintest.properties.Gen
import io.kotlintest.properties.forAll
import kategory.Applicative
import kategory.Eq
import kategory.Law
import kategory.compose
import kategory.exists
import kategory.identity
import kategory.optics.Lens

object LensLaws {

Expand Down
Loading