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.core.AppFunctionTypeReference.Companion.SUPPORTED_TYPES_STRING 20 import androidx.appfunctions.compiler.core.AppFunctionTypeReference.Companion.isSupportedType 21 import com.google.devtools.ksp.getDeclaredProperties 22 import com.google.devtools.ksp.getVisibility 23 import com.google.devtools.ksp.symbol.KSClassDeclaration 24 import com.google.devtools.ksp.symbol.Visibility 25 26 /** A helper to validate an AppFunctionSerializable declaration. */ 27 class AppFunctionSerializableValidateHelper( 28 private val annotatedSerializable: AnnotatedAppFunctionSerializable 29 ) { 30 31 /** Validates if the primary constructor is valid. */ validatePrimaryConstructornull32 fun validatePrimaryConstructor() { 33 val primaryConstructor = annotatedSerializable.primaryConstructor 34 if (primaryConstructor == null) { 35 throw ProcessingException( 36 "A valid AppFunctionSerializable must have a primary constructor.", 37 annotatedSerializable.attributeNode 38 ) 39 } 40 val primaryConstructorDeclaration = checkNotNull(primaryConstructor) 41 if (primaryConstructorDeclaration.parameters.isEmpty()) { 42 throw ProcessingException( 43 "A valid AppFunctionSerializable must have a non-empty primary constructor.", 44 annotatedSerializable.attributeNode 45 ) 46 } 47 48 if (primaryConstructorDeclaration.getVisibility() != Visibility.PUBLIC) { 49 throw ProcessingException( 50 "A valid AppFunctionSerializable must have a public primary constructor.", 51 annotatedSerializable.attributeNode 52 ) 53 } 54 55 for (parameter in primaryConstructorDeclaration.parameters) { 56 if (!parameter.isVal) { 57 throw ProcessingException( 58 "All parameters in @AppFunctionSerializable primary constructor must have getters", 59 parameter 60 ) 61 } 62 } 63 } 64 65 /** Validate if the parameters are valid. */ validateParametersnull66 fun validateParameters() { 67 val parametersToValidate = 68 annotatedSerializable 69 .getProperties() 70 .associateBy { checkNotNull(it.name) } 71 .toMutableMap() 72 73 val superTypesWithSerializableAnnotation = 74 annotatedSerializable.findSuperTypesWithSerializableAnnotation() 75 val superTypesWithCapabilityAnnotation = 76 annotatedSerializable.findSuperTypesWithCapabilityAnnotation() 77 validateSuperTypes(superTypesWithSerializableAnnotation, superTypesWithCapabilityAnnotation) 78 79 for (superType in superTypesWithSerializableAnnotation) { 80 val superTypeAnnotatedSerializable = 81 AnnotatedAppFunctionSerializable(superType).validate() 82 for (superTypeProperty in superTypeAnnotatedSerializable.getProperties()) { 83 // Parameter has now been visited 84 val parameterInSuperType = parametersToValidate.remove(superTypeProperty.name) 85 if (parameterInSuperType == null) { 86 throw ProcessingException( 87 "All parameters in @AppFunctionSerializable " + 88 "supertypes must be present in subtype", 89 superTypeProperty.type 90 ) 91 } 92 validateSerializableParameter(parameterInSuperType) 93 } 94 } 95 96 for (superType in superTypesWithCapabilityAnnotation) { 97 val capabilityProperties = superType.getDeclaredProperties() 98 99 for (superTypeProperty in capabilityProperties) { 100 // Parameter has now been visited 101 val parameterInSuperType = 102 parametersToValidate.remove(superTypeProperty.simpleName.asString()) 103 if (parameterInSuperType == null) { 104 throw ProcessingException( 105 "All Properties in @AppFunctionSchemaCapability " + 106 "supertypes must be present in subtype", 107 superTypeProperty 108 ) 109 } 110 validateSerializableParameter(parameterInSuperType) 111 } 112 } 113 114 // Validate the remaining parameters 115 if (parametersToValidate.isNotEmpty()) { 116 for ((_, parameterToValidate) in parametersToValidate) { 117 validateSerializableParameter(parameterToValidate) 118 } 119 } 120 } 121 122 /** 123 * Validates that the super types of the serializable [annotatedSerializable] are valid. 124 * 125 * A super type of a serializable class must be annotated with either 126 * [androidx.appfunctions.AppFunctionSchemaCapability] or 127 * [androidx.appfunctions.AppFunctionSerializable]. A class cannot be annotated with both 128 * annotations. 129 * 130 * @param superTypesWithSerializableAnnotation a set of [KSClassDeclaration] for all super types 131 * of the [annotatedSerializable] that are annotated with 132 * [androidx.appfunctions.AppFunctionSerializable]. 133 * @param superTypesWithCapabilityAnnotation a set of [KSClassDeclaration] for all super types 134 * of the [annotatedSerializable] that are annotated with 135 * [androidx.appfunctions.AppFunctionSchemaCapability]. 136 */ validateSuperTypesnull137 private fun validateSuperTypes( 138 superTypesWithSerializableAnnotation: Set<KSClassDeclaration>, 139 superTypesWithCapabilityAnnotation: Set<KSClassDeclaration> 140 ) { 141 val classesWithMultipleAnnotations = 142 superTypesWithSerializableAnnotation.intersect(superTypesWithCapabilityAnnotation) 143 if (classesWithMultipleAnnotations.isNotEmpty()) { 144 throw ProcessingException( 145 "A class cannot be annotated with both @AppFunctionSerializable and " + 146 "@AppFunctionSchemaCapability.", 147 classesWithMultipleAnnotations.first() // Choose the first one as a sample 148 ) 149 } 150 } 151 validateSerializableParameternull152 private fun validateSerializableParameter(propertyDeclaration: AppFunctionPropertyDeclaration) { 153 if (propertyDeclaration.isGenericType) { 154 // Don't validate a generic type. Whether a generic type is valid or not would be 155 // validated when it is parameterized. 156 return 157 } 158 if (!isSupportedType(propertyDeclaration.type)) { 159 throw ProcessingException( 160 "AppFunctionSerializable properties must be one of the following types:\n" + 161 SUPPORTED_TYPES_STRING + 162 ", an @AppFunctionSerializable or a list of @AppFunctionSerializable\nbut found " + 163 propertyDeclaration.type.toTypeName(), 164 propertyDeclaration.type 165 ) 166 } 167 } 168 } 169