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