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

commonMain.Path.kt Maven / Gradle / Ivy

There is a newer version: 0.23.0
Show newest version
@file:JvmName("Paths")

package io.kform

import kotlin.js.JsName
import kotlin.jvm.JvmField
import kotlin.jvm.JvmName
import kotlin.jvm.JvmStatic
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

/**
 * Typealias used to represent a type where either a [Path] or a [String] (representing a path) are
 * accepted.
 */
public typealias PathOrString = Any

/**
 * Representation of a path as a list of path [fragments].
 *
 * Paths represent locations of data, possibly relatively to other locations: e.g. the location of a
 * value relative to the location of another value. They can also represent locations of data from
 * the root when they contain a root fragment.
 */
@JsName("PathKt")
@Serializable(with = Path.Serializer::class)
public open class Path(fragments: List = emptyList()) {
    /**
     * Internal list of "real" fragments. For the generic [Path], this list is the same as the
     * public [fragments] one, however, [AbsolutePath] hides their internal representation (it hides
     * its first "root fragment") and the two lists differ.
     */
    internal val realFragments: List = ArrayList(fragments)

    /** List of fragments representing this path. */
    public open val fragments: List
        get() = realFragments

    /** Creates a path from its string notation. E.g. `"./path/to/somewhere"`. */
    public constructor(stringPath: String) : this(parse(stringPath))

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

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

    /**
     * Returns a new path representing the parent of this path.
     *
     * Note that the parent of the root path is the root path itself.
     */
    public open fun parent(): Path = Path(parentImpl(realFragments))

    /** Returns the path resulting from appending [fragments] to this path. */
    public fun append(vararg fragments: PathFragment): Path = Path(realFragments + fragments)

    /**
     * Returns the path resulting from appending [fragment] to this path. Equivalent to
     * `append(fragment)`.
     */
    public operator fun plus(fragment: PathFragment): Path = append(fragment)

    /** Returns the path resulting from joining [paths] together with this path. */
    public fun join(vararg paths: Path): Path =
        Path(joinImpl(realFragments, *paths.map { path -> path.realFragments }.toTypedArray()))

    /**
     * Returns the path resulting from joining the provided paths in string notation [stringPaths]
     * together with this path.
     */
    public fun join(vararg stringPaths: String): Path =
        join(*stringPaths.map { str -> Path(str) }.toTypedArray())

    /**
     * Returns the path resulting from joining [path] together with this path. Equivalent to
     * `join(path)`.
     */
    public open operator fun plus(path: Path): Path = join(path)

    /**
     * Returns the path resulting from joining the path in string notation [stringPath] together
     * with this path. Equivalent to `join(stringPath)`.
     */
    public open operator fun plus(stringPath: String): Path = join(stringPath)

    /**
     * Returns the result of resolving this path. Resolving the path removes unnecessary fragments
     * such as the [current path fragment][PathFragment.CurrentPath].
     */
    public fun resolve(): Path = Path(resolveImpl(realFragments))

    /** Returns the path resulting from resolving a list of [paths] against this path. */
    public open fun resolve(vararg paths: Path): Path =
        Path(resolveImpl(realFragments, *paths.map { path -> path.realFragments }.toTypedArray()))

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

    /**
     * Returns whether this path equals [other].
     *
     * Two paths are equal when they both resolve to paths with the exact same fragments.
     */
    override fun equals(other: Any?): Boolean =
        when {
            this === other -> true
            other !is Path -> false
            else -> resolve().realFragments == other.resolve().realFragments
        }

    /** Returns the hash code for this path. */
    override fun hashCode(): Int = resolve().realFragments.hashCode()

    /** Returns the path in string notation. */
    override fun toString(): String = buildString {
        for ((i, fragment) in realFragments.withIndex()) {
            if (fragment is PathFragment.Root) {
                clear()
                append(SEPARATOR_CHARACTER)
            } else {
                append(fragmentToString(fragment))
                // Add a trailing slash only when the last fragment is an empty id
                if (
                    i < realFragments.size - 1 ||
                        (fragment is AbsolutePathFragment.Id && fragment.id == "")
                ) {
                    append(SEPARATOR_CHARACTER)
                }
            }
        }
    }

    /** Returns an iterator over the path's fragments. */
    public open operator fun iterator(): Iterator = realFragments.iterator()

