SpecificationComputer.kt

/*
 * Copyright 2021 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 androidx.window.core

import android.util.Log
import androidx.window.core.SpecificationComputer.VerificationMode.LOG
import androidx.window.core.SpecificationComputer.VerificationMode.QUIET
import androidx.window.core.SpecificationComputer.VerificationMode.STRICT

/**
 * An [Exception] to signal that strict mode has been violated. This class should only be used
 * within the [SpecificationComputer]
 */
internal class WindowStrictModeException(message: String) : Exception(message)

/**
 * The root abstract class to represent a [SpecificationComputer]. A [SpecificationComputer] can be
 * used to validate assumptions about objects and interfaces when working with OEM provided
 * libraries. The [SpecificationComputer] has three verification modes, [STRICT], [LOG], and
 * [QUIET]. For [STRICT] mode, the [SpecificationComputer] will throw an exception when a required
 * condition is not met. For [LOG] mode, the [SpecificationComputer] will create a debug log when a
 * required condition is not met. For [QUIET] mode, value is simply converted to null if the
 * specification is not met.
 */
internal abstract class SpecificationComputer<T : Any> {
    /**
     * An enum to specify which [VerificationMode] should be used with the [SpecificationComputer]
     */
    enum class VerificationMode {
        STRICT,
        LOG,
        QUIET
    }

    /**
     * Checks if the required condition is met and returns a [SpecificationComputer] based on the
     * result. If the condition is met then the [SpecificationComputer] that is returned will
     * continue to process additional checks. If the condition fails then the
     * [SpecificationComputer] will have different behavior depending on the verification mode. If
     * the verification mode is [STRICT] an [Exception] will be thrown when [compute] is
     * called. If the verification mode is [LOG] a debug log will be written. If the verification
     * mode is [QUIET] then an empty [SpecificationComputer] is returned.
     *
     * Use [require] when an assumption must be true. One key example is when translating from an
     * OEM provided library into a local object you want to verify the attributes. If an attribute
     * does not match expectations then this can raise an exception at the source.
     *
     * One use case for this is our integration tests for devices. We can write good assumptions in
     * a test because we would need external knowledge of the device. One example is a device with a
     * fold. We expect the fold to propagate through the translation layer but we can only assert
     * the presence of a fold if we know the device has a fold. As a substitution we can turn on
     * strict mode and this will throw an exception when translating causing the test to fail.
     *
     * @param message a description of what the condition is testing. This should be user readable
     * @param condition a test that the current value is expected to pass.
     * @return A [SpecificationComputer] that will continue computing if the condition was met or
     * skip subsequent checks if the condition was not met.
     */
    abstract fun require(message: String, condition: T.() -> Boolean): SpecificationComputer<T>

    /**
     * Computes the result of the previous checks and the returned value depends on the
     * [VerificationMode]. For [STRICT] mode an [Exception] may be thrown if the condition was
     * required. For [LOG] mode a debug log is written to the [Logger]. For [QUIET] mode there are
     * no side effects.
     * @return The value if all the checks passed or none of the checks were required, null
     * otherwise.
     * @throws WindowStrictModeException if [STRICT] mode is set and a required check fails.
     */
    abstract fun compute(): T?

    protected fun createMessage(value: Any, message: String): String {
        return "$message value: $value"
    }

    companion object {
        /**
         * Start a specification for the receiver.
         *
         * @param T a non-null value that the specification will run against.
         * @param tag an identifier that will be used when writing debug logs.
         * @param verificationMode determines if the checks should throw, log, or fail silently.
         * @param logger a [Logger] that can be substituted for testing purposes. The default
         * value writes a log to LogCat
         * @return A [SpecificationComputer] that is initially valid and can check other
         * conditions.
         */
        fun <T : Any> T.startSpecification(
            tag: String,
            verificationMode: VerificationMode = BuildConfig.verificationMode,
            logger: Logger = AndroidLogger
        ): SpecificationComputer<T> {
            return ValidSpecification(this, tag, verificationMode, logger)
        }
    }
}

/**
 * A [SpecificationComputer] where the value has passed all previous checks.
 */
private class ValidSpecification<T : Any>(
    val value: T,
    val tag: String,
    val verificationMode: VerificationMode,
    val logger: Logger
) : SpecificationComputer<T>() {

    override fun require(message: String, condition: T.() -> Boolean): SpecificationComputer<T> {
        return if (condition(value)) {
            this
        } else {
            FailedSpecification(
                value = value,
                tag = tag,
                message = message,
                logger = logger,
                verificationMode = verificationMode
            )
        }
    }

    override fun compute(): T {
        return value
    }
}

/**
 * A [SpecificationComputer] that has failed a required check
 */
private class FailedSpecification<T : Any>(
    val value: T,
    val tag: String,
    val message: String,
    val logger: Logger,
    val verificationMode: VerificationMode
) : SpecificationComputer<T>() {

    val exception: WindowStrictModeException =
        WindowStrictModeException(createMessage(value, message)).apply {
            stackTrace = stackTrace.drop(2).toTypedArray()
        }

    override fun require(message: String, condition: T.() -> Boolean): SpecificationComputer<T> {
        return this
    }

    override fun compute(): T? {
        return when (verificationMode) {
            STRICT -> throw exception
            LOG -> {
                logger.debug(tag, createMessage(value, message))
                null
            }
            QUIET -> null
        }
    }
}

internal interface Logger {
    fun debug(tag: String, message: String)
}

internal object AndroidLogger : Logger {
    override fun debug(tag: String, message: String) {
        Log.d(tag, message)
    }
}