#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);
}