    public companion object {
        // Common paths:
        /** Path representing the current path (`"."`). */
        @JvmField public val CURRENT: Path = Path()

        /** Path representing the current path and all of its descendants (`"./∗∗"`). */
        @JvmField
        public val CURRENT_DEEP: Path = Path(listOf(AbsolutePathFragment.RecursiveWildcard))

        /** Path representing the parent path (`".."`). */
        @JvmField public val PARENT: Path = Path(listOf(PathFragment.ParentPath))

        /** Path representing the children of a path (`"./∗"`). */
        @JvmField public val CHILDREN: Path = Path(listOf(AbsolutePathFragment.Wildcard))

        /** Path representing the descendants of a path (`"./∗/∗∗"`). */
        @JvmField
        public val DESCENDANTS: Path =
            Path(listOf(AbsolutePathFragment.Wildcard, AbsolutePathFragment.RecursiveWildcard))

        // Strings/characters to parse paths from strings:
        /**
         * Special fragment string representing the end of a collection. Use `"~-"` within a string
         * representation of a path to represent the fragment with id `"-"` literally.
         */
        public const val COLLECTION_END_STRING: String = "-"

        /**
         * Special fragment string representing "any" fragment. Use `"~*"` within a string
         * representation of a path to represent the fragment with id `"*"` literally.
         */
        public const val WILDCARD_STRING: String = "*"

        /**
         * Special fragment string representing zero or more fragments. Use `"~**"` within a string
         * representation of a path to represent the fragment with id `"**"` literally.
         */
        public const val RECURSIVE_WILDCARD_STRING: String = "**"

        /**
         * Special fragment string representing the "current" path. Use `"~."` within a string
         * representation of a path to represent the fragment with id `"."` literally.
         */
        public const val CURRENT_PATH_STRING: String = "."

        /**
         * Special fragment string representing the "parent" path. Use `"~.."` within a string
         * representation of a path to represent the fragment with id `".."` literally.
         */
        public const val PARENT_PATH_STRING: String = ".."

        /**
         * Character used as part of an escape sequence in the string representation of paths. It
         * may be used to escape the special fragments `"."`, `".."`, `"*"`, and `"**"` with `"~."`,
         * `"~.."`, `"~*"`, and `"~**"` respectively, as well as escape characters such as `"/"`,
         * `"~"`, `"{"`, `"="`, `";"`, and `"}"`.
         */
        public const val ESCAPE_CHARACTER: Char = '~'

        /**
         * Character used to represent the root path in string form, as well as separate fragments,
         * e.g. the path with id fragments `x` and `y` is represented in string form as `"/x/y"`
         * whilst the one with no fragments (root) is represented as `"/"`.
         */
        public const val SEPARATOR_CHARACTER: Char = '/'

        // Regular expressions used to escape fragments when converting a path to
        // its string form
        private val FRAGMENT_ESCAPE_REGEX = Regex("([~/])")

        /**
         * Returns [fragment] in string notation (e.g. to be used in the string representation of a
         * path). Id fragments may end up escaped when they would otherwise represent a different
         * fragment.
         */
        @JvmStatic
        public fun fragmentToString(fragment: PathFragment): String {
            return when (fragment) {
                is AbsolutePathFragment.Id -> {
                    val id = fragment.id
                    // Escape fragments that would have special meanings
                    if (
                        id == COLLECTION_END_STRING ||
                            id == WILDCARD_STRING ||
                            id == RECURSIVE_WILDCARD_STRING ||
                            id == CURRENT_PATH_STRING ||
                            id == PARENT_PATH_STRING
                    ) {
                        return ESCAPE_CHARACTER + id
                    }
                    // Escape the escape character and the path separator
                    return id.replace(FRAGMENT_ESCAPE_REGEX, "$ESCAPE_CHARACTER$1")
                }
                is AbsolutePathFragment.CollectionEnd -> COLLECTION_END_STRING
                is AbsolutePathFragment.Wildcard -> WILDCARD_STRING
                is AbsolutePathFragment.RecursiveWildcard -> RECURSIVE_WILDCARD_STRING
                is PathFragment.Root -> SEPARATOR_CHARACTER.toString()
                is PathFragment.CurrentPath -> CURRENT_PATH_STRING
                is PathFragment.ParentPath -> PARENT_PATH_STRING
            }
        }

        /**
         * Returns the list of path fragments represented by a path in string notation [stringPath].
         */
        private fun parse(stringPath: String): List {
            val fragmentList: MutableList = mutableListOf()
            var fragmentId = ""
            var i = 0
            val l = stringPath.length
            mainLoop@ while (i < l) {
                val c = stringPath[i]
                // Unescape
                if (c == ESCAPE_CHARACTER) {
                    fragmentId += stringPath.getOrNull(++i)?.toString() ?: ""
                    ++i
                    continue
                }
                // Handle path separator
                if (c == SEPARATOR_CHARACTER) {
                    // Represent an absolute path by setting the first fragment as the
                    // root
                    if (i == 0) {
                        fragmentList += PathFragment.Root
                    } else {
                        fragmentList += AbsolutePathFragment.Id(fragmentId)
                        fragmentId = ""
                    }
                    ++i
                    continue
                }
                if (fragmentId == "") {
                    // Handle special fragments
                    for (spFragment in
                        listOf(
                            PathFragment.CurrentPath,
                            PathFragment.ParentPath,
                            AbsolutePathFragment.CollectionEnd,
                            AbsolutePathFragment.Wildcard,
                            AbsolutePathFragment.RecursiveWildcard
                        )) {
                        val spStr = fragmentToString(spFragment)
                        val spLen = spStr.length
                        if (
                            stringPath.substring(i, min(i + spLen, l)) == spStr &&
                                (i + spLen == l ||
                                    stringPath.getOrNull(i + spLen) == SEPARATOR_CHARACTER)
                        ) {
                            fragmentList += spFragment
                            i += spLen + 1
                            continue@mainLoop
                        }
                    }
                }
                fragmentId += c
                ++i
            }
            // Ignore trailing slash
            if (fragmentId != "") {
                fragmentList += AbsolutePathFragment.Id(fragmentId)
            }
            return fragmentList
        }

        /**
         * Implementation of [parent]. Returns the parent of the path represented by the provided
         * path [fragments].
         */
        internal fun parentImpl(fragments: List): List =
            when {
                fragments.isEmpty() || fragments.last() == PathFragment.ParentPath ->
                    fragments + PathFragment.ParentPath
                fragments.last() == PathFragment.Root -> fragments
                else -> fragments.dropLast(1)
            }

        /**
         * Implementation of [join]. Joins all lists of path fragments within an array
         * [fragmentsArray] into a single list of path fragments.
         */
        internal fun joinImpl(vararg fragmentsArray: List): List {
            val joined = mutableListOf()
            for (fragments in fragmentsArray) {
                joined += fragments
            }
            return joined
        }

        /**
         * Implementation of [resolve]. Resolves all provided lists of path fragments within an
         * array [fragmentsArray] into a single list of path fragments, removing unnecessary
         * fragments.
         */
        internal fun resolveImpl(vararg fragmentsArray: List): List {
            val resolved = mutableListOf()
            for (fragments in fragmentsArray) {
                for (fragment in fragments) {
                    when (fragment) {
                        is PathFragment.Root -> {
                            resolved.clear()
                            resolved += fragment
                        }
                        is PathFragment.ParentPath ->
                            when {
                                resolved.isEmpty() || resolved.last() == PathFragment.ParentPath ->
                                    resolved += fragment
                                resolved.last() != PathFragment.Root ->
                                    resolved.removeAt(resolved.lastIndex)
                            }
                        is PathFragment.CurrentPath -> {
                            /* Always unnecessary. */
                        }
                        is AbsolutePathFragment.RecursiveWildcard -> {
                            // Consecutive recursive wildcards are redundant and may hurt
                            // performance of matching algorithms
                            if (resolved.last() != fragment) {
                                resolved += fragment
                            }
                        }
                        else -> resolved += fragment
                    }
                }
            }
            return resolved
        }
    }

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

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

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

/** Converts the receiver path into an absolute path, if it wasn't one already. */
public fun Path.toAbsolutePath(): AbsolutePath =
    if (this is AbsolutePath) this else AbsolutePath(this)

/**
 * Converts the receiver [Path] or [String] into a path, if it wasn't one already.
 *
 * @throws IllegalArgumentException if the receiver is neither a [Path] nor a [String].
 */
public fun PathOrString.toPath(): Path =
    when (this) {
        is Path -> this
        is String -> Path(this)
        else -> throw IllegalArgumentException("The receiver must be either a path or a string.")
    }

/**
 * Converts the receiver [Path] or [String] into an absolute path, if it wasn't one already.
 *
 * @throws IllegalArgumentException if the receiver is neither a [Path] nor a [String].
 */
public fun PathOrString.toAbsolutePath(): AbsolutePath =
    when (this) {
        is Path -> this.toAbsolutePath()
        is String -> AbsolutePath(this)
        else -> throw IllegalArgumentException("The receiver must be either a path or a string.")
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy