diff options
| author | 2023-12-01 13:47:36 -0500 | |
|---|---|---|
| committer | 2023-12-07 22:18:37 +0000 | |
| commit | 2722e36e5591398cdf96be94639fe9ccc055a3d6 (patch) | |
| tree | fa68944754e88edbdab8e40cafb343b598c3b26f /java/src | |
| parent | c85c6c55ebdeef744cb812b74fed30cd2b8a4d49 (diff) | |
Validation framework
This pattern grew out of existing Intent data extraction code.
Goals
* abstract away the details of processing
* provide informative errors and warnings
* self documenting usage
* able to report multiple findings (warnings)
* fail assertions in acontrolled manner, not by throwing
Sample usage:
```
fun readArgs(
val bundle: Bundle
) = validateFrom(bundle::get): ValidationResult<ExampleObject> {
val intValue = required(value<Int>("KEY_1"))
val stringValue = required(value<String>("KEY_2"))
val arrayValue = optional(array<String>("KEY_3"))
val doubleValue = optional(value<Double>("KEY_4"))
ExampleObject(intValue, stringValue, arrayValue, doubleValue)
}
```
Bug: 300157408
Test: atest com.android.intentresolver.validation
Change-Id: I517e70df84c28e42023f19c8616804bc46884b49
Diffstat (limited to 'java/src')
7 files changed, 522 insertions, 0 deletions
diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt new file mode 100644 index 00000000..9a3cc9c7 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.intentresolver.v2.validation + +import android.util.Log +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import kotlin.reflect.KClass + +sealed interface Finding { + val importance: Importance + val message: String +} + +enum class Importance { + CRITICAL, + WARNING, +} + +val Finding.logcatPriority + get() = + when (importance) { + CRITICAL -> Log.ERROR + else -> Log.WARN + } + +private fun formatMessage(key: String? = null, msg: String) = buildString { + key?.also { append("['$key']: ") } + append(msg) +} + +data class IgnoredValue( + val key: String, + val reason: String, +) : Finding { + override val importance = WARNING + + override val message: String + get() = formatMessage(key, "Ignored. $reason") +} + +data class RequiredValueMissing( + val key: String, + val allowedType: KClass<*>, +) : Finding { + + override val importance = CRITICAL + + override val message: String + get() = + formatMessage( + key, + "expected value of ${allowedType.simpleName}, " + "but no value was present" + ) +} + +data class WrongElementType( + val key: String, + override val importance: Importance, + val container: KClass<*>, + val actualType: KClass<*>, + val expectedType: KClass<*> +) : Finding { + override val message: String + get() = + formatMessage( + key, + "${container.simpleName} expected with elements of " + + "${expectedType.simpleName} " + + "but found ${actualType.simpleName} values instead" + ) +} + +data class ValueIsWrongType( + val key: String, + override val importance: Importance, + val actualType: KClass<*>, + val allowedTypes: List<KClass<*>>, +) : Finding { + + override val message: String + get() = + formatMessage( + key, + "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " + + "but was ${actualType.simpleName}" + ) +} + +data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding { + override val importance: Importance + get() = CRITICAL + override val message: String + get() = + formatMessage( + key, + "An unhandled exception was caught during validation: " + + thrown.stackTraceToString() + ) +} diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt new file mode 100644 index 00000000..46939602 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.intentresolver.v2.validation + +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING + +/** + * Provides a mechanism for validating a result from a set of properties. + * + * The results of validation are provided as [findings]. + */ +interface Validation { + val findings: List<Finding> + + /** + * Require a valid property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + @Throws(InvalidResultError::class) fun <T> required(property: Validator<T>): T + + /** + * Request an optional value for a property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + fun <T> optional(property: Validator<T>): T? + + /** + * Report a property as __ignored__. + * + * The presence of any value will report a warning citing [reason]. + */ + fun <T> ignored(property: Validator<T>, reason: String) +} + +/** Performs validation for a specific key -> value pair. */ +interface Validator<T> { + val key: String + + /** + * Performs validation on a specific value from [source]. + * + * @param source a source for reading the property value. Values are intentionally untyped + * (Any?) to avoid upstream code from making type assertions through type inference. Types are + * asserted later using a [Validator]. + * @param importance the importance of any findings + */ + fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> +} + +internal class InvalidResultError internal constructor() : Error() + +/** + * Perform a number of validations on the source, assembling and returning a Result. + * + * When an exception is thrown by [validate], it is caught here. In response, a failed + * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception. + * + * @param validate perform validations and return a [ValidationResult] + */ +fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult<T> { + val validation = ValidationImpl(source) + return runCatching { validate(validation) } + .fold( + onSuccess = { result -> Valid(result, validation.findings) }, + onFailure = { + when (it) { + // A validator has interrupted validation. Return the findings. + is InvalidResultError -> Invalid(validation.findings) + + // Some other exception was thrown from [validate], + else -> Invalid(findings = listOf(UncaughtException(it))) + } + } + ) +} + +private class ValidationImpl(val source: (String) -> Any?) : Validation { + override val findings = mutableListOf<Finding>() + + override fun <T> optional(property: Validator<T>): T? = validate(property, WARNING) + + override fun <T> required(property: Validator<T>): T { + return validate(property, CRITICAL) ?: throw InvalidResultError() + } + + override fun <T> ignored(property: Validator<T>, reason: String) { + val result = property.validate(source, WARNING) + if (result.value != null) { + // Note: Any findings about the value (result.findings) are ignored. + findings += IgnoredValue(property.key, reason) + } + } + + private fun <T> validate(property: Validator<T>, importance: Importance): T? { + return runCatching { property.validate(source, importance) } + .fold( + onSuccess = { result -> + findings += result.findings + result.value + }, + onFailure = { + findings += UncaughtException(it, property.key) + null + } + ) + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt new file mode 100644 index 00000000..092cabe8 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.intentresolver.v2.validation + +import android.util.Log + +sealed interface ValidationResult<T> { + val value: T? + val findings: List<Finding> + + fun isSuccess() = value != null + + fun getOrThrow(): T = + checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") } + + fun <T> reportToLogcat(tag: String) { + findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) } + } +} + +data class Valid<T>(override val value: T?, override val findings: List<Finding> = emptyList()) : + ValidationResult<T> + +data class Invalid<T>(override val findings: List<Finding>) : ValidationResult<T> { + override val value: T? = null +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt new file mode 100644 index 00000000..3cefeb15 --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.intentresolver.v2.validation.types + +import android.content.Intent +import android.net.Uri +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType + +class IntentOrUri(override val key: String) : Validator<Intent> { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult<Intent> { + + return when (val value = source(key)) { + // An intent, return it. + is Intent -> Valid(value) + + // A Uri was supplied. + // Unfortunately, converting Uri -> Intent requires a toString(). + is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME)) + + // No value present. + null -> createResult(importance, RequiredValueMissing(key, Intent::class)) + + // Some other type. + else -> { + return createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(Intent::class, Uri::class) + ) + ) + } + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt new file mode 100644 index 00000000..c6c4abba --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import com.android.intentresolver.v2.validation.WrongElementType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class ParceledArray<T : Any>( + override val key: String, + private val elementType: KClass<T>, +) : Validator<List<T>> { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult<List<T>> { + + return when (val value: Any? = source(key)) { + // No value present. + null -> createResult(importance, RequiredValueMissing(key, elementType)) + + // A parcel does not transfer the element type information for parcelable + // arrays. This leads to a restored type of Array<Parcelable>, which is + // incompatible with Array<T : Parcelable>. + + // To handle this safely, treat as Array<*>, assert contents of the expected + // parcelable type, and return as a list. + + is Array<*> -> { + val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) } + when (invalid) { + // No invalid elements, result is ok. + null -> Valid(value.map { elementType.cast(it) }) + + // At least one incorrect element type found. + else -> + createResult( + importance, + WrongElementType( + key, + importance, + actualType = invalid::class, + container = Array::class, + expectedType = elementType + ) + ) + } + } + + // The value is not an Array at all. + else -> + createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(elementType) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt new file mode 100644 index 00000000..3287b84b --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.RequiredValueMissing +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator +import com.android.intentresolver.v2.validation.ValueIsWrongType +import kotlin.reflect.KClass +import kotlin.reflect.cast + +class SimpleValue<T : Any>( + override val key: String, + private val expected: KClass<T>, +) : Validator<T> { + + override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> { + val value: Any? = source(key) + return when { + // The value is present and of the expected type. + expected.isInstance(value) -> return Valid(expected.cast(value)) + + // No value is present. + value == null -> createResult(importance, RequiredValueMissing(key, expected)) + + // The value is some other type. + else -> + createResult( + importance, + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(expected) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt new file mode 100644 index 00000000..4e6e5dff --- /dev/null +++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2023 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. + */ +package com.android.intentresolver.v2.validation.types + +import com.android.intentresolver.v2.validation.Finding +import com.android.intentresolver.v2.validation.Importance +import com.android.intentresolver.v2.validation.Importance.CRITICAL +import com.android.intentresolver.v2.validation.Importance.WARNING +import com.android.intentresolver.v2.validation.Invalid +import com.android.intentresolver.v2.validation.Valid +import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.v2.validation.Validator + +inline fun <reified T : Any> value(key: String): Validator<T> { + return SimpleValue(key, T::class) +} + +inline fun <reified T : Any> array(key: String): Validator<List<T>> { + return ParceledArray(key, T::class) +} + +/** + * Convenience function to wrap a finding in an appropriate result type. + * + * An error [finding] is suppressed when [importance] == [WARNING] + */ +internal fun <T> createResult(importance: Importance, finding: Finding): ValidationResult<T> { + return when (importance) { + WARNING -> Valid(null, listOf(finding).filter { it.importance == WARNING }) + CRITICAL -> Invalid(listOf(finding)) + } +} |