BareGit
#include "url_builder.h"

#include <cctype>
#include <stdexcept>
#include <string_view>

namespace
{

void validateOrdinarySegment(std::string_view value)
{
    if(value.empty() || value == "." || value == "..")
    {
        throw std::invalid_argument("Invalid empty or dot path segment");
    }
}

void validateLiteral(std::string_view value)
{
    validateOrdinarySegment(value);
    if(value.contains('/') || value.contains('\\'))
    {
        throw std::invalid_argument("Literal route segment contains a slash");
    }
}

void validatePlaceholder(std::string_view value)
{
    if(value.empty() ||
       !(std::islower(static_cast<unsigned char>(value.front())) ||
         value.front() == '_'))
    {
        throw std::invalid_argument("Invalid route placeholder name");
    }

    for(char character : value.substr(1))
    {
        const auto byte = static_cast<unsigned char>(character);
        if(!(std::islower(byte) || std::isdigit(byte) || character == '_'))
        {
            throw std::invalid_argument("Invalid route placeholder name");
        }
    }
}

std::string segmentText(const RouteSegment& segment, bool allow_placeholder)
{
    switch(segment.kind)
    {
    case RouteSegmentKind::LITERAL:
        validateLiteral(segment.value);
        return segment.value;
    case RouteSegmentKind::DYNAMIC:
        validateOrdinarySegment(segment.value);
        return mw::URL::encode(segment.value);
    case RouteSegmentKind::PLACEHOLDER:
        if(!allow_placeholder)
        {
            throw std::invalid_argument(
                "Route placeholder is invalid in an absolute URL");
        }
        validatePlaceholder(segment.value);
        return ":" + segment.value;
    }

    throw std::invalid_argument("Unknown route segment kind");
}

std::string buildPath(
    const mw::URL& base_url,
    const std::vector<RouteSegment>& segments,
    bool allow_placeholder)
{
    std::string path = base_url.path();
    if(path.empty())
    {
        path = "/";
    }

    if(segments.empty())
    {
        return path;
    }

    if(path.back() != '/')
    {
        path += '/';
    }

    for(std::size_t index = 0; index < segments.size(); ++index)
    {
        if(index != 0)
        {
            path += '/';
        }
        path += segmentText(segments[index], allow_placeholder);
    }

    return path;
}

std::string buildQuery(const QueryParameters& query)
{
    std::string result;
    for(std::size_t index = 0; index < query.size(); ++index)
    {
        if(index != 0)
        {
            result += '&';
        }
        result += mw::URL::encode(query[index].first);
        result += '=';
        result += mw::URL::encode(query[index].second);
    }
    return result;
}

std::vector<RouteSegment> appendRelativePath(
    std::vector<RouteSegment> prefix,
    const std::string& relative_path)
{
    if(relative_path.empty() || relative_path.front() == '/' ||
       relative_path.front() == '\\')
    {
        throw std::invalid_argument("Invalid relative static path");
    }

    std::size_t begin = 0;
    while(begin <= relative_path.size())
    {
        const std::size_t end = relative_path.find('/', begin);
        const std::string component = relative_path.substr(begin, end - begin);
        validateOrdinarySegment(component);
        prefix.push_back({RouteSegmentKind::DYNAMIC, component});

        if(end == std::string::npos)
        {
            break;
        }
        begin = end + 1;
    }

    return prefix;
}

} // namespace

UrlBuilder::UrlBuilder(mw::URL base_url)
        : base_url_(std::move(base_url))
{}

std::string UrlBuilder::absolute(
    const std::vector<RouteSegment>& segments,
    const QueryParameters& query) const
{
    mw::URL result = base_url_;
    result.path(buildPath(base_url_, segments, false).c_str());

    if(query.empty())
    {
        result.query(nullptr);
    }
    else
    {
        result.query(buildQuery(query).c_str());
    }
    return result.str();
}

std::string UrlBuilder::absoluteFromRelativePath(
    const std::vector<RouteSegment>& mount_prefix,
    const std::string& relative_path,
    const QueryParameters& query) const
{
    return absolute(appendRelativePath(mount_prefix, relative_path), query);
}

std::string UrlBuilder::requestPath(
    const std::vector<RouteSegment>& segments) const
{
    return buildPath(base_url_, segments, true);
}