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

commonMain.internal.actions.WriteValueStateAction.kt Maven / Gradle / Ivy

There is a newer version: 0.23.0
Show newest version
package io.kform.internal.actions

import io.kform.*
import io.kform.internal.*
import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.launch

/** Action writing values and their state. */
internal abstract class WriteValueStateAction(formManager: FormManager) :
    ValueStateAction(formManager) {
    abstract val parentPath: AbsolutePath?
    abstract val fragment: AbsolutePathFragment?

    override val accesses =
        listOf(
            AccessValueStateTree(ActionAccessType.Write),
            AccessValidationState(ActionAccessType.Write),
            AccessStatefulValidationDeferredState(ActionAccessType.Read),
            AccessIsTouched(ActionAccessType.Read),
            AccessDescendantsDisplayingIssues(ActionAccessType.Read)
        )
    override val accessedPaths
        get() = listOf(accessedPath)

    /** Path representing the resources that this action will access. */
    abstract val accessedPath: AbsolutePath

    val path
        get() = if (parentPath != null) parentPath!! + fragment!! else AbsolutePath.ROOT

    override fun overridesConflictingAction(action: Action<*>): Boolean =
        (action is SetAction &&
            fragment !is AbsolutePathFragment.CollectionEnd &&
            (path + AbsolutePathFragment.RecursiveWildcard).contains(action.path)) ||
            (action is RemoveAction &&
                (path + AbsolutePathFragment.RecursiveWildcard).contains(action.parentPath))

    /**
     * Runs [fn] with an events bus to be passed to schemas, where each event emitted to it is
     * appropriately handled. This is used by [SetAction] and [RemoveAction] to handle events
     * emitted by schemas. Returns the result of [fn] after all events have been handled.
     */
    protected suspend fun  withSchemaEventsBus(
        fn: suspend (eventsChannel: SchemaEventsBus) -> TResult
    ): TResult = fn(SchemaEventsChannelBus { event -> handleSchemaEvent(event) })

    /** Handles an event emitted by a schema. */
    private suspend fun handleSchemaEvent(event: ValueEvent<*>) {
        FormManager.logger.trace { "Handling schema event: $event" }
        @Suppress("UNCHECKED_CAST")
        when (event) {
            is ValueEvent.Init<*> -> handleSchemaInitEvent(event as ValueEvent.Init)
            is ValueEvent.Change<*> -> handleSchemaChangeEvent(event as ValueEvent.Change)
            is ValueEvent.Destroy<*> -> handleSchemaDestroyAction(event as ValueEvent.Destroy)
            is ValueEvent.Add<*, *> -> handleSchemaAddAction(event as ValueEvent.Add)
            is ValueEvent.Remove<*, *> ->
                handleSchemaRemoveAction(event as ValueEvent.Remove)
        }
    }

    /**
     * Returns the state of the value with the provided [path] and [schema], creating it and its
     * parent states when necessary.
     */
    private fun initState(path: AbsolutePath, schema: Schema<*>): StateImpl {
        var parentState: ParentState? = null
        var fragment: AbsolutePathFragment.Id? = null
        var state =
            if (path == AbsolutePath.ROOT) formManager.formState
            else {
                parentState =
                    path.parent().let {
                        initState(it, schemaInfo(it).single().schema) as ParentState
                    }
                fragment = path.lastFragment as AbsolutePathFragment.Id
                parentState.childrenStates(path, fragment).singleOrNull()?.state
            }
                as StateImpl?
        if (state == null) {
            val nValidations = schema.validations.size
            val nStatefulValidations =
                schema.validations.count { validation -> validation is StatefulValidation<*, *> }
            state =
                when (schema) {
                    is CollectionSchema<*, *> ->
                        CollectionStateImpl(
                            schema.childrenStatesContainer(),
                            nValidations,
                            nStatefulValidations
                        )
                    is ParentSchema<*> ->
                        ParentStateImpl(
                            schema.childrenStatesContainer(),
                            nValidations,
                            nStatefulValidations
                        )
                    else -> StateImpl(nValidations, nStatefulValidations)
                }
            if (path == AbsolutePath.ROOT) {
                formManager.formState = state
            } else {
                parentState!!.setState(fragment!!, state)
            }
        }
        return state
    }

    /**
     * Destroys the state of the value with the provided [path] and all children states. This
     * function **can** be called with the path of a value whose state has already been destroyed.
     */
    private suspend fun destroyState(path: AbsolutePath) {
        // Keep track of how many values were destroyed that were displaying local errors/warnings
        var destroyedDisplayingLocalError = 0
        var destroyedDisplayingLocalWarning = 0

        for (info in stateInfo(path + AbsolutePathFragment.RecursiveWildcard)) {
            val state = info.state as StateImpl? ?: continue

            when (state.localDisplayStatus()) {
                DisplayStatus.Error -> ++destroyedDisplayingLocalError
                DisplayStatus.Warning -> ++destroyedDisplayingLocalWarning
                else -> {}
            }

            // Destroy state (cancels deferred validation states)
            state.destroy()

            // Destroy stateful validation states (if the validation state was in the process of
            // being updated, then the above `state.destroy()` will cause the validation state to be
            // destroyed by the update state action, otherwise we destroy it here)
            var statefulValidationIndex = 0
            for (validation in info.schema.validations) {
                if (validation is StatefulValidation<*, *>) {
                    val deferredState =
                        state.getStatefulValidationDeferredState(statefulValidationIndex++)
                            ?: continue
                    @Suppress("UNCHECKED_CAST") (validation as StatefulValidation)
                    formManager.scope.launch(CoroutineName("Destroy stateful validation state")) {
                        try {
                            // This can throw due to the above `cancel` or due to an error while
                            // updating the state, in which case we don't need to do anything as
                            // explained above; otherwise, we destroy the validation state
                            val validationState = deferredState.await()

                            FormManager.logger.trace {
                                "At '$path': Destroying validation '$validation' state: " +
                                    "$validationState"
                            }
                            validation.destroyState(validationState)
                        } catch (_: Throwable) {}
                    }
                }
            }
        }

        // Update "descendants displaying issues" of ancestors
        if (
            (destroyedDisplayingLocalError != 0 || destroyedDisplayingLocalWarning != 0) &&
                path != AbsolutePath.ROOT
        ) {
            formManager.scheduleAction(
                UpdateDescendantsDisplayingIssuesAction(
                    formManager,
                    path.parent(),
                    -destroyedDisplayingLocalError,
                    -destroyedDisplayingLocalWarning
                )
            )
        }
    }

    /** Removes all cached/external issues at [path]. */
    private suspend fun invalidateLocalValidations(path: AbsolutePath) {
        val (state, schema) = stateInfo(path).single()
        state as StateImpl

        val wasValidated =
            state.validationStatus == ValidationStatus.Validated ||
                state.validationStatus == ValidationStatus.ValidatedExceptionally
        val oldLocalDisplayStatus = state.localDisplayStatus()
        val oldDisplayStatus = state.displayStatus()

        state.removeCachedIssues()
        val removedExternalIssues = state.removeExternalIssues()
        if (removedExternalIssues.isNotEmpty()) {
            formManager.externalIssuesDependencies.removeDependenciesOfExternalIssues(
                removedExternalIssues
            )
        }

        // Update validation state as needed
        if (wasValidated) {
            state.validationStatus = ValidationStatus.Unvalidated
        }
        ValidateAction.updateValidationState(
            formManager,
            schema,
            path,
            state,
            oldLocalDisplayStatus,
            oldDisplayStatus,
            emitValidationChange = wasValidated || removedExternalIssues.isNotEmpty()
        )
    }

    /** Invalidates all validations that depend on the value at [path]. */
    private suspend fun invalidateDependingValidations(path: AbsolutePath) {
        for ((dependingPath, validation, validationIndex) in formManager.pathDependencies[path]) {
            val resolvedDependingPath = path.resolve(dependingPath)
            formManager.scheduleAction(
                InvalidateValidationAction(
                    formManager,
                    resolvedDependingPath,
                    validation,
                    validationIndex
                )
            )
        }
    }

    /** Removes all external issues depending on (but not set on) [path]. */
    private suspend fun removeDependingExternalIssues(path: AbsolutePath) {
        val dependingExternalIssues =
            formManager.externalIssuesDependencies.getAndRemoveExternalIssuesDependentOnPath(path)
        if (dependingExternalIssues.isNotEmpty()) {
            formManager.scheduleAction(
                RemoveDependingExternalIssuesAction(formManager, dependingExternalIssues)
            )
        }
    }

    /** Updates the state of all stateful validations observing the given [path]. */
    private suspend fun updateObservingStatefulValidationsStates(event: ValueEvent<*>) {
        // If the event's path matches against multiple paths in the `toObserve` list of a
        // validation, we mustn't update the state of said validation multiple times; as such, we
        // keep track of the validations we have already updated
        val updatedValidations = mutableSetOf>()
        for ((
            observingPath, validation, validationIndex, statefulValidationIndex, toObserveIndex) in
            formManager.observedPathDependencies[event.path]) {
            val resolvedObservingPath = event.path.resolve(observingPath)
            val validationInfo = Pair(resolvedObservingPath, validationIndex)
            // Mustn't update state with `init`/`destroy` events of the value being validated; also
            // mustn't update already updated state
            if (
                (event.path != resolvedObservingPath ||
                    (event !is ValueEvent.Init && event !is ValueEvent.Destroy)) &&
                    !updatedValidations.contains(validationInfo)
            ) {
                @Suppress("UNCHECKED_CAST")
                formManager.scheduleAction(
                    UpdateStatefulValidationsStateAction(
                        formManager,
                        resolvedObservingPath,
                        validation as StatefulValidation,
                        validationIndex,
                        statefulValidationIndex,
                        toObserveIndex,
                        event as ValueEvent
                    )
                )
                updatedValidations += validationInfo
            }
        }
    }

    /** Handles an "init [event]" emitted by a schema. */
    private suspend fun handleSchemaInitEvent(event: ValueEvent.Init) {
        // Initialise state and possibly parent states
        initState(event.path, event.schema)

        invalidateDependingValidations(event.path)
        updateObservingStatefulValidationsStates(event)
        removeDependingExternalIssues(event.path)
        formManager.eventsBus.emit(event)
    }

    /** Handles a "change [event]" emitted by a schema. */
    private suspend fun handleSchemaChangeEvent(event: ValueEvent.Change) {
        invalidateLocalValidations(event.path)
        invalidateDependingValidations(event.path)
        updateObservingStatefulValidationsStates(event)
        removeDependingExternalIssues(event.path)
        formManager.eventsBus.emit(event)
    }

    /** Handles a "destroy [event]" emitted by a schema. */
    private suspend fun handleSchemaDestroyAction(event: ValueEvent.Destroy) {
        // Destroy state of destroyed value (state may already have been destroyed by a remove)
        destroyState(event.path)

        // Set state to `null` in parent state when the child state exists
        val parentState = stateInfo(event.path.parent()).singleOrNull()?.state as ParentState?
        val id = event.path.lastFragment as AbsolutePathFragment.Id
        if (parentState != null && (parentState !is CollectionState || parentState.hasState(id))) {
            parentState.setState(id, null)
        }

        invalidateDependingValidations(event.path)
        updateObservingStatefulValidationsStates(event)
        removeDependingExternalIssues(event.path)
        formManager.eventsBus.emit(event)
    }

    /** Handles an "add [event]" emitted by a schema. */
    private suspend fun handleSchemaAddAction(event: ValueEvent.Add) {
        invalidateLocalValidations(event.path)
        invalidateDependingValidations(event.path)
        updateObservingStatefulValidationsStates(event)
        removeDependingExternalIssues(event.path)
        formManager.eventsBus.emit(event)
    }

    /** Handles a "remove [event]" emitted by a schema. */
    private suspend fun handleSchemaRemoveAction(event: ValueEvent.Remove) {
        // Destroy state of removed value and remove its state from parent state
        val parentState = stateInfo(event.path).single().state as CollectionState
        destroyState(event.path + event.id)
        parentState.removeState(event.id)

        invalidateLocalValidations(event.path)
        invalidateDependingValidations(event.path)
        updateObservingStatefulValidationsStates(event)
        removeDependingExternalIssues(event.path)
        formManager.eventsBus.emit(event)
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy