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

* Add named path parameters parsing

* Select match mode based on pattern

* Add examples and comments to README

* Add documentation to matchers
This commit is contained in:
bgs99 2023-07-05 14:44:19 +03:00 committed by GitHub
parent f1daa5b88b
commit 17fc522b75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 357 additions and 16 deletions

212
httplib.h
View file

@ -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
@ -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;
@ -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 &)>;
@ -772,9 +845,14 @@ protected:
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,
@ -5147,6 +5225,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
@ -5160,67 +5331,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;
}
@ -5930,10 +6110,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;
}
@ -6055,10 +6235,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;
}