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