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

commonMain.com.copperleaf.ballast.navigation.internal.RouteParser.kt Maven / Gradle / Ivy

There is a newer version: 4.2.1
Show newest version
package com.copperleaf.ballast.navigation.internal

import com.copperleaf.ballast.navigation.routing.PathSegment
import com.copperleaf.ballast.navigation.routing.QueryParameter
import com.copperleaf.ballast.navigation.routing.RouteMatcher
import com.copperleaf.kudzu.node.mapped.ValueNode
import com.copperleaf.kudzu.parser.Parser
import com.copperleaf.kudzu.parser.ParserContext
import com.copperleaf.kudzu.parser.ParserResult
import com.copperleaf.kudzu.parser.chars.CharInParser
import com.copperleaf.kudzu.parser.mapped.MappedParser
import com.copperleaf.kudzu.parser.maybe.MaybeParser
import com.copperleaf.kudzu.parser.sequence.SequenceParser
import kotlin.math.pow

internal object RouteParser {

    private val routeParser: Parser, List>>> = MappedParser(
        SequenceParser(
            PathParser.pathParser,
            MaybeParser(
                SequenceParser(
                    CharInParser('?'),
                    QueryStringParser.queryStringParser,
                )
            ),
        )
    ) { (_, path, query) ->
        val pathParameters: List = path.value
        val queryParameters: List = query.node?.node2?.value ?: emptyList()

        pathParameters to queryParameters
    }

    internal fun parseRoute(
        routeFormat: String,
        computeWeight: (List, List) -> Double,
    ): RouteMatcher {
        val parseResult = routeParser.parse(ParserContext.fromString(routeFormat))
        validateCorrectFormat(routeFormat, parseResult)

        val (pathParameters, queryParameters) = parseResult.first.value
        validatePathParameters(pathParameters)
        validateQueryParameters(queryParameters)
        validateAggregatedParameters(pathParameters, queryParameters)

        return RouteMatcherImpl(
            routeFormat = routeFormat,
            path = pathParameters,
            query = queryParameters,
            weight = computeWeight(pathParameters, queryParameters),
        )
    }

// Helpers
// ---------------------------------------------------------------------------------------------------------------------

    internal fun computeWeight(pathSegments: List, queryParameters: List): Double {

        // we require 2 more query parameters than the number of path segments for query parameters to be considered more
        // specific than the path
        val pathPowerModifier = queryParameters.size + 1

        // path has a greater weight than the query, so raise its power by the number of query paramers to ensure it
        // always matches paths before query parameters
        val pathWeight = pathSegments.reversed().foldRightIndexed(0.0) { index, next, acc ->
            acc + (next.weight * (10.0.pow(index + pathPowerModifier)))
        }

        val queryWeight = queryParameters.reversed().foldRightIndexed(0.0) { index, next, acc ->
            acc + (next.weight * (10.0.pow(index)))
        }

        return pathWeight + queryWeight
    }

    private fun validateCorrectFormat(route: String, parserResult: ParserResult<*>) {
        check(parserResult.second.isEmpty()) {
            "'$route' is not a valid route format"
        }
    }

    private fun validatePathParameters(pathSegments: List) {
        if (pathSegments.any { it.mustBeAtEnd }) {
            // if we have any optional parameters or tailcards, they must be at the end of the path and be the only one of
            // its kind

            if (pathSegments.count { it.mustBeAtEnd } > 1) {
                error("you can only have one optional parameter or tailcard, but not both")
            }
            if (!pathSegments.last().mustBeAtEnd) {
                error("optional parameters and tailcards must be at the end of the path")
            }
        }
    }

    private fun validateQueryParameters(queryParameters: List) {
        if (queryParameters.any { it.mustBeAtEnd }) {
            // if we have any remainder parameters, it must be at the end of the query string and be unique

            if (queryParameters.count { it.mustBeAtEnd } > 1) {
                error("you can only have one remainder query parameter")
            }
            if (!queryParameters.last().mustBeAtEnd) {
                error("remainder query parameter must be at the end")
            }
        }
    }

    private fun validateAggregatedParameters(pathSegments: List, queryParameters: List) {
        val paramNames = pathSegments.mapNotNull { it.paramName } + queryParameters.mapNotNull { it.paramName }
        val distinctParamNames = paramNames.distinct()
        if (distinctParamNames.size != paramNames.size) {
            error("parameter names must be unique")
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy