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

com.lightningkite.lightningserver.http.HttpEndpointMatcher.kt Maven / Gradle / Ivy

The newest version!
package com.lightningkite.lightningserver.http

import com.lightningkite.lightningserver.core.ServerPath
import io.ktor.http.*

class HttpEndpointMatcher(paths: Sequence) {
    data class Node(
        val path: Map,
        val trailingSlash: Map,
        val chainedWildcard: Map,
        val thenConstant: Map,
        val thenWildcard: Node?,
    ) {
        override fun toString(): String {
            return """Node(
                path = $path, 
                trailingSlash = $trailingSlash, 
                chainedWildcard = $chainedWildcard, 
                thenConstant = ${thenConstant.keys.joinToString()}, 
                thenWildcard = ${thenWildcard != null}, 
            )""".trimIndent().replace("\n", "")
        }
    }

    val root = run {
        fun ServerPath.Segment.s(): String? = when (this) {
            is ServerPath.Segment.Constant -> value
            is ServerPath.Segment.Wildcard -> null
        }

        fun toNode(soFar: List): Node {
            val future = paths.map { it.path }.filter {
                it.segments.asSequence().zip(soFar.asSequence())
                    .all { it.first.s() == it.second } && it.segments.size > soFar.size
            }
            return Node(
                path = paths.filter { it.path.segments.map { it.s() } == soFar && it.path.after == ServerPath.Afterwards.None }
                    .associateBy { it.method },
                trailingSlash = paths.filter { it.path.segments.map { it.s() } == soFar && it.path.after == ServerPath.Afterwards.TrailingSlash }
                    .associateBy { it.method },
                chainedWildcard = paths.filter { it.path.segments.map { it.s() } == soFar && it.path.after == ServerPath.Afterwards.ChainedWildcard }
                    .associateBy { it.method },
                thenConstant = future
                    .mapNotNull { (it.segments[soFar.size] as? ServerPath.Segment.Constant)?.value }
                    .distinct()
                    .associateWith { toNode(soFar + it) },
                thenWildcard = if (future.any { it.segments[soFar.size] is ServerPath.Segment.Wildcard })
                    toNode(soFar + null)
                else null
            )
        }
        toNode(listOf())
    }

    data class Match(
        val endpoint: HttpEndpoint,
        val parts: Map,
        val wildcard: String?
    )

    fun match(string: String, method: HttpMethod): Match? =
        match(string.split('/').filter { it.isNotEmpty() }, string.endsWith('/'), method)

    fun match(pathParts: List, endingSlash: Boolean, method: HttpMethod): Match? {
        if (pathParts.isEmpty())
            return (root.path[method] ?: root.trailingSlash[method] ?: root.chainedWildcard[method])?.let {
                Match(
                    it,
                    mapOf(),
                    if (it.path.after == ServerPath.Afterwards.ChainedWildcard) "" else null
                )
            }
//        println("Navigating $pathParts with ending slash $endingSlash")
        val wildcards = ArrayList()
        var current = root
        val soFar = arrayListOf()
        var beyond = false
        for (part in pathParts) {
            soFar.add(current)
//            println("Current is $current, looking for $part")
            val c = current.thenConstant[part]
            if (c != null) {
                current = c
                continue
            }
            val w = current.thenWildcard
            if (w != null) {
                current = w
                wildcards.add(part)
                continue
            }
            beyond = true
            break
        }
//        println("Stopped at $current")
        return if (beyond) {
//            println("Searching for wildcard ending")
            soFar.asReversed().asSequence().mapNotNull {
                it.chainedWildcard[method]?.let {
                    Match(
                        endpoint = it,
                        parts = it.path.segments.filterIsInstance().zip(wildcards)
                            .associate { it.first.name to it.second.decodeURLPart() },
                        wildcard = pathParts.drop(it.path.segments.size)
                            .joinToString("/") + (if (endingSlash) "/" else "")
                    )
                }
            }.firstOrNull()
        } else if (endingSlash) {
//            println("Pulling trailingSlash")
            current.trailingSlash[method]?.let {
                Match(
                    endpoint = it,
                    parts = it.path.segments.filterIsInstance().zip(wildcards)
                        .associate { it.first.name to it.second.decodeURLPart() },
                    wildcard = null
                )
            }
        } else {
//            println("Pulling path")
            current.path[method]?.let {
                Match(
                    endpoint = it,
                    parts = it.path.segments.filterIsInstance().zip(wildcards)
                        .associate { it.first.name to it.second.decodeURLPart() },
                    wildcard = null
                )
            }
        } ?: run {
//            println("Searching for wildcard ending")
            soFar.asReversed().asSequence().mapNotNull {
                it.chainedWildcard[method]?.let {
                    Match(
                        endpoint = it,
                        parts = it.path.segments.filterIsInstance().zip(wildcards)
                            .associate { it.first.name to it.second.decodeURLPart() },
                        wildcard = pathParts.drop(it.path.segments.size)
                            .joinToString("/") + (if (endingSlash) "/" else "")
                    )
                }
            }.firstOrNull()
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy