Merge branch 'feature/virtual-hosts'

This commit is contained in:
R.Y.A.N 2025-03-09 15:56:18 +01:00
commit 53d43dfea6
Signed by: R.Y.A.N
GPG Key ID: 3BD93EABD1407B82
23 changed files with 591 additions and 290 deletions

View File

@ -0,0 +1,43 @@
#include <seafire/routing/builder.hxx>
#include <seafire/routing/flatten.hxx>
#include <vector>
namespace seafire::routing
{
builder_t::
builder_t()
{}
std::list<virtual_host_t> const&
builder_t::
virtual_hosts() const
{
return _vhosts;
}
virtual_host_t&
builder_t::
add_virtual_host(std::string vhost)
{
_vhosts.emplace_back(std::move(vhost));
return _vhosts.back();
}
routing_table_t
builder_t::
build() const
{
std::vector<endpoint_t> endpoints;
for (auto const& vhost : _vhosts) {
for (auto const& r : vhost.routes()) {
flatten(endpoints, vhost.host(), vhost.middleware(), r);
}
}
return routing_table_t{std::move(endpoints)};
}
} // namespace seafire::routing

View File

@ -0,0 +1,42 @@
#ifndef seafire__routing__builder_hxx_
#define seafire__routing__builder_hxx_
#include <seafire/routing/routing-table.hxx>
#include <seafire/routing/virtual-host.hxx>
#include <seafire/server/request-handler.hxx>
#include <list>
#include <string>
namespace seafire::routing
{
class builder_t
{
public:
builder_t();
builder_t(builder_t const&) = delete;
builder_t(builder_t&&) = delete;
std::list<virtual_host_t> const&
virtual_hosts() const;
virtual_host_t&
add_virtual_host(std::string);
routing_table_t
build() const;
builder_t& operator=(builder_t const&) = delete;
builder_t& operator=(builder_t&&) = delete;
private:
std::list<virtual_host_t> _vhosts;
};
} // namespace seafire::routing
#endif

View File

