1 /*
<lambda>null2 * 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 */
interfacenull130 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] */
mapnull135 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 */
valuenull150 private val parseString: ValueParser<String> = ValueParser { value -> Result.success(value) }
151
valuenull152 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
valuenull157 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
valuenull162 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