Skip to content

Commit

Permalink
Added support for enums (improvements needed) (#2)
Browse files Browse the repository at this point in the history
* Added support for enums (improvements needed)

* Improved enum and other types detection.

* checked kts file
  • Loading branch information
kasem-sm authored Aug 18, 2022
1 parent d72ad78 commit b40d5ba
Show file tree
Hide file tree
Showing 7 changed files with 134 additions and 104 deletions.
7 changes: 7 additions & 0 deletions buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
plugins {
`kotlin-dsl`
}

repositories {
mavenCentral()
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class StoreProcessor(
it is KSClassDeclaration && it.validate()
}
.forEach {
visitor = StoreVisitor(logger, resolver)
visitor = StoreVisitor(logger)
it.accept(visitor, Unit)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,44 +5,40 @@
package kasem.sm.easystore.processor

import com.google.devtools.ksp.processing.KSPLogger
import com.google.devtools.ksp.processing.Resolver
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSVisitorVoid
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.ksp.toClassName
import kasem.sm.easystore.processor.generator.DsFunctionsGenerator
import kasem.sm.easystore.processor.generator.dsImportNameGenerator
import kasem.sm.easystore.processor.ksp.checkIfReturnTypeExists
import kasem.sm.easystore.processor.ksp.getStoreAnnotationArgs
import kasem.sm.easystore.processor.ksp.toKClass
import kasem.sm.easystore.processor.ksp.isEnumClass
import kasem.sm.easystore.processor.ksp.supportedTypes
import kasem.sm.easystore.processor.validators.validateFunctionNameAlreadyExistsOrNot
import kasem.sm.easystore.processor.validators.validatePreferenceKeyIsUniqueOrNot
import kasem.sm.easystore.processor.validators.validateStoreArgs

internal data class Imports(
val packageName: String,
val names: List<String>
)

class StoreVisitor(
private val logger: KSPLogger,
private val resolver: Resolver
private val logger: KSPLogger
) : KSVisitorVoid() {

internal lateinit var className: String
internal lateinit var packageName: String

internal val generatedFunctions = mutableListOf<FunSpec>()
internal val generatedProperties = mutableListOf<PropertySpec>()
internal val generatedImports = mutableListOf<Imports>()
internal val generatedImportNames = mutableListOf<String>()

private val generator = DsFunctionsGenerator()

override fun visitClassDeclaration(classDeclaration: KSClassDeclaration, data: Unit) {
if (classDeclaration.classKind != ClassKind.INTERFACE) {
logger.error("Only interface can be annotated with @EasyStore", classDeclaration)
return
}

packageName = classDeclaration.packageName.asString()
Expand Down Expand Up @@ -94,42 +90,49 @@ class StoreVisitor(

function.checkIfReturnTypeExists(logger)

val resolverBuiltIns = resolver.builtIns
val functionParameterType = function.parameters[0].type.resolve()

val functionParameterKClass = functionParameterType.toKClass(resolverBuiltIns)
if (functionParameterKClass == null) {
val functionParameterKClass = functionParameterType.toClassName()

val showError = when {
supportedTypes.find { functionParameterKClass == it } != null -> false
functionParameterType.isEnumClass -> false
else -> true
}

if (showError) {
logger.error("$functionName parameter type $functionParameterType is not supported by Datastore yet!")
return
}

functionParameterType.dsImportNameGenerator(resolverBuiltIns) { name ->
generatedImports.add(Imports("androidx.datastore.preferences.core", listOf(name)))
functionParameterType.dsImportNameGenerator { name ->
generatedImportNames.add(name)
}

generator
.generateDSAddFunction(
function = function,
actualFunctionName = functionName,
actualFunctionParameterName = function.parameters[0].name?.getShortName(),
functionParamType = functionParameterType,
preferenceKeyPropertyName = preferenceKeyPropertyName
).apply {
generatedFunctions.add(this)
}

generator
.generateDSGetFunction(
functionParameterType = functionParameterType,
functionName = getterFunctionName,
preferenceKeyPropertyName = preferenceKeyPropertyName,
parameterType = functionParameterKClass
actualFunctionParameter = functionParameterType
).apply {
generatedFunctions.add(this)
}

generator
.generateDSKeyProperty(
functionParameterType = functionParameterType,
resolverBuiltIns = resolverBuiltIns,
preferenceKeyName = preferenceKeyName,
functionParameterKClass = functionParameterKClass
preferenceKeyName = preferenceKeyName
).apply {
generatedProperties.add(this)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,55 +4,45 @@
*/
package kasem.sm.easystore.processor.generator

import com.google.devtools.ksp.processing.KSBuiltIns
import com.google.devtools.ksp.symbol.KSFunctionDeclaration
import com.google.devtools.ksp.symbol.KSType
import com.squareup.kotlinpoet.ClassName
import com.google.devtools.ksp.symbol.Modifier
import com.squareup.kotlinpoet.CodeBlock
import com.squareup.kotlinpoet.FunSpec
import com.squareup.kotlinpoet.KModifier
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.toClassName
import kasem.sm.easystore.processor.ksp.isEnumClass
import kasem.sm.easystore.processor.ksp.toDataStoreKey
import kotlinx.coroutines.flow.Flow

internal class DsFunctionsGenerator {

fun generateDSKeyProperty(
functionParameterType: KSType,
resolverBuiltIns: KSBuiltIns,
preferenceKeyName: String,
functionParameterKClass: ClassName
preferenceKeyName: String
): PropertySpec {
val preferenceKeyPropertyName = "${preferenceKeyName.uppercase()}_KEY"

val dataStoreKeyType = functionParameterType.toDataStoreKey(
resolverBuiltIns,
preferenceKeyName
).parameterizedBy(functionParameterKClass)
val dataStoreKeyType = functionParameterType.toDataStoreKey().parameterizedBy(
if (functionParameterType.isEnumClass) {
String::class.asClassName()
} else functionParameterType.toClassName()
)

val codeBlock = when (functionParameterType) {
resolverBuiltIns.intType -> """
intPreferencesKey("$preferenceKeyName")
""".trimIndent()
resolverBuiltIns.stringType -> """
stringPreferencesKey("$preferenceKeyName")
""".trimIndent()
resolverBuiltIns.doubleType -> """
doublePreferencesKey("$preferenceKeyName")
""".trimIndent()
resolverBuiltIns.booleanType -> """
booleanPreferencesKey("$preferenceKeyName")
""".trimIndent()
resolverBuiltIns.floatType -> """
floatPreferencesKey("$preferenceKeyName")
""".trimIndent()
resolverBuiltIns.longType -> """
longPreferencesKey("$preferenceKeyName")
""".trimIndent()
else -> throw UnknownError()
val codeBlock = when (functionParameterType.declaration.simpleName.asString()) {
Int::class.simpleName -> "intPreferencesKey(\"$preferenceKeyName\")"
String::class.simpleName -> "stringPreferencesKey(\"$preferenceKeyName\")"
Double::class.simpleName -> "doublePreferencesKey(\"$preferenceKeyName\")"
Boolean::class.simpleName -> "booleanPreferencesKey(\"$preferenceKeyName\")"
Float::class.simpleName -> "floatPreferencesKey(\"$preferenceKeyName\")"
Long::class.simpleName -> "longPreferencesKey(\"$preferenceKeyName\")"
else -> {
if (functionParameterType.declaration.modifiers.first() == Modifier.ENUM) {
"stringPreferencesKey(\"$preferenceKeyName\")"
} else throw Exception()
}
}

return PropertySpec.builder(
Expand All @@ -65,39 +55,64 @@ internal class DsFunctionsGenerator {
}

fun generateDSAddFunction(
function: KSFunctionDeclaration,
actualFunctionName: String,
actualFunctionParameterName: String?,
functionParamType: KSType,
preferenceKeyPropertyName: String
): FunSpec {
val actualFunctionParameter = function.parameters[0].name?.getShortName()
val actualFunctionName = function.simpleName.asString()
val type = function.parameters[0].type.resolve().toClassName()
val isEnum = functionParamType.isEnumClass

// Check if it's enum and not String::class
val afterElvis = if (isEnum) {
(actualFunctionParameterName ?: "value") + ".name"
} else actualFunctionParameterName ?: "value"

return FunSpec.builder(
name = actualFunctionName
).apply {
addModifiers(KModifier.SUSPEND)
addParameter(
name = actualFunctionParameter ?: "value",
type = type
name = actualFunctionParameterName ?: "value",
type = functionParamType.toClassName()
)
addCode(
CodeBlock.of(
"""
dataStore.edit { preferences ->
preferences[$preferenceKeyPropertyName] = $actualFunctionParameter
}
""".trimIndent()
dataStore.edit { preferences ->
preferences[$preferenceKeyPropertyName] = $afterElvis
}
"""
.trimIndent()
)
)
}.build()
}

fun generateDSGetFunction(
functionParameterType: KSType,
functionName: String,
preferenceKeyPropertyName: String,
parameterType: ClassName
actualFunctionParameter: KSType
): FunSpec {
val codeBlock = """
val paramType = if (functionParameterType.isEnumClass) {
actualFunctionParameter.toClassName()
} else functionParameterType.toClassName()

val codeBlock = if (functionParameterType.isEnumClass) {
"""
return dataStore.data
.catch { exception ->
if (exception is IOException) {
emit(emptyPreferences())
} else {
throw exception
}
}.map { preference ->
$paramType.valueOf(preference[$preferenceKeyPropertyName] ?: defaultValue.name)
}
""".trimIndent()
} else {
"""
return dataStore.data
.catch { exception ->
if (exception is IOException) {
Expand All @@ -108,13 +123,14 @@ internal class DsFunctionsGenerator {
}.map { preference ->
preference[$preferenceKeyPropertyName] ?: defaultValue
}
""".trimIndent()
""".trimIndent()
}

return FunSpec.builder(
name = functionName
).apply {
addParameter("defaultValue", parameterType)
returns(Flow::class.asClassName().parameterizedBy(parameterType))
addParameter("defaultValue", paramType)
returns(Flow::class.asClassName().parameterizedBy(paramType))
addCode(codeBlock)
}.build()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,23 @@
*/
package kasem.sm.easystore.processor.generator

import com.google.devtools.ksp.processing.KSBuiltIns
import com.google.devtools.ksp.symbol.KSType
import com.google.devtools.ksp.symbol.Modifier

internal fun KSType.dsImportNameGenerator(
resolverBuiltIns: KSBuiltIns,
import: (String) -> Unit
) {
when (this) {
resolverBuiltIns.intType -> import("intPreferencesKey")
resolverBuiltIns.stringType -> import("stringPreferencesKey")
resolverBuiltIns.doubleType -> import("doublePreferencesKey")
resolverBuiltIns.booleanType -> import("booleanPreferencesKey")
resolverBuiltIns.floatType -> import("floatPreferencesKey")
resolverBuiltIns.longType -> import("longPreferencesKey")
when (declaration.simpleName.asString()) {
Int::class.simpleName -> import("intPreferencesKey")
String::class.simpleName -> import("stringPreferencesKey")
Double::class.simpleName -> import("doublePreferencesKey")
Boolean::class.simpleName -> import("booleanPreferencesKey")
Float::class.simpleName -> import("floatPreferencesKey")
Long::class.simpleName -> import("longPreferencesKey")
else -> {
if (declaration.modifiers.first() == Modifier.ENUM) {
import("stringPreferencesKey")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,14 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.asClassName
import kasem.sm.easystore.processor.Imports
import kasem.sm.easystore.processor.StoreVisitor

internal class StoreFileGenerator(
visitor: StoreVisitor
) {
private val packageName: String = visitor.packageName
private val className: String = visitor.className
private val generatedImports: List<Imports> = visitor.generatedImports
private val generatedImports: List<String> = visitor.generatedImportNames
private val generatedFunctions: List<FunSpec> = visitor.generatedFunctions
private val generatedProperties: List<PropertySpec> = visitor.generatedProperties

Expand All @@ -37,7 +36,7 @@ internal class StoreFileGenerator(
addNecessaryDataStoreImports()
// Other optional imports
generatedImports.forEach {
addImport(it.packageName, it.names)
addImport(packageName = "androidx.datastore.preferences.core", it)
}

addType(
Expand Down
Loading

0 comments on commit b40d5ba

Please sign in to comment.