@ -4,15 +4,26 @@ namespace seafire::routing
{ {
endpoint_t:: endpoint_t::
endpoint_t(std::string pattern, server::request_handler_t handler) endpoint_t(std::string host,
: pattern_{std::move(pattern)}, handler_{std::move(handler)} std::string path,
server::request_handler_t handler)
: host_{std::move(host)},
path_{std::move(path)},
handler_{std::move(handler)}
{} {}
std::string const& std::string const&
endpoint_t:: endpoint_t::
pattern() const host() const
{ {
return pattern_; return host_;
}
std::string const&
endpoint_t::
path() const
{
return path_;
} }
server::request_handler_t const& server::request_handler_t const&
@ -25,7 +36,7 @@ namespace seafire::routing
std::ostream& std::ostream&
to_stream(std::ostream& o, endpoint_t const& ep) to_stream(std::ostream& o, endpoint_t const& ep)
{ {
return o << ep.pattern(); return o << ep.host() << ": " << ep.path();
} }
std::ostream& std::ostream&

View File

@ -12,16 +12,20 @@ namespace seafire::routing
class endpoint_t class endpoint_t
{ {
public: public:
endpoint_t(std::string, server::request_handler_t); endpoint_t(std::string, std::string, server::request_handler_t);
std::string const& std::string const&
pattern() const; host() const;
std::string const&
path() const;
server::request_handler_t const& server::request_handler_t const&
handler() const; handler() const;
private: private:
std::string pattern_; std::string host_;
std::string path_;
server::request_handler_t handler_; server::request_handler_t handler_;
}; };

View File

@ -1,50 +0,0 @@
#include <seafire/routing/flatten-route.hxx>
namespace seafire::routing
{
void
flatten_route(std::vector<endpoint_t>& endpoints,
std::vector<server::middleware_t> middlewares,
route_t const& route,
std::string const& root)
{
auto const path = root + route.path();
// append any middlewares if we have any.
//
for (auto const& m : route.middleware()) {
middlewares.emplace_back(m);
}
// generate an endpoint for this route if we have a handler.
//
if (auto const& h = route.handler()) {
endpoints.emplace_back(path, server::make_middleware(middlewares, *h));
}
// flatten any child routes.
//
for (auto const& child_route : route.children()) {
flatten_route(endpoints,
middlewares,
child_route,
route.path().empty() ? path : path + '/');
}
}
void
flatten_route(std::vector<endpoint_t>& endpoints, route_t const& r)
{
flatten_route(endpoints, {}, r, "/");
}
std::vector<endpoint_t>
flatten_route(route_t const& r)
{
std::vector<endpoint_t> endpoints;
flatten_route(endpoints, r);
return endpoints;
}
} // namespace seafire::routing

View File

@ -1,28 +0,0 @@
#ifndef seafire__routing__flatten_route_hxx_
#define seafire__routing__flatten_route_hxx_
#include <seafire/routing/endpoint.hxx>
#include <seafire/routing/route.hxx>
#include <seafire/server/middleware.hxx>
#include <vector>
namespace seafire::routing
{
void
flatten_route(std::vector<endpoint_t>& endpoints,
std::vector<server::middleware_t>,
route_t const& r,
std::string const& root);
void
flatten_route(std::vector<endpoint_t>&, route_t const&);
std::vector<endpoint_t>
flatten_route(route_t const&);
} // namespace seafire::routing
#endif

View File

@ -0,0 +1,55 @@
#include <seafire/routing/flatten.hxx>
namespace seafire::routing
{
void
flatten(std::vector<endpoint_t>& endpoints,
std::string const& vhost,
std::vector<server::middleware_t> middlewares,
route_t const& route,
std::string const& root)
{
auto const path = root + route.path();
// append any middlewares if we have any.
//
for (auto const& m : route.middleware()) {
middlewares.emplace_back(m);
}
// generate an endpoint for this route if we have a handler.
//
if (auto const& h = route.handler()) {
endpoints.emplace_back(vhost, path, server::make_middleware(middlewares, *h));
}
// flatten any child routes.
//
for (auto const& child_route : route.children()) {
flatten(endpoints,
vhost,
middlewares,
child_route,
route.path().empty() ? path : path + '/');
}
}
void
flatten(std::vector<endpoint_t>& endpoints,
std::string const& vhost,
std::vector<server::middleware_t> middlewares,
route_t const& r)
{
flatten(endpoints, vhost, middlewares, r, "/");
}
std::vector<endpoint_t>
flatten(std::string const& vhost, route_t const& r)
{
std::vector<endpoint_t> endpoints;
flatten(endpoints, vhost, {}, r);
return endpoints;
}
} // namespace seafire::routing

View File

@ -0,0 +1,32 @@
#ifndef seafire__routing__flatten_route_hxx_
#define seafire__routing__flatten_route_hxx_
#include <seafire/routing/endpoint.hxx>
#include <seafire/routing/route.hxx>
#include <seafire/server/middleware.hxx>
#include <vector>
namespace seafire::routing
{
void
flatten(std::vector<endpoint_t>& endpoints,
std::string const&,
std::vector<server::middleware_t>,
route_t const& r,
std::string const& root);
void
flatten(std::vector<endpoint_t>&,
std::string const&,
std::vector<server::middleware_t>,
route_t const&);
std::vector<endpoint_t>
flatten(std::string const&, route_t const&);
} // namespace seafire::routing
#endif

View File

@ -0,0 +1,63 @@
#ifndef seafire__routing__host_parameter_hxx_
#define seafire__routing__host_parameter_hxx_
#include <seafire/server/parameters.hxx>
#include <seafire/server/request.hxx>
#include <seafire/routing/parameters.hxx>
#include <optional>
namespace seafire::routing
{
template<
server::parameter_name_t Name,
typename ParameterType = server::string_parameter_t
>
class host_parameter_t
: public server::named_parameter_t<Name>
{
public:
using parameter_type = ParameterType;
using value_type = typename parameter_type::value_type;
host_parameter_t(std::optional<value_type> value)
: _value{std::move(value)}
{}
using server::named_parameter_t<Name>::name;
std::optional<value_type> const&
value() const
{
return _value;
}
operator std::optional<value_type> const&() const
{
return value();
}
std::optional<value_type> const*
operator->() const
{
return &value();
}
static
host_parameter_t<Name, ParameterType>
fetch(server::request_t& req)
{
auto v = req.extensions().use<host_parameters_t>().get(name());
return parameter_type::try_parse(v);
}
private:
std::optional<value_type> _value;
};
} // namespace seafire::routing
#endif

View File

@ -1,24 +0,0 @@
#ifndef seafire__routing__match_path_hxx_
#define seafire__routing__match_path_hxx_
#include <seafire/routing/route-parameters.hxx>
#include <code/uri/uri.hxx>
#include <string>
namespace seafire::routing
{
bool
match_path(std::string const&,
std::string const&,
route_parameters_t&);
std::string
render_path(std::string const&,
route_parameters_t&);
} // namespace seafire::routing
#endif

View File

@ -1,18 +1,18 @@
#include <seafire/routing/match-path.hxx> #include <seafire/routing/match.hxx>
#include <sstream> #include <sstream>
#include <vector> #include <vector>
#include <iostream>
namespace seafire::routing namespace seafire::routing
{ {
bool bool
match_path(std::string const& pattern, match(std::string const& subject,
std::string const& subject, std::string const& pattern,
route_parameters_t& params) char delim,
parameters_t& params)
{ {
route_parameters_t tmp_params; parameters_t tmp_params;
// p/pend = pattern iterators. // p/pend = pattern iterators.
// //
@ -46,7 +46,7 @@ namespace seafire::routing
// v/vend = local subject iterators. // v/vend = local subject iterators.
// //
auto const v = s; auto const v = s;
while (s != send && (greedy || '/' != *s)) { while (s != send && (greedy || delim != *s)) {
++s; ++s;
} }
auto const vend = s; auto const vend = s;
@ -83,8 +83,7 @@ namespace seafire::routing
} }
std::string std::string
render_path(std::string const& pattern, render(std::string const& pattern, parameters_t& params)
route_parameters_t const& params)
{ {
std::stringstream str; std::stringstream str;

22
seafire/routing/match.hxx Normal file
View File

@ -0,0 +1,22 @@
#ifndef seafire__routing__match_hxx_
#define seafire__routing__match_hxx_
#include <seafire/routing/parameters.hxx>
#include <string>
namespace seafire::routing
{
bool
match(std::string const&,
std::string const&,
char,
parameters_t&);
std::string
render(std::string const&, parameters_t&);
} // namespace seafire::routing
#endif

View File

@ -1,24 +1,24 @@
#include <seafire/routing/route-parameters.hxx> #include <seafire/routing/parameters.hxx>
namespace seafire::routing namespace seafire::routing
{ {
std::map<std::string, std::string>& parameters_t::map_type&
route_parameters_t:: parameters_t::
map() map()
{ {
return kv_; return _values;
} }
std::map<std::string, std::string> const& parameters_t::map_type const&
route_parameters_t:: parameters_t::
map() const map() const
{ {
return kv_; return _values;
} }
std::optional<std::string> std::optional<std::string>
route_parameters_t:: parameters_t::
get(std::string const& key) const get(std::string const& key) const
{ {
if (auto it = map().find(key); it != map().end()) if (auto it = map().find(key); it != map().end())

View File

@ -0,0 +1,56 @@
#ifndef seafire_routing__parameters_hxx_
#define seafire_routing__parameters_hxx_
#include <map>
#include <optional>
#include <string>
#include <utility>
namespace seafire::routing
{
class parameters_t
{
public:
using map_type = std::map<std::string, std::string>;
parameters_t() = default;
parameters_t(map_type values)
: _values{std::move(values)}
{}
map_type&
map();
map_type const&
map() const;
std::optional<std::string>
get(std::string const&) const;
private:
map_type _values;
};
class host_parameters_t
: public parameters_t
{
public:
using parameters_t::parameters_t;
};
class route_parameters_t
: public parameters_t
{
public:
using parameters_t::parameters_t;
};
} // namespace seafire::routing
#endif

View File

@ -4,7 +4,7 @@
#include <seafire/server/parameters.hxx> #include <seafire/server/parameters.hxx>
#include <seafire/server/request.hxx> #include <seafire/server/request.hxx>
#include <seafire/routing/route-parameters.hxx> #include <seafire/routing/parameters.hxx>
#include <optional> #include <optional>
@ -16,23 +16,18 @@ namespace seafire::routing
typename ParameterType = server::string_parameter_t typename ParameterType = server::string_parameter_t
> >
class route_parameter_t class route_parameter_t
: public server::named_parameter_t<Name>
{ {
public: public:
using parameter_type = ParameterType; using parameter_type = ParameterType;
using value_type = typename parameter_type::value_type; using value_type = typename parameter_type::value_type;
static
std::string const&
name()
{
static std::string const name{Name};
return name;
}
route_parameter_t(std::optional<value_type> value) route_parameter_t(std::optional<value_type> value)
: _value{std::move(value)} : _value{std::move(value)}
{} {}
using server::named_parameter_t<Name>::name;
std::optional<value_type> const& std::optional<value_type> const&
value() const value() const
{ {

View File

@ -1,30 +0,0 @@
#ifndef seafire__routing__route_parameters_hxx_
#define seafire__routing__route_parameters_hxx_
#include <map>
#include <optional>
#include <string>
namespace seafire::routing
{
class route_parameters_t
{
public:
std::map<std::string, std::string>&
map();
std::map<std::string, std::string> const&
map() const;
std::optional<std::string>
get(std::string const& key) const;
private:
std::map<std::string, std::string> kv_;
};
} // namespace seafire::routing
#endif

View File

@ -6,18 +6,21 @@ namespace seafire::routing
{ {
static static
void std::string
ensure_valid_path(std::string const& path) validate_path(std::string path)
{ {
if (!path.empty()) { if (!path.empty()) {
if (path.front() == '/') { if (path.front() == '/') {
throw std::invalid_argument{"route path must not start with '/'"}; throw std::invalid_argument{"route path must not begin with '/'"};
} }
if (path.back() == '/') { if (path.back() == '/') {
throw std::invalid_argument{"route path must not end with '/'"}; throw std::invalid_argument{"route path must not end with '/'"};
} }
} }
return path;
} }
route_t:: route_t::
@ -25,17 +28,13 @@ namespace seafire::routing
route_t:: route_t::
route_t(std::string path) route_t(std::string path)
: path_{std::move(path)} : path_{validate_path(std::move(path))}
{ {}
ensure_valid_path(path_);
}
route_t:: route_t::
route_t(std::string path, server::request_handler_t handler) route_t(std::string path, server::request_handler_t handler)
: path_{std::move(path)}, handler_{std::move(handler)} : path_{validate_path(std::move(path))}, handler_{std::move(handler)}
{ {}
ensure_valid_path(path_);
}
std::string const& std::string const&
route_t:: route_t::
@ -80,26 +79,20 @@ namespace seafire::routing
return children_.back(); return children_.back();
} }
route_t&
route_t::
add_route(route_t r)
{
children_.emplace_back(std::move(r));
return children_.back();
}
route_t& route_t&
route_t:: route_t::
add_route(std::string path) add_route(std::string path)
{ {
return add_route(route_t{std::move(path)}); children_.emplace_back(std::move(path));
return children_.back();
} }
route_t& route_t&
route_t:: route_t::
add_route(std::string path, server::request_handler_t handler) add_route(std::string path, server::request_handler_t handler)
{ {
return add_route(route_t{std::move(path), std::move(handler)}); children_.emplace_back(std::move(path), std::move(handler));
return children_.back();
} }
std::ostream& std::ostream&

View File

@ -24,6 +24,9 @@ namespace seafire::routing
route_t(std::string, server::request_handler_t); route_t(std::string, server::request_handler_t);
route_t(route_t const&) = delete;
route_t(route_t&&) = delete;
std::string const& std::string const&
path() const; path() const;
@ -42,15 +45,15 @@ namespace seafire::routing
route_t& route_t&
add_route(); add_route();
route_t&
add_route(route_t);
route_t& route_t&
add_route(std::string); add_route(std::string);
route_t& route_t&
add_route(std::string, server::request_handler_t); add_route(std::string, server::request_handler_t);
route_t& operator=(route_t const&) = delete;
route_t& operator=(route_t&&) = delete;
private: private:
std::string path_; std::string path_;
std::vector<server::middleware_t> middleware_; std::vector<server::middleware_t> middleware_;

View File

@ -1,6 +1,8 @@
#include <seafire/routing/router.hxx> #include <seafire/routing/router.hxx>
#include <seafire/routing/diagnostics.hxx> #include <seafire/routing/diagnostics.hxx>
#include <seafire/protocol/rfc7230/host.hxx>
namespace seafire::routing namespace seafire::routing
{ {
@ -58,11 +60,23 @@ namespace seafire::routing
{ {
trace() << "on_request(...)"; trace() << "on_request(...)";
auto host = get<seafire::protocol::rfc7230::host_t>(req);
if (!host) {
trace() << "host not present on request!";
res.send(server::common_error_t::not_found);
return;
}
auto hostname = host->hostname();
auto path = normalize_path(req.get_message().target_uri().path_str()); auto path = normalize_path(req.get_message().target_uri().path_str());
trace() << "locating endpoint for [" << path << "]"; trace() << "locating endpoint:\n"
<< " -> hostname: " << hostname << '\n'
<< " -> path : " << path
;
auto result = routing_table().find_route(path); auto result = routing_table().find_route(hostname, path);
if (!result) { if (!result) {
trace() << "endpoint for [" << path << "] not found"; trace() << "endpoint for [" << path << "] not found";
@ -72,15 +86,19 @@ namespace seafire::routing
auto trace_endpoint = [&result](common::diagnostics_t::proxy_t proxy) auto trace_endpoint = [&result](common::diagnostics_t::proxy_t proxy)
{ {
proxy << "endpoint found"; proxy << "endpoint found!";
for (auto const& j : result->params.map()) for (auto const& j : result->host_params.map())
proxy << "\n -> " << j.first << " = " << j.second << '\n'; proxy << "\n -> host param : " << j.first << " = " << j.second << '\n';
for (auto const& j : result->route_params.map())
proxy << "\n -> route param: " << j.first << " = " << j.second << '\n';
}; };
trace_endpoint(trace()); trace_endpoint(trace());
req.extensions().extend(&res.memory().alloc(result->params)); req.extensions().extend(&res.memory().alloc(result->host_params));
req.extensions().extend(&res.memory().alloc(result->route_params));
result->handler.invoke(req, res); result->handler.invoke(req, res);
} }

View File

@ -1,7 +1,6 @@
#include <seafire/routing/routing-table.hxx> #include <seafire/routing/routing-table.hxx>
#include <seafire/routing/match-path.hxx> #include <seafire/routing/match.hxx>
#include <seafire/routing/flatten-route.hxx>
namespace seafire::routing namespace seafire::routing
{ {
@ -20,66 +19,27 @@ namespace seafire::routing
std::optional<routing_table_t::find_result_t> std::optional<routing_table_t::find_result_t>
routing_table_t:: routing_table_t::
find_route(std::string const& path) const find_route(std::string const& host, std::string const& path) const
{ {
for (auto const& e : endpoints()) { for (auto const& e : endpoints()) {
route_parameters_t params; host_parameters_t host_params;
if (match_path(e.pattern(), path, params)) if (!match(host, e.host(), '.', host_params)) {
return find_result_t{e.handler(), std::move(params)}; continue;
}
route_parameters_t route_params;
if (match(path, e.path(), '/', route_params)) {
return find_result_t{
std::move(host_params),
std::move(route_params),
e.handler()
};
}
} }
return {}; return std::nullopt;
}
routing_table_t::builder_t::
builder_t() = default;
routing_table_t::builder_t::
~builder_t() noexcept = default;
route_t&
routing_table_t::builder_t::
add_route()
{
roots_.emplace_back();
return roots_.back();
}
route_t&
routing_table_t::builder_t::
add_route(route_t r)
{
roots_.emplace_back(std::move(r));
return roots_.back();
}
route_t&
routing_table_t::builder_t::
add_route(std::string path)
{
return add_route(route_t{std::move(path)});
}
route_t&
routing_table_t::builder_t::
add_route(std::string path,
server::request_handler_t handler)
{
return add_route({std::move(path), std::move(handler)});
}
routing_table_t
routing_table_t::builder_t::
build() const
{
std::vector<endpoint_t> endpoints;
for (auto const& r : roots_) {
flatten_route(endpoints, r);
}
return routing_table_t{std::move(endpoints)};
} }
} // namespace seafire::routing } // namespace seafire::routing

View File

@ -2,7 +2,7 @@
#define seafire__routing__routing_table_hxx_ #define seafire__routing__routing_table_hxx_
#include <seafire/routing/endpoint.hxx> #include <seafire/routing/endpoint.hxx>
#include <seafire/routing/route-parameters.hxx> #include <seafire/routing/parameters.hxx>
#include <seafire/routing/route.hxx> #include <seafire/routing/route.hxx>
#include <seafire/server/request-handler.hxx> #include <seafire/server/request-handler.hxx>
@ -18,12 +18,12 @@ namespace seafire::routing
class routing_table_t class routing_table_t
{ {
public: public:
class builder_t;
struct find_result_t struct find_result_t
{ {
host_parameters_t host_params;
route_parameters_t route_params;
server::request_handler_t const& handler; server::request_handler_t const& handler;
route_parameters_t params;
}; };
@ -34,45 +34,17 @@ namespace seafire::routing
endpoints() const; endpoints() const;
std::optional<find_result_t> std::optional<find_result_t>
find_route(std::string const&) const; find_route(std::string const&, std::string const&) const;
private: private:
static
bool
match_host(std::string const&, std::string const&);
std::vector<endpoint_t> endpoints_; std::vector<endpoint_t> endpoints_;
}; };
class routing_table_t::builder_t {
public:
builder_t();
builder_t(builder_t const&) = delete;
builder_t(builder_t&&) = delete;
~builder_t() noexcept;
route_t&
add_route();
route_t&
add_route(route_t);
route_t&
add_route(std::string);
route_t&
add_route(std::string, server::request_handler_t);
routing_table_t
build() const;
builder_t& operator=(builder_t const&) = delete;
builder_t& operator=(builder_t&&) = delete;
private:
std::list<route_t> roots_;
};
} // namespace seafire::routing } // namespace seafire::routing
#endif #endif

View File

@ -0,0 +1,100 @@
#include <seafire/routing/virtual-host.hxx>
namespace seafire::routing
{
static
std::string
validate_host(std::string host)
{
if (host.empty()) {
throw std::invalid_argument{"host must not be empty"};
}
if (host.front() == '.') {
throw std::invalid_argument{"host must not begin with '.'"};
}
if (host.back() == '.') {
throw std::invalid_argument{"host must not end with '.'"};
}
return host;
}
virtual_host_t::
virtual_host_t(std::string host)
: _host{validate_host(std::move(host))}
{}
std::string const&
virtual_host_t::
host() const
{
return _host;
}
std::vector<server::middleware_t> const&
virtual_host_t::
middleware() const
{
return _middleware;
}
std::list<route_t> const&
virtual_host_t::
routes() const
{
return _routes;
}
void
virtual_host_t::
use(server::middleware_t m)
{
_middleware.emplace_back(std::move(m));
}
route_t&
virtual_host_t::
add_route()
{
_routes.emplace_back();
return _routes.back();
}
route_t&
virtual_host_t::
add_route(std::string path)
{
_routes.emplace_back(std::move(path));
return _routes.back();
}
route_t&
virtual_host_t::
add_route(std::string path, server::request_handler_t handler)
{
_routes.emplace_back(std::move(path), std::move(handler));
return _routes.back();
}
std::ostream&
to_stream(std::ostream& o, virtual_host_t const& vhost)
{
o << " => " << vhost.host() << '\n';
for (auto const& r : vhost.routes()) {
to_stream(o, r, 2);
}
return o;
}
std::ostream&
operator<<(std::ostream& o, virtual_host_t const& vhost)
{
return to_stream(o, vhost);
}
} // namespace seafire::routing

View File

@ -0,0 +1,65 @@
#ifndef seafire_routing__virtual_host_hxx_
#define seafire_routing__virtual_host_hxx_
#include <seafire/routing/route.hxx>
#include <seafire/server/middleware.hxx>
#include <seafire/server/request-handler.hxx>
#include <iostream>
#include <list>
#include <optional>
#include <vector>
namespace seafire::routing
{
class virtual_host_t
{
public:
explicit
virtual_host_t(std::string);
virtual_host_t(virtual_host_t const&) = delete;
virtual_host_t(virtual_host_t&&) = delete;
std::string const&
host() const;
std::vector<server::middleware_t> const&
middleware() const;
std::list<route_t> const&
routes() const;
void
use(server::middleware_t);
route_t&
add_route();
route_t&
add_route(std::string);
route_t&
add_route(std::string, server::request_handler_t);
virtual_host_t& operator=(virtual_host_t const&) = delete;
virtual_host_t& operator=(virtual_host_t&&) = delete;
private:
std::string _host;
std::vector<server::middleware_t> _middleware;
std::list<route_t> _routes;
};
std::ostream&
to_stream(std::ostream&, virtual_host_t const&);
std::ostream&
operator<<(std::ostream&, virtual_host_t const&);
} // namespace seafire::routing
#endif