Skip to content

Add named path parameters parsing (Implements #1587) #1608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jul 5, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,20 @@ int main(void)
res.set_content("Hello World!", "text/plain");
});

// Match the request path against a regular expression
// and extract its captures
svr.Get(R"(/numbers/(\d+))", [&](const Request& req, Response& res) {
auto numbers = req.matches[1];
res.set_content(numbers, "text/plain");
});

// Capture the second segment of the request path as "id" path param
svr.Get("/users/:id", [&](const Request& req, Response& res) {
auto user_id = req.path_params.at("id");
res.set_content(user_id, "text/plain");
});

// Extract values from HTTP headers and URL query params
svr.Get("/body-header-param", [](const Request& req, Response& res) {
if (req.has_header("Content-Length")) {
auto val = req.get_header_value("Content-Length");
Expand Down
212 changes: 196 additions & 16 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ using socket_t = int;
#include <string>
#include <sys/stat.h>
#include <thread>
#include <unordered_map>
#include <unordered_set>
#include <utility>

#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
Expand Down Expand Up @@ -472,6 +474,7 @@ struct Request {
MultipartFormDataMap files;
Ranges ranges;
Match matches;
std::unordered_map<std::string, std::string> path_params;

// for client
ResponseHandler response_handler;
Expand Down Expand Up @@ -665,6 +668,76 @@ using SocketOptions = std::function<void(socket_t sock)>;

void default_socket_options(socket_t sock);

namespace detail {

class MatcherBase {
public:
virtual ~MatcherBase() = default;

// Match request path and populate its matches and
virtual bool match(Request &request) const = 0;
};

/**
* Captures parameters in request path and stores them in Request::path_params
*
* Capture name is a substring of a pattern from : to /.
* The rest of the pattern is matched agains the request path directly
* Parameters are captured starting from the next character after
* the end of the last matched static pattern fragment until the next /.
*
* Example pattern:
* "/path/fragments/:capture/more/fragments/:second_capture"
* Static fragments:
* "/path/fragments/", "more/fragments/"
*
* Given the following request path:
* "/path/fragments/:1/more/fragments/:2"
* the resulting capture will be
* {{"capture", "1"}, {"second_capture", "2"}}
*/
class PathParamsMatcher : public MatcherBase {
public:
PathParamsMatcher(const std::string &pattern);

bool match(Request &request) const override;

private:
static constexpr char marker = ':';
// Treat segment separators as the end of path parameter capture
// Does not need to handle query parameters as they are parsed before path
// matching
static constexpr char separator = '/';

// Contains static path fragments to match against, excluding the '/' after
// path params
// Fragments are separated by path params
std::vector<std::string> static_fragments_;
// Stores the names of the path parameters to be used as keys in the
// Request::path_params map
std::vector<std::string> param_names_;
};

/**
* Performs std::regex_match on request path
* and stores the result in Request::matches
*
* Note that regex match is performed directly on the whole request.
* This means that wildcard patterns may match multiple path segments with /:
* "/begin/(.*)/end" will match both "/begin/middle/end" and "/begin/1/2/end".
*/
class RegexMatcher : public MatcherBase {
public:
RegexMatcher(const std::string &pattern) : regex_(pattern) {}

bool match(Request &request) const override;

private:
std::regex regex_;
};

} // namespace detail

class Server {
public:
using Handler = std::function<void(const Request &, Response &)>;
Expand Down Expand Up @@ -772,9 +845,14 @@ class Server {
size_t payload_max_length_ = CPPHTTPLIB_PAYLOAD_MAX_LENGTH;

private:
using Handlers = std::vector<std::pair<std::regex, Handler>>;
using Handlers =
std::vector<std::pair<std::unique_ptr<detail::MatcherBase>, Handler>>;
using HandlersForContentReader =
std::vector<std::pair<std::regex, HandlerWithContentReader>>;
std::vector<std::pair<std::unique_ptr<detail::MatcherBase>,
HandlerWithContentReader>>;

static std::unique_ptr<detail::MatcherBase>
make_matcher(const std::string &pattern);

socket_t create_server_socket(const std::string &host, int port,
int socket_flags,
Expand Down Expand Up @@ -5143,6 +5221,99 @@ inline socket_t BufferStream::socket() const { return 0; }

inline const std::string &BufferStream::get_buffer() const { return buffer; }

inline PathParamsMatcher::PathParamsMatcher(const std::string &pattern) {
// One past the last ending position of a path param substring
std::size_t last_param_end = 0;

#ifndef CPPHTTPLIB_NO_EXCEPTIONS
// Needed to ensure that parameter names are unique during matcher
// construction
// If exceptions are disabled, only last duplicate path
// parameter will be set
std::unordered_set<std::string> param_name_set;
#endif

while (true) {
const auto marker_pos = pattern.find(marker, last_param_end);
if (marker_pos == std::string::npos) { break; }

static_fragments_.push_back(
pattern.substr(last_param_end, marker_pos - last_param_end));

const auto param_name_start = marker_pos + 1;

auto sep_pos = pattern.find(separator, param_name_start);
if (sep_pos == std::string::npos) { sep_pos = pattern.length(); }

auto param_name =
pattern.substr(param_name_start, sep_pos - param_name_start);

#ifndef CPPHTTPLIB_NO_EXCEPTIONS
if (param_name_set.find(param_name) != param_name_set.cend()) {
std::string msg = "Encountered path parameter '" + param_name +
"' multiple times in route pattern '" + pattern + "'.";
throw std::invalid_argument(msg);
}
#endif

param_names_.push_back(std::move(param_name));

last_param_end = sep_pos + 1;
}

if (last_param_end < pattern.length()) {
static_fragments_.push_back(pattern.substr(last_param_end));
}
}

inline bool PathParamsMatcher::match(Request &request) const {
request.matches = {};
request.path_params.clear();
request.path_params.reserve(param_names_.size());

// One past the position at which the path matched the pattern last time
std::size_t starting_pos = 0;
for (size_t i = 0; i < static_fragments_.size(); ++i) {
const auto &fragment = static_fragments_[i];

if (starting_pos + fragment.length() > request.path.length()) {
return false;
}

// Avoid unnecessary allocation by using strncmp instead of substr +
// comparison
if (std::strncmp(request.path.c_str() + starting_pos, fragment.c_str(),
fragment.length()) != 0) {
return false;
}

starting_pos += fragment.length();

// Should only happen when we have a static fragment after a param
// Example: '/users/:id/subscriptions'
// The 'subscriptions' fragment here does not have a corresponding param
if (i >= param_names_.size()) { continue; }

auto sep_pos = request.path.find(separator, starting_pos);
if (sep_pos == std::string::npos) { sep_pos = request.path.length(); }

const auto &param_name = param_names_[i];

request.path_params.emplace(
param_name, request.path.substr(starting_pos, sep_pos - starting_pos));

// Mark everythin up to '/' as matched
starting_pos = sep_pos + 1;
}
// Returns false if the path is longer than the pattern
return starting_pos >= request.path.length();
}

inline bool RegexMatcher::match(Request &request) const {
request.path_params.clear();
return std::regex_match(request.path, request.matches, regex_);
}

} // namespace detail

// HTTP server implementation
Expand All @@ -5156,67 +5327,76 @@ inline Server::Server()

inline Server::~Server() {}

inline std::unique_ptr<detail::MatcherBase>
Server::make_matcher(const std::string &pattern) {
if (pattern.find("/:") != std::string::npos) {
return detail::make_unique<detail::PathParamsMatcher>(pattern);
} else {
return detail::make_unique<detail::RegexMatcher>(pattern);
}
}

inline Server &Server::Get(const std::string &pattern, Handler handler) {
get_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Post(const std::string &pattern, Handler handler) {
post_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Post(const std::string &pattern,
HandlerWithContentReader handler) {
post_handlers_for_content_reader_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Put(const std::string &pattern, Handler handler) {
put_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Put(const std::string &pattern,
HandlerWithContentReader handler) {
put_handlers_for_content_reader_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Patch(const std::string &pattern, Handler handler) {
patch_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Patch(const std::string &pattern,
HandlerWithContentReader handler) {
patch_handlers_for_content_reader_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Delete(const std::string &pattern, Handler handler) {
delete_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Delete(const std::string &pattern,
HandlerWithContentReader handler) {
delete_handlers_for_content_reader_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

inline Server &Server::Options(const std::string &pattern, Handler handler) {
options_handlers_.push_back(
std::make_pair(std::regex(pattern), std::move(handler)));
std::make_pair(make_matcher(pattern), std::move(handler)));
return *this;
}

Expand Down Expand Up @@ -5926,10 +6106,10 @@ inline bool Server::routing(Request &req, Response &res, Stream &strm) {
inline bool Server::dispatch_request(Request &req, Response &res,
const Handlers &handlers) {
for (const auto &x : handlers) {
const auto &pattern = x.first;
const auto &matcher = x.first;
const auto &handler = x.second;

if (std::regex_match(req.path, req.matches, pattern)) {
if (matcher->match(req)) {
handler(req, res);
return true;
}
Expand Down Expand Up @@ -6051,10 +6231,10 @@ inline bool Server::dispatch_request_for_content_reader(
Request &req, Response &res, ContentReader content_reader,
const HandlersForContentReader &handlers) {
for (const auto &x : handlers) {
const auto &pattern = x.first;
const auto &matcher = x.first;
const auto &handler = x.second;

if (std::regex_match(req.path, req.matches, pattern)) {
if (matcher->match(req)) {
handler(req, res, content_reader);
return true;
}
Expand Down
Loading