RouteSerializer.kt

/*
 * Copyright 2024 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

@file:OptIn(kotlinx.serialization.ExperimentalSerializationApi::class)

package androidx.navigation.serialization

import androidx.annotation.RestrictTo
import androidx.navigation.NamedNavArgument
import androidx.navigation.NavType
import androidx.navigation.navArgument
import kotlin.reflect.KType
import kotlinx.serialization.InternalSerializationApi
import kotlinx.serialization.KSerializer
import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.capturedKClass
import kotlinx.serialization.serializer

/**
 * Generates a route pattern for use in Navigation functions such as [::navigate] from
 * a serializer of class T where T is a concrete class or object.
 *
 * The generated route pattern contains the path, path args, and query args.
 * See [RouteBuilder.Builder.computeParamType] for logic on how parameter type (path or query)
 * is computed.
 *
 * @param [typeMap] A mapping of KType to the custom NavType<*>. For example given
 * an argument of "val userId: UserId", the map should contain [typeOf<UserId>() to MyNavType].
 * @param [path] The base path to append arguments to. If null, base path defaults to
 * [KSerializer.descriptor].serialName.
 */
internal fun <T> KSerializer<T>.generateRoutePattern(
    typeMap: Map<KType, NavType<*>> = emptyMap(),
    path: String? = null,
): String {
    assertNotAbstractClass {
        throw IllegalArgumentException(
            "Cannot generate route pattern from polymorphic class " +
                "${descriptor.capturedKClass?.simpleName}. Routes can only be generated from " +
                "concrete classes or objects."
        )
    }

    val map = mutableMapOf<String, NavType<Any?>>()
    for (i in 0 until descriptor.elementsCount) {
        val argName = descriptor.getElementName(i)
        val type = descriptor.getElementDescriptor(i).computeNavType(argName, typeMap)
        map[argName] = type
    }
    val builder = if (path != null) {
        RouteBuilder.Pattern(path, this, map)
    } else {
        RouteBuilder.Pattern(this, map)
    }
    for (elementIndex in 0 until descriptor.elementsCount) {
        builder.addArg(elementIndex)
    }
    return builder.build()
}

/**
 * Returns a list of [NamedNavArgument].
 *
 * By default this method only supports conversion to NavTypes that are declared in
 * [NavType.Companion] class. To convert non-natively supported types, the custom NavType must be
 * provided via [typeMap].
 *
 * Short summary of NavArgument generation principles:
 * 1. NavArguments will only be generated on variables with kotlin backing fields
 * 2. Arg Name is based on variable name
 * 3. Nullability is based on variable Type's nullability
 * 4. defaultValuePresent is based on whether variable has default value
 *
 * This generator does not check for validity as a NavType.
 * This means if a NavType is not nullable (i.e. Int), and the KType was Int?, it relies on the
 * navArgument builder to throw exception.
 *
 * @param [typeMap] A mapping of KType to the custom NavType<*>. For example given
 * an argument of "val userId: UserId", the map should
 * contain [typeOf<UserId>() to MyNavType]. Custom NavTypes take priority over native
 * NavTypes. This means you can override native NavTypes such as [NavType.IntType] with your own
 * implementation of NavType<Int>.
 */
internal fun <T> KSerializer<T>.generateNavArguments(
    typeMap: Map<KType, NavType<*>> = emptyMap()
): List<NamedNavArgument> {
    assertNotAbstractClass {
        throw IllegalArgumentException(
            "Cannot generate NavArguments for polymorphic serializer $this. Arguments " +
                "can only be generated from concrete classes or objects."
        )
    }

    return List(descriptor.elementsCount) { index ->
        val name = descriptor.getElementName(index)
        navArgument(name) {
            val element = descriptor.getElementDescriptor(index)
            val isNullable = element.isNullable
            type = element.computeNavType(name, typeMap)
            nullable = isNullable
            if (descriptor.isElementOptional(index)) {
                // Navigation mostly just cares about defaultValuePresent state for
                // non-nullable args to verify DeepLinks at a later stage.
                // We know that non-nullable types cannot have null values, so it is
                // safe to mark this as true without knowing actual value.
                unknownDefaultValuePresent = true
            }
        }
    }
}

/**
 * Generates a route filled in with argument value for use in Navigation functions such as
 * [::navigate] from a destination instance of type T.
 *
 * The generated route pattern contains the path, path args, and query args.
 * See [RouteBuilder.Builder.computeParamType] for logic on how parameter type (path or query)
 * is computed.
 *
 * [T] as receiver to allow secondary constructors for nav builders (i.e. NavGraphBuilder)
 * to take object <T : Any> as parameter
 */
@OptIn(InternalSerializationApi::class)
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun <T : Any> T.generateRouteWithArgs(
    typeMap: Map<String, NavType<Any?>>
): String = RouteEncoder(this::class.serializer(), typeMap).encodeRouteWithArgs(this)

private fun <T> KSerializer<T>.assertNotAbstractClass(handler: () -> Unit) {
    // abstract class
    if (this is PolymorphicSerializer) {
        handler()
    }
}

/**
 * Computes and return the [NavType] based on the SerialDescriptor of a class type.
 *
 * Match priority:
 * 1. Match with custom NavType provided in [typeMap]
 * 2. Match to a built-in NavType such as [NavType.IntType], [NavType.BoolArrayType] etc.
 */
@Suppress("UNCHECKED_CAST")
private fun SerialDescriptor.computeNavType(
    name: String,
    typeMap: Map<KType, NavType<*>>
): NavType<Any?> {
    val customType = typeMap.keys
        .find { kType -> matchKType(kType) }
        ?.let { typeMap[it] } as? NavType<Any?>
    val result = customType ?: getNavType()
    if (result == UNKNOWN) {
        throw IllegalArgumentException(
            "Cannot cast $name of type $serialName to a NavType. Make sure " +
                "to provide custom NavType for this argument."
        )
    }
    return result as NavType<Any?>
}