1 /*
2  * Copyright 2025 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.appfunctions.compiler.processors
18 
19 import androidx.appfunctions.compiler.AppFunctionCompiler
20 import androidx.appfunctions.compiler.core.AnnotatedAppFunctions
21 import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
22 import androidx.appfunctions.compiler.core.fromCamelCaseToScreamingSnakeCase
23 import com.google.devtools.ksp.processing.CodeGenerator
24 import com.google.devtools.ksp.processing.Dependencies
25 import com.google.devtools.ksp.processing.Resolver
26 import com.google.devtools.ksp.processing.SymbolProcessor
27 import com.google.devtools.ksp.symbol.KSAnnotated
28 import com.squareup.kotlinpoet.FileSpec
29 import com.squareup.kotlinpoet.KModifier
30 import com.squareup.kotlinpoet.PropertySpec
31 import com.squareup.kotlinpoet.TypeSpec
32 import com.squareup.kotlinpoet.asTypeName
33 
34 /**
35  * The processor to generate ID classes for AppFunction.
36  *
37  * For each AppFunction class, a corresponding ID class would be generated in the same package for
38  * developer to access the function id easily. For example,
39  * ```
40  * class NoteFunction: CreateNote {
41  *   @AppFunction
42  *   override suspend fun createNote(): Note { ... }
43  * }
44  * ```
45  *
46  * A corresponding `NoteFunctionIds` class will be generated:
47  * ```
48  * object NoteFunctionIds {
49  *   const val CREATE_NOTE_ID = "someId"
50  * }
51  * ```
52  */
53 class AppFunctionIdProcessor(
54     private val codeGenerator: CodeGenerator,
55 ) : SymbolProcessor {
processnull56     override fun process(resolver: Resolver): List<KSAnnotated> {
57         val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver)
58         val appFunctionClasses = appFunctionSymbolResolver.resolveAnnotatedAppFunctions()
59         for (appFunctionClass in appFunctionClasses) {
60             generateAppFunctionIdClasses(appFunctionClass)
61         }
62         return emptyList()
63     }
64 
generateAppFunctionIdClassesnull65     private fun generateAppFunctionIdClasses(appFunctionClass: AnnotatedAppFunctions) {
66         val originalPackageName = appFunctionClass.classDeclaration.packageName.asString()
67         val originalClassName = appFunctionClass.classDeclaration.simpleName.asString()
68 
69         val idClassName = getAppFunctionIdClassName(originalClassName)
70         val classBuilder =
71             TypeSpec.objectBuilder(idClassName).apply {
72                 addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
73                 addAppFunctionIdProperties(appFunctionClass)
74             }
75 
76         val fileSpec =
77             FileSpec.builder(originalPackageName, idClassName).addType(classBuilder.build()).build()
78         codeGenerator
79             .createNewFile(
80                 Dependencies(
81                     // Isolating, because the information to construct the ID class only comes
82                     // from a single class containing AppFunction implementations and never from
83                     // other or new files.
84                     aggregating = false,
85                     checkNotNull(appFunctionClass.classDeclaration.containingFile)
86                 ),
87                 originalPackageName,
88                 idClassName
89             )
90             .bufferedWriter()
91             .use { fileSpec.writeTo(it) }
92     }
93 
getAppFunctionIdClassNamenull94     private fun getAppFunctionIdClassName(functionClassName: String): String {
95         return "${functionClassName}Ids"
96     }
97 
addAppFunctionIdPropertiesnull98     private fun TypeSpec.Builder.addAppFunctionIdProperties(
99         appFunctionClass: AnnotatedAppFunctions
100     ) {
101         for (appFunctionDeclaration in appFunctionClass.appFunctionDeclarations) {
102             // For example, transform method name from "createNote" to "CREATE_NOTE"
103             val functionMethodName =
104                 appFunctionDeclaration.simpleName.asString().fromCamelCaseToScreamingSnakeCase()
105             val propertySpec =
106                 PropertySpec.builder("${functionMethodName}_ID", String::class.asTypeName())
107                     .addModifiers(KModifier.CONST)
108                     .initializer(
109                         "%S",
110                         appFunctionClass.getAppFunctionIdentifier(appFunctionDeclaration)
111                     )
112                     .build()
113             this.addProperty(propertySpec)
114         }
115     }
116 }
117