1 /*
<lambda>null2  * Copyright 2024 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 @file:OptIn(kotlinx.serialization.ExperimentalSerializationApi::class)
18 
19 package androidx.navigation.serialization
20 
21 import androidx.annotation.RestrictTo
22 import androidx.navigation.NamedNavArgument
23 import androidx.navigation.NavType
24 import androidx.navigation.navArgument
25 import kotlin.jvm.JvmName
26 import kotlin.reflect.KType
27 import kotlinx.serialization.InternalSerializationApi
28 import kotlinx.serialization.KSerializer
29 import kotlinx.serialization.PolymorphicSerializer
30 import kotlinx.serialization.descriptors.SerialDescriptor
31 import kotlinx.serialization.descriptors.StructureKind
32 import kotlinx.serialization.descriptors.capturedKClass
33 import kotlinx.serialization.serializer
34 
35 /**
36  * Generates a route pattern for use in Navigation functions such as [::navigate] from a serializer
37  * of class T where T is a concrete class or object.
38  *
39  * The generated route pattern contains the path, path args, and query args. See
40  * [RouteBuilder.computeParamType] for logic on how parameter type (path or query) is computed.
41  *
42  * @param [typeMap] A mapping of KType to the custom NavType<*>. For example given an argument of
43  *   "val userId: UserId", the map should contain [typeOf<UserId>() to MyNavType].
44  * @param [path] The base path to append arguments to. If null, base path defaults to
45  *   [KSerializer.descriptor].serialName.
46  */
47 internal fun <T> KSerializer<T>.generateRoutePattern(
48     typeMap: Map<KType, NavType<*>> = emptyMap(),
49     path: String? = null,
50 ): String {
51     assertNotAbstractClass {
52         throw IllegalArgumentException(
53             "Cannot generate route pattern from polymorphic class " +
54                 "${descriptor.capturedKClass?.simpleName}. Routes can only be generated from " +
55                 "concrete classes or objects."
56         )
57     }
58     val builder =
59         if (path != null) {
60             RouteBuilder(path, this)
61         } else {
62             RouteBuilder(this)
63         }
64     forEachIndexed(typeMap) { index, argName, navType ->
65         builder.appendPattern(index, argName, navType)
66     }
67     return builder.build()
68 }
69 
70 /**
71  * Returns a list of [NamedNavArgument].
72  *
73  * By default this method only supports conversion to NavTypes that are declared in
74  * [NavType.Companion] class. To convert non-natively supported types, the custom NavType must be
75  * provided via [typeMap].
76  *
77  * Short summary of NavArgument generation principles:
78  * 1. NavArguments will only be generated on variables with kotlin backing fields
79  * 2. Arg Name is based on variable name
80  * 3. Nullability is based on variable Type's nullability
81  * 4. defaultValuePresent is based on whether variable has default value
82  *
83  * This generator does not check for validity as a NavType. This means if a NavType is not nullable
84  * (i.e. Int), and the KType was Int?, it relies on the navArgument builder to throw exception.
85  *
86  * @param [typeMap] A mapping of KType to the custom NavType<*>. For example given an argument of
87  *   "val userId: UserId", the map should contain [typeOf<UserId>() to MyNavType]. Custom NavTypes
88  *   take priority over native NavTypes. This means you can override native NavTypes such as
89  *   [NavType.IntType] with your own implementation of NavType<Int>.
90  */
91 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
generateNavArgumentsnull92 public fun <T> KSerializer<T>.generateNavArguments(
93     typeMap: Map<KType, NavType<*>> = emptyMap()
94 ): List<NamedNavArgument> {
95     assertNotAbstractClass {
96         throw IllegalArgumentException(
97             "Cannot generate NavArguments for polymorphic serializer $this. Arguments " +
98                 "can only be generated from concrete classes or objects."
99         )
100     }
101 
102     return List(descriptor.elementsCount) { index ->
103         val name = descriptor.getElementName(index)
104         navArgument(name) {
105             val element = descriptor.getElementDescriptor(index)
106             val isNullable = element.isNullable
107             type =
108                 element.computeNavType(typeMap)
109                     ?: throw IllegalArgumentException(
110                         unknownNavTypeErrorMessage(
111                             name,
112                             element.serialName,
113                             this@generateNavArguments.descriptor.serialName,
114                             typeMap.toString()
115                         )
116                     )
117             nullable = isNullable
118             if (descriptor.isElementOptional(index)) {
119                 // Navigation mostly just cares about defaultValuePresent state for
120                 // non-nullable args to verify DeepLinks at a later stage.
121                 // We know that non-nullable types cannot have null values, so it is
122                 // safe to mark this as true without knowing actual value.
123                 unknownDefaultValuePresent = true
124             }
125         }
126     }
127 }
128 
129 /**
130  * Generates a route filled in with argument value for use in Navigation functions such as
131  * [::navigate] from a destination instance of type T.
132  *
133  * The generated route pattern contains the path, path args, and query args. See
134  * [RouteBuilder.computeParamType] for logic on how parameter type (path or query) is computed.
135  */
136 @OptIn(InternalSerializationApi::class)
137 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
generateRouteWithArgsnull138 public fun <T : Any> generateRouteWithArgs(route: T, typeMap: Map<String, NavType<Any?>>): String {
139     val serializer = route::class.serializer()
140     val argMap: Map<String, List<String>> = RouteEncoder(serializer, typeMap).encodeToArgMap(route)
141     val builder = RouteBuilder(serializer)
142     serializer.forEachIndexed(typeMap) { index, argName, navType ->
143         val value = argMap[argName]!!
144         builder.appendArg(index, argName, navType, value)
145     }
146     return builder.build()
147 }
148 
assertNotAbstractClassnull149 private fun <T> KSerializer<T>.assertNotAbstractClass(handler: () -> Unit) {
150     // abstract class
151     if (this is PolymorphicSerializer) {
152         handler()
153     }
154 }
155 
156 /**
157  * Computes and return the [NavType] based on the SerialDescriptor of a class type.
158  *
159  * Match priority:
160  * 1. Match with custom NavType provided in [typeMap]
161  * 2. Match to a built-in NavType such as [NavType.IntType], [NavType.BoolArrayType] etc.
162  */
163 @Suppress("UNCHECKED_CAST")
computeNavTypenull164 private fun SerialDescriptor.computeNavType(typeMap: Map<KType, NavType<*>>): NavType<Any?>? {
165     val customType =
166         typeMap.keys.find { kType -> matchKType(kType) }?.let { typeMap[it] } as? NavType<Any?>
167     val result = customType ?: getNavType()
168     return if (result == UNKNOWN) null else result as NavType<Any?>
169 }
170 
171 @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
generateHashCodenull172 public fun <T> KSerializer<T>.generateHashCode(): Int {
173     var hash = descriptor.serialName.hashCode()
174     for (i in 0 until descriptor.elementsCount) {
175         hash = 31 * hash + descriptor.getElementName(i).hashCode()
176     }
177     return hash
178 }
179 
180 @JvmName("forEachIndexedKType")
forEachIndexednull181 private fun <T> KSerializer<T>.forEachIndexed(
182     typeMap: Map<KType, NavType<*>> = emptyMap(),
183     operation: (index: Int, argName: String, navType: NavType<Any?>) -> Unit
184 ) {
185     for (i in 0 until descriptor.elementsCount) {
186         val argName = descriptor.getElementName(i)
187 
188         val navType =
189             descriptor.getElementDescriptor(i).computeNavType(typeMap)
190                 ?: throw IllegalArgumentException(
191                     unknownNavTypeErrorMessage(
192                         argName,
193                         descriptor.getElementDescriptor(i).serialName,
194                         descriptor.serialName,
195                         typeMap.toString()
196                     )
197                 )
198         operation(i, argName, navType)
199     }
200 }
201 
202 @JvmName("forEachIndexedName")
forEachIndexednull203 private fun <T> KSerializer<T>.forEachIndexed(
204     typeMap: Map<String, NavType<Any?>>,
205     operation: (index: Int, argName: String, navType: NavType<Any?>) -> Unit
206 ) {
207     for (i in 0 until descriptor.elementsCount) {
208         val argName = descriptor.getElementName(i)
209         val navType = typeMap[argName]
210         checkNotNull(navType) { "Cannot locate NavType for argument [$argName]" }
211         operation(i, argName, navType)
212     }
213 }
214 
unknownNavTypeErrorMessagenull215 private fun unknownNavTypeErrorMessage(
216     fieldName: String,
217     fieldType: String,
218     className: String,
219     typeMap: String
220 ) =
221     "Route $className could not find any NavType for argument $fieldName " +
222         "of type $fieldType - typeMap received was $typeMap"
223 
224 internal fun SerialDescriptor.isValueClass(): Boolean =
225     kind == StructureKind.CLASS && isInline && elementsCount == 1
226