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

commonMain.AbsolutePath.kt Maven / Gradle / Ivy

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

import kotlin.js.JsName
import kotlin.jvm.JvmField
import kotlin.jvm.JvmOverloads
import kotlin.math.min
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder

/**
 * Representation of an absolute path as a subtype of [Path] and as a list of absolute path
 * fragments. Absolute paths are always "resolved" (i.e. they contain no unnecessary fragments such
 * as current path fragments or consecutive recursive wildcard fragments).
 *
 * Absolute paths represent locations of data from a root location: e.g. the location of a value
 * within a value where the outer value is seen as the "root" value.
 *
 * Unlike regular paths, the root fragment is always implicit in absolute paths and is not visible
 * in the list of fragments.
 */
@JsName("AbsolutePathKt")
@Serializable(with = AbsolutePath.Serializer::class)
public class AbsolutePath
private constructor(fragments: List, needsResolving: Boolean) :
    Path(if (needsResolving) resolveImpl(listOf(PathFragment.Root), fragments) else fragments) {
    // The [realFragments] have a [PathFragment.RootFragment] at index `0`
    @Suppress("UNCHECKED_CAST")
    override val fragments: List
        get() = realFragments.drop(1) as List

    /** Size of the path (number of fragments it has). */
    public val size: Int
        get() = realFragments.size - 1

    /** Whether this path represents the root path. */
    public val isRoot: Boolean
        get() = realFragments.size == 1

    /** Last fragment of this path or `null` for the root path. */
    public val lastFragment: AbsolutePathFragment?
        get() = if (isRoot) null else realFragments.last() as AbsolutePathFragment

    /** Creates an absolute path from a list of [fragments]. */
    @JvmOverloads
    public constructor(
        fragments: List = emptyList()
    ) : this(
        // Remove consecutive recursive wildcard fragments
        listOf(PathFragment.Root) +
            fragments.filterIndexed { i, fragment ->
                fragment != AbsolutePathFragment.RecursiveWildcard ||
                    fragments.getOrNull(i + 1) != AbsolutePathFragment.RecursiveWildcard
            },
        needsResolving = false
    )

    /**
     * Creates an absolute path from a path [path]. The path will be resolved against the root path.
     */
    public constructor(
        path: Path
    ) : this(path.realFragments, needsResolving = path !is AbsolutePath)

    /**
     * Creates an absolute path from a string representation of a path [stringPath]. The string will
     * first be converted into a path and then resolved against the root path.
     */
    public constructor(stringPath: String) : this(Path(stringPath))

    /**
     * Returns the fragment of this path with index [index].
     *
     * @throws IndexOutOfBoundsException When an out of bounds [index] is provided.
     */
    public override fun fragment(index: Int): AbsolutePathFragment =
        realFragments[index + 1] as AbsolutePathFragment // Skip root fragment

    /**
     * Returns the fragment of this path with index [index].
     *
     * @throws IndexOutOfBoundsException When an out of bounds [index] is provided.
     */
    public override operator fun get(index: Int): AbsolutePathFragment = fragment(index)

    /** Returns whether this path has at least one non-recursive wildcard. */
    public fun hasWildcard(): Boolean =
        realFragments.any { fragment -> fragment is AbsolutePathFragment.Wildcard }

    /** Returns whether this path has at least one recursive wildcard. */
    public fun hasRecursiveWildcard(): Boolean =
        realFragments.any { fragment -> fragment is AbsolutePathFragment.RecursiveWildcard }

    /** Returns whether this path has at least one wildcard (either recursive or not). */
    public fun hasAnyWildcard(): Boolean =
        realFragments.any { fragment ->
            fragment is AbsolutePathFragment.Wildcard ||
                fragment is AbsolutePathFragment.RecursiveWildcard
        }

    /**
     * Returns an absolute path representing the parent of this path. Equivalent to `resolve("..")`.
     *
     * Note that the parent of the root path is the root path itself.
     */
    override fun parent(): AbsolutePath =
        AbsolutePath(parentImpl(realFragments), needsResolving = false)

    /** Returns the absolute path resulting from appending [fragments] to this path. */
    public fun append(vararg fragments: AbsolutePathFragment): AbsolutePath =
        AbsolutePath(
            // Remove consecutive recursive wildcard fragments
            realFragments +
                fragments.filterIndexed { i, fragment ->
                    fragment != AbsolutePathFragment.RecursiveWildcard ||
                        (if (i == 0) realFragments.last() else fragments[i - 1]) !=
                            AbsolutePathFragment.RecursiveWildcard
                },
            needsResolving = false
        )

    /** Returns the absolute path resulting from appending [fragment] to this path. */
    public operator fun plus(fragment: AbsolutePathFragment): AbsolutePath = append(fragment)

    /** Returns the absolute path resulting from resolving a list of [paths] against this path. */
    override fun resolve(vararg paths: Path): AbsolutePath =
        when {
            paths.isEmpty() -> this
            // Optimise case where last path (in most cases the only path) is an absolute path, in
            // which case no resolving is needed
            paths.last() is AbsolutePath -> paths.last() as AbsolutePath
            else ->
                AbsolutePath(
                    resolveImpl(
                        realFragments,
                        *paths.map { path -> path.realFragments }.toTypedArray()
                    ),
                    needsResolving = false
                )
        }

    /**
     * Returns the absolute path resulting from resolving a list of paths in string notation
     * [stringPaths] against this path.
     */
    override fun resolve(vararg stringPaths: String): AbsolutePath =
        resolve(*stringPaths.map { str -> Path(str) }.toTypedArray())

    /**
     * Returns the absolute path resulting from resolving [path] against this path. Equivalent to
     * `resolve(path)`.
     */
    public override operator fun plus(path: Path): AbsolutePath = resolve(path)

    /**
     * Returns the absolute path resulting from resolving the path in string notation [stringPath]
     * against this path. Equivalent to `resolve(stringPath)`.
     */
    public override operator fun plus(stringPath: String): AbsolutePath = resolve(stringPath)

    /**
     * Returns whether this path matches with [path]. Note that [path] will be converted to an
     * [absolute path][AbsolutePath] when it isn't one.
     *
     * For the purpose of matching, a [wildcard fragment][AbsolutePathFragment.Wildcard] matches
     * against any other fragment and a [recursive wildcard fragment]
     * [AbsolutePathFragment.RecursiveWildcard] matches against zero or more any other fragments.
     * Non-wildcard fragments match against each other on a basis of equality (`==`).
     */
    public fun matches(path: Path): Boolean =
        matchesImpl(fragments, path.toAbsolutePath().fragments)

    /**
     * Returns whether this path contains [path]. Note that [path] will be converted to an
     * [absolute path][AbsolutePath] when it isn't one.
     *
     * We say that a path `p1` contains a path `p2` when all paths that [match][matches] against
     * `p2` also [match][matches] against `p1`. I.e. `p1.contains(p2) == true` iff there does
     * **not** exist a path `p3` such that `p3.matches(p2) == true` and `p3.matches(p1) == false`.
     */
    public operator fun contains(path: Path): Boolean =
        containsImpl(fragments, path.toAbsolutePath().fragments)

    /**
     * Returns a relative path representing this path, but relative to [path]. Note that [path] will
     * be converted to an [absolute path][AbsolutePath] when it isn't one.
     *
     * In other words, if `relPath1 = path1.relativeTo(path2)`, then `path2.resolve(relPath1) ==
     * path1`.
     */
    public fun relativeTo(path: Path): Path =
        Path(relativeImpl(path.toAbsolutePath().fragments, fragments))

    override fun equals(other: Any?): Boolean =
        when {
            this === other -> true
            other !is Path -> false
            other is AbsolutePath -> realFragments == other.realFragments
            else -> realFragments == other.resolve().realFragments
        }

    override fun hashCode(): Int = realFragments.hashCode()

    override operator fun iterator(): Iterator = fragments.iterator()

    public companion object {
        // Common paths:
        /** Absolute root path (`"/"`). */
        @JvmField public val ROOT: AbsolutePath = AbsolutePath()

        /** Absolute path that matches all other paths (`"/∗∗"`). */
        @JvmField
        public val MATCH_ALL: AbsolutePath =
            AbsolutePath(listOf(AbsolutePathFragment.RecursiveWildcard))

        /**
         * Implementation of [matches]. Returns whether two lists of fragments match.
         *
         * The algorithm recursively matches the two lists taking into consideration recursive
         * wildcard fragments. It implements the following pseudo-algorithm:
         * ```
         * matches l1 l2 = match (l1, l2):
         * | ([], [])         -> true
         * | ([**], [])       -> true
         * | ([], [**])       -> true
         * | (**::t1, h2::t2) -> matches(t1, h2::t2) || matches(**::t1, t2)
         * | (h1::t1, **::t2) -> matches(h1::t1, t2) || matches(t1, **::t2)
         * | (*::t1, _::t2)   -> matches(t1, t2)
         * | (_::t1, *::t2)   -> matches(t1, t2)
         * | (h::t1, h::t2)   -> matches(t1, t2)
         * | _                -> false
         * ```
         *
         * @param f1 First list of fragments.
         * @param f2 Second list of fragments.
         * @param i1 Index of `f1`'s head. `l1` in the above pseudocode is represented by
         *   `f1.slice(i1)`.
         * @param i2 Index of `f2`'s head. `l2` in the above pseudocode is represented by
         *   `f2.slice(i2)`.
         */
        private fun matchesImpl(
            f1: List,
            f2: List,
            i1: Int = 0,
            i2: Int = 0
        ): Boolean {
            // List sizes
            val s1 = f1.size - i1
            val s2 = f2.size - i2

            // List heads
            val h1 = f1.getOrNull(i1)
            val h2 = f2.getOrNull(i2)

            // Base cases:
            // | ([], [])   -> true
            // | ([**], []) -> true
            // | ([], [**]) -> true
            if (
                (s1 == 0 && s2 == 0) ||
                    (s1 == 1 && s2 == 0 && h1 is AbsolutePathFragment.RecursiveWildcard) ||
                    (s1 == 0 && s2 == 1 && h2 is AbsolutePathFragment.RecursiveWildcard)
            ) {
                return true
            }

            // Cases where both lists have at least 1 element:
            if (s1 > 0 && s2 > 0) {
                // Recursive wildcard cases:
                // | (**::t1, h2::t2) -> matches(t1, h2::t2) || matches(**::t1, t2)
                // | (h1::t1, **::t2) -> matches(h1::t1, t2) || matches(t1, **::t2)
                if (
                    h1 is AbsolutePathFragment.RecursiveWildcard ||
                        h2 is AbsolutePathFragment.RecursiveWildcard
                ) {
                    return matchesImpl(f1, f2, i1 + 1, i2) || matchesImpl(f1, f2, i1, i2 + 1)
                }

                // Other cases:
                // | (*::t1, _::t2) -> matches(t1, t2)
                // | (_::t1, *::t2) -> matches(t1, t2)
                // | (h::t1, h::t2) -> matches(t1, t2)
                if (
                    h1 is AbsolutePathFragment.Wildcard ||
                        h2 is AbsolutePathFragment.Wildcard ||
                        h1 == h2
                ) {
                    return matchesImpl(f1, f2, i1 + 1, i2 + 1)
                }
            }

            // _ -> false
            return false
        }

        /**
         * Implementation of [contains]. Returns whether the first list of fragments contains the
         * second one.
         *
         * The algorithm recursively checks whether one list contains the other taking into
         * consideration recursive wildcard fragments. It implements the following pseudo-algorithm:
         * ```
         * contains l1 l2 = match (l1, l2):
         * | ([], [])         -> true
         * | ([**], _)        -> true
         * | (**::t1, h2::t2) -> contains(t1, h2::t2) || contains(**::t1, t2)
         * | (*::t1, h2::t2)  -> h2 != ** && contains(t1, t2)
         * | (h::t1, h::t2)   -> contains(t1, t2)
         * | _                -> false
         * ```
         *
         * @param f1 First list of fragments.
         * @param f2 Second list of fragments.
         * @param i1 Index of `f1`'s head. `l1` in the above pseudocode is represented by
         *   `f1.slice(i1)`.
         * @param i2 Index of `f2`'s head. `l2` in the above pseudocode is represented by
         *   `f2.slice(i2)`.
         */
        private fun containsImpl(
            f1: List,
            f2: List,
            i1: Int = 0,
            i2: Int = 0
        ): Boolean {
            // List sizes
            val s1 = f1.size - i1
            val s2 = f2.size - i2

            // List heads
            val h1 = f1.getOrNull(i1)
            val h2 = f2.getOrNull(i2)

            // Base cases:
            // | ([], [])  -> true
            // | ([**], _) -> true
            if ((s1 == 0 && s2 == 0) || (s1 == 1 && h1 is AbsolutePathFragment.RecursiveWildcard)) {
                return true
            }

            // Cases where both lists have at least 1 element:
            if (s1 > 0 && s2 > 0) {
                // Recursive wildcard case:
                // | (**::t1, h2::t2) -> contains(**::t1, t2) || contains(t1, h2::t2)
                if (h1 is AbsolutePathFragment.RecursiveWildcard) {
                    return containsImpl(f1, f2, i1 + 1, i2) || containsImpl(f1, f2, i1, i2 + 1)
                }

                // Other cases:
                // | (*::t1, h2::t2) -> h2 != ** && contains(t1, t2)
                // | (h::t1, h::t2)  -> h != - && contains(t1, t2)
                if (
                    (h1 is AbsolutePathFragment.Wildcard &&
                        h2 !is AbsolutePathFragment.RecursiveWildcard) || h1 == h2
                ) {
                    return containsImpl(f1, f2, i1 + 1, i2 + 1)
                }
            }

            // _ -> false
            return false
        }
    }

    /** Returns the relative path from [from] to [to]. Used to implement [relativeTo]. */
    private fun relativeImpl(
        from: List,
        to: List
    ): List {
        val result = mutableListOf()
        val size = min(from.size, to.size)
        var samePartsSize = size
        for (i in 0 until size) {
            if (from[i] != to[i]) {
                samePartsSize = i
                break
            }
        }

        for (i in samePartsSize until from.size) {
            result += PathFragment.ParentPath
        }
        result += (to.slice(samePartsSize until to.size))
        return result
    }

    /** Absolute path serialiser, serialising absolute paths as strings. */
    public object Serializer : KSerializer {
        override val descriptor: SerialDescriptor =
            PrimitiveSerialDescriptor("io.kform.AbsolutePath", PrimitiveKind.STRING)

        override fun serialize(encoder: Encoder, value: AbsolutePath): Unit =
            encoder.encodeString(value.toString())

        override fun deserialize(decoder: Decoder): AbsolutePath =
            AbsolutePath(decoder.decodeString())
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy