1 /*
2  * Copyright (C) 2023 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 com.android.systemui.statusbar.commandline
18 
19 import kotlin.contracts.ExperimentalContracts
20 import kotlin.contracts.InvocationKind
21 import kotlin.contracts.contract
22 
23 /**
24  * Utilities for parsing the [String] command line arguments. Arguments are related to the
25  * [Parameter] type, which declares the number of, and resulting type of, the arguments that it
26  * takes when parsing. For Example:
27  * ```
28  * my-command --param <str> --param2 <int>
29  * ```
30  *
31  * Defines 2 parameters, the first of which takes a string, and the second requires an int. Because
32  * fundamentally _everything_ is a string, we have to define a convenient way to get from the
33  * incoming `StringArg` to the resulting `T`-arg, where `T` is the type required by the client.
34  *
35  * Parsing is therefore a relatively straightforward operation: (String) -> T. However, since
36  * parsing can always fail, the type is actually (String) -> Result<T>. We will always want to fail
37  * on the first error and propagate it to the caller (typically this results in printing the `help`
38  * message of the command`).
39  *
40  * The identity parsing is trivial:
41  * ```
42  * (s: String) -> String = { s -> s }
43  * ```
44  *
45  * Basic mappings are actually even provided by Kotlin's stdlib:
46  * ```
47  * (s: String) -> Boolean = { s -> s.toBooleanOrNull() }
48  * (s: String) -> Int = { s -> s.toIntOrNull() }
49  * ...
50  * ```
51  *
52  * In order to properly encode errors, we will ascribe an error type to any `null` values, such that
53  * parsing looks like this:
54  * ```
55  * val mapping: (String) -> T? = {...} // for some T
56  * val parser: (String) -> Result<T> = { s ->
57  *   mapping(s)?.let {
58  *     Result.success(it)
59  *   } ?: Result.failure(/* some failure type */)
60  * }
61  * ```
62  *
63  * Composition
64  *
65  * The ability to compose value parsing enables us to provide a couple of reasonable default parsers
66  * and allow clients to seamlessly build upon that using map functions. Consider the case where we
67  * want to validate that a value is an [Int] between 0 and 100. We start with the generic [Int]
68  * parser, and a validator, of the type (Int) -> Result<Int>:
69  * ```
70  * val intParser = { s ->
71  *   s.toStringOrNull().?let {...} ?: ...
72  * }
73  *
74  * val validator = { i ->
75  *   if (i > 100 || i < 0) {
76  *     Result.failure(...)
77  *   } else {
78  *     Result.success(i)
79  *   }
80  * ```
81  *
82  * In order to combine these functions, we need to define a new [flatMap] function that can get us
83  * from a `Result<T>` to a `Result<R>`, and short-circuit on any error. We want to see this:
84  * ```
85  * val validatingParser = { s ->
86  *   intParser.invoke(s).flatMap { i ->
87  *     validator(i)
88  *   }
89  * }
90  * ```
91  *
92  * The flatMap is relatively simply defined, we can mimic the existing definition for [Result.map],
93  * though the implementation is uglier because of the `internal` definition for `value`
94  *
95  * ```
96  * inline fun <R, T> Result<T>.flatMap(transform: (value: T) -> Result<R>): Result<R> {
97  *   return when {
98  *     isSuccess -> transform(getOrThrow())
99  *     else -> Result.failure(exceptionOrNull()!!)
100  *   }
101  * }
102  * ```
103  */
104 
105 /**
106  * Given a [transform] that returns a [Result], apply the transform to this result, unwrapping the
107  * return value so that
108  *
109  * These [contract] and [callsInPlace] methods are copied from the [Result.map] definition
110  */
111 @OptIn(ExperimentalContracts::class)
112 inline fun <R, T> Result<T>.flatMap(transform: (value: T) -> Result<R>): Result<R> {
113     contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) }
114 
115     return when {
116         // Should never throw, we just don't have access to [this.value]
117         isSuccess -> transform(getOrThrow())
118         // Exception should never be null here
119         else -> Result.failure(exceptionOrNull()!!)
120     }
121 }
122 
123 /**
124  * ValueParser turns a [String] into a Result<A> by applying a transform. See the default
125  * implementations below for starting points. The intention here is to provide the base mappings and
126  * allow clients to attach their own transforms. They are expected to succeed or return null on
127  * failure. The failure is propagated to the command parser as a Result and will fail on any
128  * [Result.failure]
129  */
130 fun interface ValueParser<out A> {
131     fun parseValue(value: String): Result<A>
132 }
133 
134 /** Map a [ValueParser] of type A to one of type B, by applying the given [transform] */
135 inline fun <A, B> ValueParser<A>.map(crossinline transform: (A) -> B?): ValueParser<B> {
136     return ValueParser<B> { value ->
137         this.parseValue(value).flatMap { a ->
138             transform(a)?.let { b -> Result.success(b) }
139                 ?: Result.failure(ArgParseError("Failed to transform value $value"))
140         }
141     }
142 }
143 
144 /**
145  * Base type parsers are provided by the lib, and can be simply composed upon by [ValueParser.map]
146  * functions on the parser
147  */
148 
149 /** String parsing always succeeds if the value exists */
150 private val parseString: ValueParser<String> = ValueParser { value -> Result.success(value) }
151 
152 private val parseBoolean: ValueParser<Boolean> = ValueParser { value ->
153     value.toBooleanStrictOrNull()?.let { Result.success(it) }
154         ?: Result.failure(ArgParseError("Failed to parse $value as a boolean"))
155 }
156 
157 private val parseInt: ValueParser<Int> = ValueParser { value ->
158     value.toIntOrNull()?.let { Result.success(it) }
159         ?: Result.failure(ArgParseError("Failed to parse $value as an int"))
160 }
161 
162 private val parseFloat: ValueParser<Float> = ValueParser { value ->
163     value.toFloatOrNull()?.let { Result.success(it) }
164         ?: Result.failure(ArgParseError("Failed to parse $value as a float"))
165 }
166 
167 /** Default parsers that can be use as-is, or [map]ped to another type */
168 object Type {
169     val Boolean = parseBoolean
170     val Int = parseInt
171     val Float = parseFloat
172     val String = parseString
173 }
174