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.core
18 
19 import androidx.appfunctions.compiler.AppFunctionCompiler
20 import androidx.appfunctions.compiler.core.IntrospectionHelper.APP_FUNCTIONS_AGGREGATED_DEPS_PACKAGE_NAME
21 import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionComponentRegistryAnnotation
22 import com.google.devtools.ksp.processing.CodeGenerator
23 import com.google.devtools.ksp.processing.Dependencies
24 import com.google.devtools.ksp.symbol.KSFile
25 import com.squareup.kotlinpoet.AnnotationSpec
26 import com.squareup.kotlinpoet.FileSpec
27 import com.squareup.kotlinpoet.TypeSpec
28 import com.squareup.kotlinpoet.buildCodeBlock
29 
30 /** A helper class to generate AppFunction component registry. */
31 class AppFunctionComponentRegistryGenerator(private val codeGenerator: CodeGenerator) {
32     /**
33      * Generates AppFunction component registry..
34      *
35      * For example, if a list of components under module "myLibrary" were provided to generate
36      * INVENTORY registry,
37      * * "com.android.Test1"
38      * * "com.android.Test2"
39      * * "com.android.diff.Test1"
40      *
41      * It would generate
42      *
43      * ```
44      * package appfunctions_aggregated_deps
45      *
46      * @AppFunctionComponentRegistry(
47      *   componentCategory = "INVENTORY",
48      *   componentNames = [
49      *     "com.android.Test1",
50      *     "com.android.Test2",
51      *     "com.android.diff.Test1",
52      *   ]
53      * )
54      * @Generated
55      * public class `$Mylibrary_InventoryComponentRegistry`
56      * ```
57      */
generateRegistrynull58     fun generateRegistry(
59         moduleName: String,
60         category: String,
61         components: List<AppFunctionComponent>,
62     ) {
63         val className = getRegistryClassName(moduleName, category)
64         val annotationBuilder =
65             AnnotationSpec.builder(AppFunctionComponentRegistryAnnotation.CLASS_NAME)
66                 .addMember(
67                     "${AppFunctionComponentRegistryAnnotation.PROPERTY_COMPONENT_CATEGORY} = %S",
68                     category,
69                 )
70                 .addMember(
71                     buildCodeBlock {
72                         addStatement(
73                             "${AppFunctionComponentRegistryAnnotation.PROPERTY_COMPONENT_NAMES} = ["
74                         )
75                         indent()
76                         // Ensure the generated registry is stable
77                         val sortedQualifiedNames =
78                             components.map(AppFunctionComponent::qualifiedName).sorted()
79                         for (componentName in sortedQualifiedNames) {
80                             addStatement("%S,", componentName)
81                         }
82                         unindent()
83                         add("]")
84                     }
85                 )
86 
87         val registryClassBuilder = TypeSpec.classBuilder(className)
88         registryClassBuilder.addAnnotation(annotationBuilder.build())
89         registryClassBuilder.addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
90 
91         val fileSpec =
92             FileSpec.builder(APP_FUNCTIONS_AGGREGATED_DEPS_PACKAGE_NAME, className)
93                 .addType(registryClassBuilder.build())
94                 .build()
95 
96         val sourceFiles = components.flatMap { it.sourceFiles }.toSet()
97         codeGenerator
98             .createNewFile(
99                 Dependencies(aggregating = true, sources = sourceFiles.toTypedArray()),
100                 APP_FUNCTIONS_AGGREGATED_DEPS_PACKAGE_NAME,
101                 className,
102             )
103             .bufferedWriter()
104             .use { fileSpec.writeTo(it) }
105     }
106 
getRegistryClassNamenull107     private fun getRegistryClassName(moduleName: String, componentCategory: String): String {
108         val prefix = moduleName.toPascalCase()
109         val componentCategoryPascalCase = componentCategory.toPascalCase()
110         return "${'$'}${prefix}_${componentCategoryPascalCase}ComponentRegistry"
111     }
112 
113     /** Wrapper to hold AppFunction component data. */
114     class AppFunctionComponent(
115         /** The component class qualified name. */
116         val qualifiedName: String,
117         /** The source files used to generate the component. */
118         val sourceFiles: Set<KSFile> = emptySet(),
119     )
120 }
121