1 /*
2  * 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(ExperimentalSerializationApi::class)
18 
19 package androidx.navigation.serialization
20 
21 import androidx.navigation.CollectionNavType
22 import androidx.navigation.NavType
23 import kotlinx.serialization.ExperimentalSerializationApi
24 import kotlinx.serialization.KSerializer
25 
26 /** Builds navigation routes from a destination class or instance. */
27 internal class RouteBuilder<T> {
28     private val serializer: KSerializer<T>
29     private val path: String
30     private var pathArgs = ""
31     private var queryArgs = ""
32 
33     /**
34      * Create a builder that builds a route URL
35      *
36      * @param serializer The serializer for destination type T (class, object etc.) to build the
37      *   route for.
38      */
39     constructor(serializer: KSerializer<T>) {
40         this.serializer = serializer
41         path = serializer.descriptor.serialName
42     }
43 
44     /**
45      * Create a builder that builds a route URL
46      *
47      * @param path The base uri path to which arguments are appended
48      * @param serializer The serializer for destination type T (class, object etc.) to build the
49      *   route for.
50      */
51     constructor(path: String, serializer: KSerializer<T>) {
52         this.serializer = serializer
53         this.path = path
54     }
55 
56     /** Returns final route */
buildnull57     fun build() = path + pathArgs + queryArgs
58 
59     /** Append string to the route's (url) path */
60     private fun addPath(path: String) {
61         pathArgs += "/$path"
62     }
63 
64     /** Append string to the route's (url) query parameter */
addQuerynull65     private fun addQuery(name: String, value: String) {
66         val symbol = if (queryArgs.isEmpty()) "?" else "&"
67         queryArgs += "$symbol$name=$value"
68     }
69 
appendPatternnull70     fun appendPattern(index: Int, name: String, type: NavType<Any?>) {
71         val paramType = computeParamType(index, type)
72         when (paramType) {
73             ParamType.PATH -> addPath("{$name}")
74             ParamType.QUERY -> addQuery(name, "{$name}")
75         }
76     }
77 
appendArgnull78     fun appendArg(index: Int, name: String, type: NavType<Any?>, value: List<String>) {
79         val paramType = computeParamType(index, type)
80         when (paramType) {
81             ParamType.PATH -> {
82                 // path arguments should be a single string value of primitive types
83                 require(value.size == 1) {
84                     "Expected one value for argument $name, found ${value.size}" + "values instead."
85                 }
86                 addPath(value.first())
87             }
88             ParamType.QUERY -> value.forEach { addQuery(name, it) }
89         }
90     }
91 
92     /**
93      * Given the descriptor of [T], computes the [ParamType] of the element (argument) at [index].
94      *
95      * Query args if either conditions met:
96      * 1. has default value
97      * 2. is of [CollectionNavType]
98      */
computeParamTypenull99     private fun computeParamType(index: Int, type: NavType<Any?>) =
100         if (type is CollectionNavType || serializer.descriptor.isElementOptional(index)) {
101             ParamType.QUERY
102         } else {
103             ParamType.PATH
104         }
105 
106     private enum class ParamType {
107         PATH,
108         QUERY
109     }
110 }
111