diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index b72a5da..d91c92a 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -6,4 +6,4 @@
-
+
\ No newline at end of file
diff --git a/docs/topics/Annotation-Argument-Processor.md b/docs/topics/Annotation-Argument-Processor.md
index 3bed099..fc03160 100644
--- a/docs/topics/Annotation-Argument-Processor.md
+++ b/docs/topics/Annotation-Argument-Processor.md
@@ -1,6 +1,6 @@
# Annotation Argument Processor
-Even though the [Annotation Argument API](Annotation-Argument-Processor.md) provides an easier to use API than the
+Even though the [Annotation Argument API](Annotation-Argument-Processor.md) provides an easier to use API than the
default KSP API, it can still require some boiler-plate to setup. For this reason an annotation processor is provided
to generate that boiler plate for you
@@ -51,12 +51,12 @@ Please note that the actual class returned is a "data class representation" of t
```kotlin
public data class InlineConstructor private constructor(
- public val forClass: KSType, // KClass turns into KSType
- public val functionName: String,
- public val nameMapping: List, // Array turns into List
- public val ignoreBuilders: List, // Array turns into list
- public val useQualifiedName: Boolean,
- public val nameProperty: String?, // This becomes nullable, because it is annotated with @NullIfDefault
+ public val forClass: KSType, // KClass turns into KSType
+ public val functionName: String,
+ public val nameMapping: List, // Array turns into List
+ public val ignoreBuilders: List, // Array turns into list
+ public val useQualifiedName: Boolean,
+ public val nameProperty: String?, // This becomes nullable, because it is annotated with @NullIfDefault
) {
public data class NameMapping private constructor(
public val originalName: String,
@@ -65,7 +65,52 @@ public data class InlineConstructor private constructor(
}
```
-> You can find the full code [here](https://github.com/kordlib/codegen-kt/blob/main/ksp-annotations/src/commonMain/kotlin/InlineConstructor.kt)
+> You can find the full
+> code [here](https://github.com/kordlib/codegen-kt/blob/main/ksp-annotations/src/commonMain/kotlin/InlineConstructor.kt)
-Please read the documentation of the `ProcessorAnnotation` annotation [here](https://codegen.kord.dev/api/ksp/dev.kord.codegen.ksp.annotations/-processor-annotation/index.html)
+Please read the documentation of the `ProcessorAnnotation`
+annotation [here](https://codegen.kord.dev/api/ksp/dev.kord.codegen.ksp.annotations/-processor-annotation/index.html)
+
+### Default values
+
+The processor also allows you to use some other default values than in you annotation, for example `null` or default to
+another propery
+
+```kotlin
+const val DEFAULT_VALUE = "DEFAULT!"
+
+annotation class TestAnnotation(
+ @NullIfDefault
+ val nullable: String = DEFAULT_VALUE // will be represented as "null" if not explicitly defined
+ // Will be the same as "nullable" if default
+ // Please note that since "nullable" is annotated with @NullIfDefault other will be nullable too
+ @OtherIfDefault("nullable")
+val other: String = DEFAULT_VALUE
+)
+```
+
+### Validation
+
+You are also able to validate that certain values are different, let's say we want either an int or a string value,
+but not both or none
+
+```kotlin
+const val NO_INT = -1
+const val NO_STRING = ""
+
+@Eihter(["intValue", "stringValue"], exclusive = true)
+annotation class ExpressionValue(
+ @NullIfDefault
+ val intValue: Int = NO_INT,
+ @NullIfDefault
+ val stringValue: Int = NO_STRING,
+)
+```
+
+This will throw an `IllegalStateException` in the [factory functions](#use-the-generated-code) when
+none or both values are present
+
+> Note that all properties used in an `@Either` annotation must be annotated with `@NullIfDefault`
+>
+{style="warning"}
diff --git a/docs/topics/about.topic b/docs/topics/about.topic
index a5d45ca..050ca6e 100644
--- a/docs/topics/about.topic
+++ b/docs/topics/about.topic
@@ -26,6 +26,4 @@
-
-
diff --git a/kotlinpoet/annotations/src/commonMain/kotlin/InlineConstructor.kt b/kotlinpoet/annotations/src/commonMain/kotlin/InlineConstructor.kt
index 4d17f7f..e1cabc2 100644
--- a/kotlinpoet/annotations/src/commonMain/kotlin/InlineConstructor.kt
+++ b/kotlinpoet/annotations/src/commonMain/kotlin/InlineConstructor.kt
@@ -49,7 +49,9 @@ public annotation class InlineConstructor(
val ignoreBuilders: Array = [],
val useQualifiedName: Boolean = false,
@NullIfDefault
- val nameProperty: String = NO_DELEGATION
+ val nameProperty: String = NO_DELEGATION,
+ @NullIfDefault
+ val nameProperty2: String = NO_DELEGATION
) {
/**
diff --git a/ksp/annotations/api/annotations.api b/ksp/annotations/api/annotations.api
index df5ddee..17889ca 100644
--- a/ksp/annotations/api/annotations.api
+++ b/ksp/annotations/api/annotations.api
@@ -1,6 +1,10 @@
public abstract interface annotation class dev/kord/codegen/ksp/annotations/NullIfDefault : java/lang/annotation/Annotation {
}
+public abstract interface annotation class dev/kord/codegen/ksp/annotations/OtherIfDefault : java/lang/annotation/Annotation {
+ public abstract fun name ()Ljava/lang/String;
+}
+
public abstract interface annotation class dev/kord/codegen/ksp/annotations/ProcessorAnnotation : java/lang/annotation/Annotation {
public abstract fun packageName ()Ljava/lang/String;
}
diff --git a/ksp/annotations/build.gradle.kts b/ksp/annotations/build.gradle.kts
index 127dce1..380585f 100644
--- a/ksp/annotations/build.gradle.kts
+++ b/ksp/annotations/build.gradle.kts
@@ -1,6 +1,7 @@
plugins {
org.jetbrains.kotlin.multiplatform
`kord-publishing`
+ com.google.devtools.ksp
}
base {
@@ -26,4 +27,19 @@ kotlin {
// tvos()
// macosX64()
// macosArm64()
+
+ sourceSets {
+ named("jvmMain") {
+ kotlin.srcDirs("build/generated/ksp/jvm/jvmMain/kotlin")
+ dependencies {
+ compileOnly(libs.ksp.api)
+ compileOnly(libs.codegen.ksp)
+ }
+ }
+ }
+}
+
+dependencies {
+ "kspJvm"(libs.codegen.ksp.processor)
+
}
diff --git a/ksp/annotations/src/commonMain/kotlin/ProcessorAnnotation.kt b/ksp/annotations/src/commonMain/kotlin/ProcessorAnnotation.kt
index 47c603c..c33838c 100644
--- a/ksp/annotations/src/commonMain/kotlin/ProcessorAnnotation.kt
+++ b/ksp/annotations/src/commonMain/kotlin/ProcessorAnnotation.kt
@@ -1,5 +1,7 @@
package dev.kord.codegen.ksp.annotations
+private const val packageName = "dev.kord.codegen.ksp.processor"
+
/**
* This annotation instructs the processor to generate a data class wrapper around the annotation with
* factory functions to instantiate it from a `KSAnnotation`.
@@ -32,14 +34,38 @@ package dev.kord.codegen.ksp.annotations
@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.ANNOTATION_CLASS)
-@ProcessorAnnotation(packageName = "dev.kord.codegen.ksp.processor")
+@ProcessorAnnotation(packageName)
public annotation class ProcessorAnnotation(
val packageName: String
)
/**
- * Instructs the processor to use null, if [AnnotationArguments.isDefault] returns `true` for this property.
+ * Instructs the processor to use null, if `AnnotationArguments.isDefault` returns `true` for this property.
*/
+@MustBeDocumented
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.FIELD)
public annotation class NullIfDefault
+
+/**
+ * Instructs the processor to use value of [name], if `AnnotationArguments.isDefault` returns `true` for this property.
+ *
+ * @property name the name of the property to use as a default
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.FIELD)
+@ProcessorAnnotation(packageName)
+public annotation class OtherIfDefault(val name: String)
+
+/**
+ * Instructs the processor to validate that either of [names] are present.
+ *
+ * @property exclusive whether there should be only once exclusive value
+ */
+@MustBeDocumented
+@Retention(AnnotationRetention.SOURCE)
+@Target(AnnotationTarget.ANNOTATION_CLASS)
+@Repeatable
+@ProcessorAnnotation(packageName)
+public annotation class Either(val names: Array, val exclusive: Boolean = false)
diff --git a/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt b/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt
index 9e345a9..ba5bd1a 100644
--- a/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt
+++ b/ksp/processor/src/main/kotlin/generator/DataClassRepresentation.kt
@@ -1,6 +1,8 @@
package dev.kord.codegen.ksp.processor.generator
+import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getDeclaredProperties
+import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.symbol.*
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
@@ -8,8 +10,10 @@ import com.squareup.kotlinpoet.ksp.addOriginatingKSFile
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.toTypeName
import dev.kord.codegen.kotlinpoet.*
-import dev.kord.codegen.ksp.processor.NULL_IF_DEFAULT
+import dev.kord.codegen.ksp.annotations.NullIfDefault
+import dev.kord.codegen.ksp.annotations.OtherIfDefault
import dev.kord.codegen.ksp.processor.PROCESSOR_ANNOTATION
+import dev.kord.codegen.ksp.processor.getOtherIfDefault
fun KSType.isMappedAnnotation(rootType: KSClassDeclaration): Boolean {
val declaration = declaration
@@ -47,11 +51,16 @@ private fun KSTypeReference.dataClassType(rootType: KSClassDeclaration): TypeNam
}
}
+@OptIn(KspExperimental::class)
fun KSPropertyDeclaration.dataClassType(rootType: KSClassDeclaration): TypeName {
val notNullType = type.dataClassType(rootType)
- return if (annotations.any { it.annotationType.resolve().declaration.qualifiedName!!.asString() == NULL_IF_DEFAULT }) {
+ return if (isAnnotationPresent(NullIfDefault::class)) {
notNullType.copy(nullable = true)
+ } else if (isAnnotationPresent(OtherIfDefault::class)) {
+ val annotation = getOtherIfDefault()
+ val otherField = rootType.getDeclaredProperties().first { it.simpleName.asString() == annotation.name }
+ otherField.dataClassType(rootType)
} else {
notNullType
}
diff --git a/ksp/processor/src/main/kotlin/generator/FactoryFunction.kt b/ksp/processor/src/main/kotlin/generator/FactoryFunction.kt
index ea1fde5..f459849 100644
--- a/ksp/processor/src/main/kotlin/generator/FactoryFunction.kt
+++ b/ksp/processor/src/main/kotlin/generator/FactoryFunction.kt
@@ -1,18 +1,26 @@
package dev.kord.codegen.ksp.processor.generator
+import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.getDeclaredProperties
+import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.symbol.KSAnnotation
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.ksp.toClassName
+import dev.kord.codegen.kotlinpoet.addCode
import dev.kord.codegen.kotlinpoet.addFunction
import dev.kord.codegen.kotlinpoet.addParameter
import dev.kord.codegen.kotlinpoet.withControlFlow
+import dev.kord.codegen.ksp.annotations.Either
+import dev.kord.codegen.ksp.annotations.NullIfDefault
+import dev.kord.codegen.ksp.annotations.OtherIfDefault
import dev.kord.codegen.ksp.processor.ARGUMENTS
import dev.kord.codegen.ksp.processor.ARGUMENTS_NOT_NULL
-import dev.kord.codegen.ksp.processor.NULL_IF_DEFAULT
+import dev.kord.codegen.ksp.processor.getEithers
+import dev.kord.codegen.ksp.processor.getOtherIfDefault
context(TypeSpec.Builder)
+@OptIn(KspExperimental::class)
fun KSClassDeclaration.factoryFunction(packageName: String) {
addFunction(simpleName.asString()) {
val type = ClassName(packageName, toClassName().simpleNames)
@@ -41,10 +49,13 @@ fun KSClassDeclaration.factoryFunction(packageName: String) {
add(".let(%1L.Companion::%1L)", argumentType.declaration.simpleName.asString())
}
- if (it.annotations.any { it.annotationType.resolve().declaration.qualifiedName!!.asString() == NULL_IF_DEFAULT }) {
+ if (it.isAnnotationPresent(NullIfDefault::class) || it.isAnnotationPresent(OtherIfDefault::class)) {
withControlFlow(".takeIf") {
add("!arguments.isDefault(%T::%L)", toClassName(), it.simpleName.asString())
- add("\n")
+ }
+
+ if (it.isAnnotationPresent(OtherIfDefault::class)) {
+ add("?:·%L", it.getOtherIfDefault().name)
}
}
add("\n")
@@ -52,6 +63,30 @@ fun KSClassDeclaration.factoryFunction(packageName: String) {
addCode(property)
CodeBlock.of("%L", it.simpleName.asString())
}.joinToString(", ")
+
+ if (isAnnotationPresent(Either::class)) {
+ getEithers().forEach { (names, exclusive) ->
+ addCode {
+ val nameList = names.map { CodeBlock.of("%N", it) }.joinToCode(", ")
+ val condition = CodeBlock.of("it != null")
+ val check = buildCodeBlock {
+ add("listOf(%L)", nameList)
+ if (exclusive) {
+ add(".singleOrNull { %L }", condition)
+ add("·==·null")
+ } else {
+ add(".none { %L }", condition)
+ }
+ }
+ withControlFlow("if (%L)", check) {
+ add("error(%S)", "Either of these values must be present: $names")
+ add("\n")
+ }
+ add("\n")
+ }
+ }
+ }
+
addCode("return·%T(%L)", type, valueArguments)
}
}
diff --git a/ksp/processor/src/main/kotlin/generator/Generator.kt b/ksp/processor/src/main/kotlin/generator/Generator.kt
index 58c65c2..c42ec3e 100644
--- a/ksp/processor/src/main/kotlin/generator/Generator.kt
+++ b/ksp/processor/src/main/kotlin/generator/Generator.kt
@@ -1,13 +1,21 @@
package dev.kord.codegen.ksp.processor.generator
+import com.google.devtools.ksp.KspExperimental
+import com.google.devtools.ksp.getDeclaredProperties
+import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSPropertyDeclaration
import com.squareup.kotlinpoet.AnnotationSpec
import com.squareup.kotlinpoet.DelicateKotlinPoetApi
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.ksp.toClassName
import com.squareup.kotlinpoet.ksp.writeTo
import dev.kord.codegen.kotlinpoet.FileSpec
+import dev.kord.codegen.ksp.annotations.NullIfDefault
+import dev.kord.codegen.ksp.annotations.OtherIfDefault
+import dev.kord.codegen.ksp.processor.getEithers
+import dev.kord.codegen.ksp.processor.getOtherIfDefault
import dev.kord.codegen.ksp.processor.getProcessorAnnotation
data class ProcessingContext(
@@ -17,8 +25,50 @@ data class ProcessingContext(
val packageName: String
)
-@OptIn(DelicateKotlinPoetApi::class)
+private data class Property(
+ val index: Int,
+ val declaration: KSPropertyDeclaration
+) : KSPropertyDeclaration by declaration
+
+@OptIn(DelicateKotlinPoetApi::class, KspExperimental::class)
fun SymbolProcessorEnvironment.processAnnotation(declaration: KSClassDeclaration) {
+ val propertyTypes = declaration
+ .getDeclaredProperties()
+ .mapIndexed { index, it -> it.simpleName.asString() to Property(index, it) }
+ .toMap()
+ declaration.getEithers().forEach {
+ it.names.forEach { name ->
+ val property = propertyTypes[name] ?: run {
+ logger.error("Either type does not exist", declaration)
+ return
+ }
+
+ if (!property.isAnnotationPresent(NullIfDefault::class)) {
+ logger.error("Either can only be used with @NullIfDefault values", declaration)
+ return
+ }
+ }
+ }
+ declaration.getDeclaredProperties().forEachIndexed { index, it ->
+ if (it.isAnnotationPresent(OtherIfDefault::class)) {
+ if (it.isAnnotationPresent(NullIfDefault::class)) {
+ logger.error("Usage of both @NullIfDefault and @OtherIfDefault is not allowed", it)
+ } else {
+ val name = it.getOtherIfDefault().name
+ val property = propertyTypes[name] ?: return run {
+ logger.error("Specified property name does not exist: $name", it)
+ }
+ if (!it.type.resolve().isAssignableFrom(property.type.resolve())) {
+ logger.error("Specified property has different type! Expected ${it.type} found $property", it)
+ return
+ }
+ if (index < property.index) {
+ logger.error("Specified property comes after current property", it)
+ return
+ }
+ }
+ }
+ }
val packageName = declaration.getProcessorAnnotation().packageName
val fileSpec = FileSpec(packageName, declaration.simpleName.asString()) {
addKotlinDefaultImports(includeJs = false)
diff --git a/ksp/src/main/kotlin/TypeResolvers.kt b/ksp/src/main/kotlin/TypeResolvers.kt
index 27beae4..47c9ebe 100644
--- a/ksp/src/main/kotlin/TypeResolvers.kt
+++ b/ksp/src/main/kotlin/TypeResolvers.kt
@@ -1,7 +1,9 @@
package dev.kord.codegen.ksp
+import com.google.devtools.ksp.KspExperimental
import com.google.devtools.ksp.findActualType
import com.google.devtools.ksp.getAnnotationsByType
+import com.google.devtools.ksp.isAnnotationPresent
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.*
@@ -51,7 +53,8 @@ public inline fun KSTypeReference.isOfType(): Boolean = isOfType(T::
/**
* Checks whether an [KSAnnotation] is of type by it's [qualifiedName].
*/
-public fun KSAnnotation.isOfType(qualifiedName: String): Boolean = annotationType.isOfType(qualifiedName, canBeTypeAlias = false)
+public fun KSAnnotation.isOfType(qualifiedName: String): Boolean =
+ annotationType.isOfType(qualifiedName, canBeTypeAlias = false)
/**
* Checks whether an [KSTypeReference] is of type by it's [qualifiedName].
@@ -74,6 +77,12 @@ public fun KSTypeReference.isOfType(qualifiedName: String, canBeTypeAlias: Boole
return actualDeclaration.qualifiedName?.asString() == qualifiedName
}
+/**
+ * Checks whether [A] is present on [KSAnnotated].
+ */
+@KspExperimental
+public inline fun KSAnnotated.isAnnotationPresent(): Boolean = isAnnotationPresent(A::class)
+
/**
* Fast check whether a [KSTypeReference] definitively does not match, this does not mean that it does it returning true
*/