All Downloads are FREE. Search and download functionalities are using the official Maven repository.

commonMain.ParameterizeConfiguration.kt Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2024 Ben Woodworth
 *
 * 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.benwoodworth.parameterize

import com.benwoodworth.parameterize.ParameterizeConfiguration.Builder
import kotlin.coroutines.Continuation
import kotlin.coroutines.RestrictsSuspension
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.native.concurrent.ThreadLocal

/**
 * Configuration options for [parameterize].
 */
public class ParameterizeConfiguration internal constructor(
    /** @see Builder.decorator */
    public val decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit,

    /** @see Builder.onFailure */
    public val onFailure: OnFailureScope.(failure: Throwable) -> Unit,

    /** @see Builder.onComplete */
    public val onComplete: OnCompleteScope.() -> Unit
) {
    /** @suppress */
    @ThreadLocal
    public companion object {
        @PublishedApi
        internal val default: ParameterizeConfiguration = Builder(null).build()

        public operator fun invoke(
            from: ParameterizeConfiguration = default,
            builderAction: Builder.() -> Unit,
        ): ParameterizeConfiguration =
            Builder(from).apply(builderAction).build()
    }

    /** @suppress */
    override fun toString(): String =
        "ParameterizeConfiguration(" +
                "decorator=$decorator, " +
                "onFailure=$onFailure, " +
                "onComplete=$onComplete)"

    /** @see ParameterizeConfiguration */
    public class Builder internal constructor(from: ParameterizeConfiguration?) {
        /**
         * Decorates the [parameterize] block, enabling additional shared logic to be inserted around each iteration.
         *
         * The provided `iteration` function must be invoked exactly once. It will return without throwing even if the
         * iteration fails, guaranteeing that any post-iteration cleanup will always be executed.
         *
         * **Note:** Failures from within the [decorator] will propagate out uncaught, terminating [parameterize]
         * without [onFailure] or [onComplete] being executed.
         *
         * Defaults to:
         * ```
         * decorator = { iteration ->
         *     iteration()
         * }
         * ```
         *
         * @throws ParameterizeException if the `iteration` function is not invoked exactly once.
         *
         * @see DecoratorScope.isFirstIteration
         * @see DecoratorScope.isLastIteration
         */
        public var decorator: suspend DecoratorScope.(iteration: suspend DecoratorScope.() -> Unit) -> Unit =
            from?.decorator
                ?: { iteration ->
                    iteration()
                }

        /**
         * Invoked after each failing iteration to configure how a failure should be handled.
         *
         * This handler is executed after the [parameterize] block's [decorator] completes, and before [onComplete].
         *
         * **Note:** Failures from within [onFailure] will propagate out uncaught, terminating [parameterize] without
         * [onComplete] being executed. Immediately re-throwing the provided failure in this way may be desirable if the
         * [parameterize] block is not meant to fail.
         *
         * Defaults to:
         * ```
         * onFailure = { failure ->
         *     throw failure
         * }
         * ```
         *
         * @see OnFailureScope.breakEarly
         * @see OnFailureScope.recordFailure
         */
        public var onFailure: OnFailureScope.(failure: Throwable) -> Unit = from?.onFailure
            ?: { failure ->
                throw failure
            }

        /**
         * Invoked after [parameterize] has finished iterating, enabling the final results to be handled.
         *
         * **Note:** The default [onFailure] handler prevents [onComplete] from executing when there is a failure,
         * immediately throwing instead of continuing and tracking failures to be handled here.
         *
         *
         * Defaults to:
         * ```
         * onComplete = {
         *     if (failureCount > 0) throw ParameterizeFailedError()
         * }
         * ```
         */
        public var onComplete: OnCompleteScope.() -> Unit = from?.onComplete
            ?: {
                if (failureCount > 0) throw ParameterizeFailedError()
            }

        internal fun build(): ParameterizeConfiguration = ParameterizeConfiguration(
            decorator = decorator,
            onFailure = onFailure,
            onComplete = onComplete,
        )
    }

    /** @see Builder.decorator */
    @RestrictsSuspension
    public class DecoratorScope internal constructor(
        private val parameterizeState: ParameterizeState
    ) {
        private var initializedIsLastIteration = false

        /**
         * True if this is the first [parameterize] iteration.
         */
        public val isFirstIteration: Boolean = parameterizeState.isFirstIteration

        /**
         * True if this is the last [parameterize] iteration. Cannot be known until after the iteration runs.
         *
         * @throws ParameterizeException if accessed before the iteration function has been invoked.
         */
        public var isLastIteration: Boolean = false
            get() {
                parameterizeState.checkState(initializedIsLastIteration) {
                    "Last iteration cannot be known until after the iteration function is invoked"
                }

                return field
            }
            internal set(value) {
                field = value
                initializedIsLastIteration = true
            }

        internal suspend inline fun suspendDecorator(
            crossinline block: (Continuation) -> Unit
        ): Unit =
            suspendCoroutineUninterceptedOrReturn { c ->
                block(c)
                COROUTINE_SUSPENDED
            }
    }

    /** @see Builder.onFailure */
    public class OnFailureScope internal constructor(
        private val state: ParameterizeState,

        /**
         * The number of iterations that have been executed, including this failing iteration.
         */
        public val iterationCount: Long,

        /**
         * The number of failures that have occurred, including this failure.
         */
        public val failureCount: Long
    ) {
        /**
         * The parameter arguments that resulted in the failure.
         */
        public val arguments: List> by lazy {
            state.getFailureArguments()
        }

        /**
         * Specifies whether [parameterize] should break and complete early, or continue to the next iteration.
         *
         * Defaults to `false`.
         */
        public var breakEarly: Boolean = false

        /**
         * Specifies whether this iteration's [arguments] and the resulting failure should be added to the list of
         * [recordedFailures][OnCompleteScope.recordedFailures] provided to [ParameterizeConfiguration.onComplete].
         *
         * Defaults to `false`.
         */
        public var recordFailure: Boolean = false
    }

    /** @see Builder.onComplete */
    public class OnCompleteScope internal constructor(
        /**
         * The total number of iterations, including [skips][skipCount] and [failures][failureCount].
         */
        public val iterationCount: Long,

        /**
         * The number of skipped iterations.
         *
         * A [parameterize] iteration will be skipped when a [parameter] is declared without any
         * [arguments][ParameterizeScope.Parameter.arguments].
         */
        public val skipCount: Long,

        /**
         * The number of failing iterations.
         */
        public val failureCount: Long,

        /**
         * True if [parameterize] completed with more combinations of arguments left to iterate, because of
         * [onFailure]'s [breakEarly][OnFailureScope.breakEarly] being set.
         *
         * **Note:** Setting [breakEarly][OnFailureScope.breakEarly] on the last iteration will not make this property
         * true, since all combinations have been iterated through.
         */
        public val completedEarly: Boolean,

        /** @see OnFailureScope.recordFailure */
        public val recordedFailures: List
    ) {
        /**
         * The number of successful iterations, completed without [skipping][skipCount] or [failing][failureCount].
         */
        public val successCount: Long
            get() = iterationCount - skipCount - failureCount

        /** @see ParameterizeFailedError */
        public operator fun ParameterizeFailedError.Companion.invoke(): ParameterizeFailedError =
            ParameterizeFailedError(recordedFailures, successCount, failureCount, completedEarly)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy