commit ef549ce2f59e881f94ee84c0c4c17e2e4d6bbd5c Author: Ryan Date: Tue Dec 24 22:55:12 2024 +0100 Hello libcode-seafire-routing diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..ec9f3de --- /dev/null +++ b/.editorconfig @@ -0,0 +1,17 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +indent_size = 4 +max_line_length = off +trim_trailing_whitespace = false + +[*.yaml] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..176a458 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/.gitea/workflows/on-push.yaml b/.gitea/workflows/on-push.yaml new file mode 100644 index 0000000..23a9e11 --- /dev/null +++ b/.gitea/workflows/on-push.yaml @@ -0,0 +1,24 @@ +name: on-push +on: [push] + +jobs: + build-and-test: + runs-on: linux + container: code.helloryan.se/infra/buildenv/cxx-amd64-fedora-40:latest + volumes: + - /build + steps: + - name: Clone repository + uses: actions/checkout@v3 + - name: Authenticate + run: | + git config unset http.https://code.helloryan.se/.extraheader + echo "${{ secrets.NETRC }}" >> ~/.netrc + - name: Initialize + run: | + bpkg create -d /build cc config.cc.coptions="-Wall -Werror" + bdep init -A /build + - name: Build + run: b + - name: Test + run: b test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c19e5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.bdep/ +Doxyfile + +# Local default options files. +# +.build2/local/ + +# Compiler/linker output. +# +*.d +*.t +*.i +*.i.* +*.ii +*.ii.* +*.o +*.obj +*.gcm +*.pcm +*.ifc +*.so +*.dylib +*.dll +*.a +*.lib +*.exp +*.pdb +*.ilk +*.exe +*.exe.dlls/ +*.exe.manifest +*.pc diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..dfc745b --- /dev/null +++ b/LICENSE @@ -0,0 +1,31 @@ +Copyright © 2024 Ryan. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. All advertising materials mentioning features or use of this software must + display the following acknowledgement: + + This product includes software developed by Ryan, http://helloryan.se/. + +4. Neither the name(s) of the copyright holder(s) nor the names of its + contributors may be used to endorse or promote products derived from this + software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER "AS IS" AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES +OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN +NO EVENT SHALL COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c5bdb7 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# libcode-seafire-routing + +![Build status](https://code.helloryan.se/code/libcode-seafire-routing/actions/workflows/on-push.yaml/badge.svg) + +## Requirements + +None, other than a modern C++-compiler. + +## Building + +See the wiki, https://code.helloryan.se/code/wiki/wiki/Build-Instructions, for +build instructions. + +## Contact + +Please report bugs and issues by sending an e-mail to: ryan@helloryan.se. + +## Contributing + +Please send an e-mail to ryan@helloryan.se to request an account and +write-access to the libcode-seafire-routing repository. diff --git a/build/.gitignore b/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/build/bootstrap.build b/build/bootstrap.build new file mode 100644 index 0000000..fd9a7a7 --- /dev/null +++ b/build/bootstrap.build @@ -0,0 +1,7 @@ +project = libcode-seafire-routing + +using version +using config +using test +using install +using dist diff --git a/build/export.build b/build/export.build new file mode 100644 index 0000000..a2b0c02 --- /dev/null +++ b/build/export.build @@ -0,0 +1,6 @@ +$out_root/ +{ + include code/seafire/routing/ +} + +export $out_root/code/seafire/routing/$import.target diff --git a/build/root.build b/build/root.build new file mode 100644 index 0000000..21e0a2e --- /dev/null +++ b/build/root.build @@ -0,0 +1,16 @@ +# Uncomment to suppress warnings coming from external libraries. +# +#cxx.internal.scope = current + +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/buildfile b/buildfile new file mode 100644 index 0000000..bbe185e --- /dev/null +++ b/buildfile @@ -0,0 +1,5 @@ +./: {code/ tests/} doc{README.md} legal{LICENSE} manifest + +# Don't install tests. +# +tests/: install = false diff --git a/code/seafire/routing/.gitignore b/code/seafire/routing/.gitignore new file mode 100644 index 0000000..b1ed0e0 --- /dev/null +++ b/code/seafire/routing/.gitignore @@ -0,0 +1,9 @@ +# Generated version header. +# +version.hxx + +# Unit test executables and Testscript output directories +# (can be symlinks). +# +*.test +test-*.test diff --git a/code/seafire/routing/buildfile b/code/seafire/routing/buildfile new file mode 100644 index 0000000..a197c7c --- /dev/null +++ b/code/seafire/routing/buildfile @@ -0,0 +1,67 @@ +intf_libs = # Interface dependencies. +impl_libs = # Implementation dependencies. + +import impl_libs =+ libcode-uri%lib{code-uri} +import intf_libs =+ libcode-seafire-common%lib{code-seafire-common} +import intf_libs =+ libcode-seafire-protocol%lib{code-seafire-protocol} +import intf_libs =+ libcode-seafire-server%lib{code-seafire-server} + +./: lib{code-seafire-routing}: libul{code-seafire-routing} + +libul{code-seafire-routing}: {hxx ixx txx cxx}{** -**.test... -version} \ + {hxx }{ version} + +libul{code-seafire-routing}: $impl_libs $intf_libs + +# Unit tests. +# +exe{*.test}: +{ + test = true + install = false +} + +for t: cxx{**.test...} +{ + d = $directory($t) + n = $name($t)... + + ./: $d/exe{$n}: $t $d/{hxx ixx txx}{+$n} $d/testscript{+$n} + $d/exe{$n}: libul{code-seafire-routing}: bin.whole = false +} + +hxx{version}: in{version} $src_root/manifest +{ + dist = true + clean = ($src_root != $out_root) +} + +# Build options. +# +cxx.poptions =+ "-I$out_root" "-I$src_root" + +# Export options. +# +lib{code-seafire-routing}: +{ + cxx.export.poptions = "-I$out_root" "-I$src_root" + cxx.export.libs = $intf_libs +} + +# For pre-releases use the complete version to make sure they cannot +# be used in place of another pre-release or the final version. See +# the version module for details on the version.* variable values. +# +if $version.pre_release + lib{code-seafire-routing}: bin.lib.version = "-$version.project_id" +else + lib{code-seafire-routing}: bin.lib.version = "-$version.major.$version.minor" + +# Install into the code/seafire/routing/ subdirectory of, say, /usr/include/ +# recreating subdirectories. +# +{hxx ixx txx}{*}: +{ + install = include/code/seafire/routing/ + install.subdirs = true +} diff --git a/code/seafire/routing/diagnostics.cxx b/code/seafire/routing/diagnostics.cxx new file mode 100644 index 0000000..cc88b8c --- /dev/null +++ b/code/seafire/routing/diagnostics.cxx @@ -0,0 +1,15 @@ +#include + +namespace code::seafire::routing +{ + + /// Returns a reference to the routing diagnostic category. + /// + common::diagnostics_t::category_t const& + routing_category() + { + static common::diagnostics_t::category_t category{"router"}; + return category; + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/diagnostics.hxx b/code/seafire/routing/diagnostics.hxx new file mode 100644 index 0000000..774dd8f --- /dev/null +++ b/code/seafire/routing/diagnostics.hxx @@ -0,0 +1,14 @@ +#ifndef code__seafire__routing__diagnostics_hxx_ +#define code__seafire__routing__diagnostics_hxx_ + +#include + +namespace code::seafire::routing +{ + + common::diagnostics_t::category_t const& + routing_category(); + +} // namespace code::seafire::routing + +#endif diff --git a/code/seafire/routing/endpoint.cxx b/code/seafire/routing/endpoint.cxx new file mode 100644 index 0000000..e3e52a9 --- /dev/null +++ b/code/seafire/routing/endpoint.cxx @@ -0,0 +1,37 @@ +#include + +namespace code::seafire::routing +{ + + endpoint_t:: + endpoint_t(std::string pattern, server::request_handler_t handler) + : pattern_{std::move(pattern)}, handler_{std::move(handler)} + {} + + std::string const& + endpoint_t:: + pattern() const + { + return pattern_; + } + + server::request_handler_t const& + endpoint_t:: + handler() const + { + return handler_; + } + + std::ostream& + to_stream(std::ostream& o, endpoint_t const& ep) + { + return o << ep.pattern(); + } + + std::ostream& + operator<<(std::ostream& o, endpoint_t const& ep) + { + return to_stream(o, ep); + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/endpoint.hxx b/code/seafire/routing/endpoint.hxx new file mode 100644 index 0000000..c7d75b1 --- /dev/null +++ b/code/seafire/routing/endpoint.hxx @@ -0,0 +1,37 @@ +#ifndef code__seafire__routing__endpoint_hxx_ +#define code__seafire__routing__endpoint_hxx_ + +#include + +#include +#include + +namespace code::seafire::routing +{ + + class endpoint_t + { + public: + endpoint_t(std::string, server::request_handler_t); + + std::string const& + pattern() const; + + server::request_handler_t const& + handler() const; + + private: + std::string pattern_; + server::request_handler_t handler_; + + }; + + std::ostream& + to_stream(std::ostream&, endpoint_t const&); + + std::ostream& + operator<<(std::ostream&, endpoint_t const&); + +} // namespace code::seafire::routing + +#endif diff --git a/code/seafire/routing/flatten-route.cxx b/code/seafire/routing/flatten-route.cxx new file mode 100644 index 0000000..83a41b1 --- /dev/null +++ b/code/seafire/routing/flatten-route.cxx @@ -0,0 +1,50 @@ +#include + +namespace code::seafire::routing +{ + + void + flatten_route(std::vector& endpoints, + std::vector 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& endpoints, route_t const& r) + { + flatten_route(endpoints, {}, r, "/"); + } + + std::vector + flatten_route(route_t const& r) + { + std::vector endpoints; + flatten_route(endpoints, r); + return endpoints; + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/flatten-route.hxx b/code/seafire/routing/flatten-route.hxx new file mode 100644 index 0000000..9bf6f2f --- /dev/null +++ b/code/seafire/routing/flatten-route.hxx @@ -0,0 +1,28 @@ +#ifndef code__seafire__routing__flatten_route_hxx_ +#define code__seafire__routing__flatten_route_hxx_ + +#include +#include + +#include + +#include + +namespace code::seafire::routing +{ + + void + flatten_route(std::vector& endpoints, + std::vector, + route_t const& r, + std::string const& root); + + void + flatten_route(std::vector&, route_t const&); + + std::vector + flatten_route(route_t const&); + +} // namespace code::seafire::routing + +#endif diff --git a/code/seafire/routing/match-path.cxx b/code/seafire/routing/match-path.cxx new file mode 100644 index 0000000..b928988 --- /dev/null +++ b/code/seafire/routing/match-path.cxx @@ -0,0 +1,116 @@ +#include + +#include +#include +#include + +namespace code::seafire::routing +{ + + bool + match_path(std::string const& pattern, + std::string const& subject, + route_parameters_t& params) + { + route_parameters_t tmp_params; + + // p/pend = pattern iterators. + // + auto p = pattern.begin(); + auto const pend = pattern.end(); + + // s/send = subject iterators. + // + auto s = subject.begin(); + auto const send = subject.end(); + + auto match_param = [&]() + { + // k/kend = local pattern iterators. + // + auto const k = p; + while (p != pend && '}' != *p) { + ++p; + } + auto const kend = p; + ++p; + + if (k == kend) { + throw std::invalid_argument{"empty parameter name"}; + } + + // a greedy parameter will eat the rest of the subject. + // + bool const greedy{'*' == *k && 1 == std::distance(k, kend)}; + + // v/vend = local subject iterators. + // + auto const v = s; + while (s != send && (greedy || '/' != *s)) { + ++s; + } + auto const vend = s; + + tmp_params.map().emplace(std::string{k, kend}, std::string{v, vend}); + }; + + while (p != pend) { + if (*p == '{') { + ++p; + match_param(); + continue; + } + + if (s == send) { + break; + } + + if (*p != *s) { + return false; + } + + ++p; + ++s; + } + + if (p != pend || s != send) { + return false; + } + + params = std::move(tmp_params); + + return true; + } + + std::string + render_path(std::string const& pattern, + route_parameters_t const& params) + { + std::stringstream str; + + auto p = pattern.begin(); + auto pend = pattern.end(); + + while (p != pend) { + if (*p == '{') { + ++p; + + auto const k = p; + while (p != pend && '}' != *p) { + ++p; + } + auto const kend = p; + + str << params.get(std::string{k, kend}).value_or(""); + } + else { + str << *p; + } + + ++p; + } + + return str.str(); + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/match-path.hxx b/code/seafire/routing/match-path.hxx new file mode 100644 index 0000000..897423e --- /dev/null +++ b/code/seafire/routing/match-path.hxx @@ -0,0 +1,24 @@ +#ifndef code__seafire__routing__match_path_hxx_ +#define code__seafire__routing__match_path_hxx_ + +#include + +#include + +#include + +namespace code::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 code::seafire::routing + +#endif diff --git a/code/seafire/routing/route-parameter.hxx b/code/seafire/routing/route-parameter.hxx new file mode 100644 index 0000000..5d55553 --- /dev/null +++ b/code/seafire/routing/route-parameter.hxx @@ -0,0 +1,49 @@ +#ifndef code__seafire__routing__route_parameters_hxx_ +#define code__seafire__routing__route_parameters_hxx_ + +#include +#include + +#include + +namespace code::seafire::routing +{ + + template< + server::parameter_name Name, + typename ParameterType = server::string_parameter + > + class route_parameter_t + { + public: + using parameter_type = ParameterType; + using value_type = typename parameter_type::value_type; + + static + std::string const& + name(); + + route_parameter_t(std::optional); + + std::optional const& + value() const; + + operator std::optional const&() const; + + std::optional const* + operator->() const; + + static + route_parameter_t + fetch(server::request_t&); + + private: + std::optional value_; + + }; + +} // namespace code::seafire::routing + +#include + +#endif diff --git a/code/seafire/routing/route-parameter.txx b/code/seafire/routing/route-parameter.txx new file mode 100644 index 0000000..ef387ad --- /dev/null +++ b/code/seafire/routing/route-parameter.txx @@ -0,0 +1,51 @@ +namespace code::seafire::routing +{ + + template + std::string const& + route_parameter_t:: + name() + { + static std::string const name{Name}; + return name; + } + + template + route_parameter_t:: + route_parameter_t(std::optional value) + : value_{std::move(value)} + {} + + template + std::optional::value_type> const& + route_parameter_t:: + value() const + { + return value; + } + + template + route_parameter_t:: + operator std::optional const&() const + { + return value(); + } + + template + std::optional::value_type> const* + route_parameter_t:: + operator->() const + { + return &value(); + } + + template + route_parameter_t + route_parameter_t:: + fetch(server::request_t&) + { + auto v = req.extensions().use().get(name()); + return parameter_type::try_parse(v); + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/route-parameters.cxx b/code/seafire/routing/route-parameters.cxx new file mode 100644 index 0000000..a604e9c --- /dev/null +++ b/code/seafire/routing/route-parameters.cxx @@ -0,0 +1,30 @@ +#include + +namespace code::seafire::routing +{ + + std::map& + route_parameters_t:: + map() + { + return kv_; + } + + std::map const& + route_parameters_t:: + map() const + { + return kv_; + } + + std::optional + route_parameters_t:: + get(std::string const& key) const + { + if (auto it = map().find(key); it != map().end()) + return it->second; + + return std::nullopt; + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/route-parameters.hxx b/code/seafire/routing/route-parameters.hxx new file mode 100644 index 0000000..f5824ed --- /dev/null +++ b/code/seafire/routing/route-parameters.hxx @@ -0,0 +1,30 @@ +#ifndef code__seafire__routing__route_parameters_hxx_ +#define code__seafire__routing__route_parameters_hxx_ + +#include +#include +#include + +namespace code::seafire::routing +{ + + class route_parameters_t + { + public: + std::map& + map(); + + std::map const& + map() const; + + std::optional + get(std::string const& key) const; + + private: + std::map kv_; + + }; + +} // namespace code::seafire::routing + +#endif diff --git a/code/seafire/routing/route.cxx b/code/seafire/routing/route.cxx new file mode 100644 index 0000000..ed88c56 --- /dev/null +++ b/code/seafire/routing/route.cxx @@ -0,0 +1,134 @@ +#include + +#include + +namespace code::seafire::routing +{ + + static + void + ensure_valid_path(std::string const& path) + { + if (!path.empty()) { + if (path.front() == '/') { + throw std::invalid_argument{"route path must not start with '/'"}; + } + + if (path.back() == '/') { + throw std::invalid_argument{"route path must not end with '/'"}; + } + } + } + + route_t:: + route_t() = default; + + route_t:: + route_t(std::string path) + : path_{std::move(path)} + { + ensure_valid_path(path_); + } + + route_t:: + route_t(std::string path, server::request_handler_t handler) + : path_{std::move(path)}, handler_{std::move(handler)} + { + ensure_valid_path(path_); + } + + std::string const& + route_t:: + path() const + { + return path_; + } + + std::vector const& + route_t:: + middleware() const + { + return middleware_; + } + + std::optional const& + route_t:: + handler() const + { + return handler_; + } + + std::list const& + route_t:: + children() const + { + return children_; + } + + void + route_t:: + use(server::middleware_t m) + { + middleware_.emplace_back(std::move(m)); + } + + route_t& + route_t:: + add_route() + { + children_.emplace_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:: + add_route(std::string path) + { + return add_route(route_t{std::move(path)}); + } + + route_t& + route_t:: + add_route(std::string path, server::request_handler_t handler) + { + return add_route(route_t{std::move(path), std::move(handler)}); + } + + std::ostream& + to_stream(std::ostream& o, route_t const& r, std::size_t indent) + { + o << std::string(indent, ' '); + o << " -> '" << (r.path().empty() ? "" : r.path()) << '\''; + + if (!r.middleware().empty()) { + o << " (with middleware)"; + } + + if (!r.handler()) { + o << " (null handler)"; + } + + o << '\n'; + + for (auto const& child : r.children()) { + to_stream(o, child, indent + 2); + } + + return o; + } + + std::ostream& + operator<<(std::ostream& o, route_t const& route) + { + return to_stream(o, route, 0); + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/route.hxx b/code/seafire/routing/route.hxx new file mode 100644 index 0000000..85b2cdc --- /dev/null +++ b/code/seafire/routing/route.hxx @@ -0,0 +1,74 @@ +#ifndef code__seafire__routing__route_hxx_ +#define code__seafire__routing__route_hxx_ + +#include + +#include +#include + +#include +#include +#include +#include + +namespace code::seafire::routing +{ + + class route_t + { + public: + route_t(); + + explicit + route_t(std::string); + + route_t(std::string, server::request_handler_t); + + std::string const& + path() const; + + std::vector const& + middleware() const; + + std::optional const& + handler() const; + + std::list const& + children() const; + + void + use(server::middleware_t); + + 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); + + private: + std::string path_; + std::vector middleware_; + std::optional handler_; + + // must be std::list to prevent invalidation of references to + // individual routes. + // + std::list children_; + + }; + + std::ostream& + to_stream(std::ostream&, route_t const&, std::size_t); + + std::ostream& + operator<<(std::ostream&, route_t const&); + +} // namespace code::seafire::routing + +#endif diff --git a/code/seafire/routing/router.cxx b/code/seafire/routing/router.cxx new file mode 100644 index 0000000..9f5f131 --- /dev/null +++ b/code/seafire/routing/router.cxx @@ -0,0 +1,102 @@ +#include +#include + +namespace code::seafire::routing +{ + + static + std::string + normalize_path(std::string const& path) + { + std::stringstream ipath{path}; + std::vector segments; + + for (std::string segment; std::getline(ipath, segment, '/');) { + if (segment.empty()) { + continue; + } + + if (segment == ".") { + continue; + } + + if (segment == "..") { + if (!segments.empty()) { + segments.pop_back(); + } + continue; + } + + segments.push_back(segment); + } + + std::string normalized; + + for (auto const& j : segments) { + normalized += '/'; + normalized += j; + } + + return normalized.empty() ? "/" : normalized; + } + + router_t:: + router_t(common::diagnostics_t& diagnostics, routing_table_t table) + : diagnostics_{diagnostics}, rt_{std::move(table)} + {} + + routing_table_t const& + router_t:: + routing_table() const + { + return rt_; + } + + void + router_t:: + on_request(server::request_t& req, server::response_t& res) const + { + trace() << "on_request(...)"; + + auto path = normalize_path(req.get_message().target_uri().path_str()); + + trace() << "locating endpoint for [" << path << "]"; + + auto result = routing_table().find_route(path); + + if (!result) { + trace() << "endpoint for [" << path << "] not found"; + res.send(server::common_error_t::not_found); + return; + } + + auto trace_endpoint = [&result](common::diagnostics_t::proxy_t proxy) + { + proxy << "endpoint found"; + + for (auto const& j : result->params.map()) + proxy << "\n -> " << j.first << " = " << j.second << '\n'; + }; + + trace_endpoint(trace()); + + req.extensions().extend(&res.memory().alloc(result->params)); + + result->handler.invoke(req, res); + } + + void + router_t:: + operator()(server::request_t& req, server::response_t& res) const + { + on_request(req, res); + } + + common::diagnostics_t::proxy_t + router_t:: + trace() const + { + return diagnostics_ << routing_category(); + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/router.hxx b/code/seafire/routing/router.hxx new file mode 100644 index 0000000..d505020 --- /dev/null +++ b/code/seafire/routing/router.hxx @@ -0,0 +1,40 @@ +#ifndef code__seafire__routing__router_hxx_ +#define code__seafire__routing__router_hxx_ + +#include +#include + +#include + +#include +#include + +namespace code::seafire::routing +{ + + class router_t + { + public: + router_t(common::diagnostics_t&, routing_table_t); + + routing_table_t const& + routing_table() const; + + void + on_request(server::request_t&, server::response_t&) const; + + void + operator()(server::request_t&, server::response_t&) const; + + private: + common::diagnostics_t::proxy_t + trace() const; + + common::diagnostics_t& diagnostics_; + routing_table_t rt_; + + }; + +} // namespace code::seafire::routing + +#endif diff --git a/code/seafire/routing/routing-table.cxx b/code/seafire/routing/routing-table.cxx new file mode 100644 index 0000000..299575e --- /dev/null +++ b/code/seafire/routing/routing-table.cxx @@ -0,0 +1,85 @@ +#include + +#include +#include + +namespace code::seafire::routing +{ + + routing_table_t:: + routing_table_t(std::vector endpoints) + : endpoints_{std::move(endpoints)} + {} + + std::vector const& + routing_table_t:: + endpoints() const + { + return endpoints_; + } + + std::optional + routing_table_t:: + find_route(std::string const& path) const + { + for (auto const& e : endpoints()) { + route_parameters_t params; + + if (match_path(e.pattern(), path, params)) + return find_result_t{e.handler(), std::move(params)}; + } + + return {}; + } + + 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 endpoints; + + for (auto const& r : roots_) { + flatten_route(endpoints, r); + } + + return routing_table_t{std::move(endpoints)}; + } + +} // namespace code::seafire::routing diff --git a/code/seafire/routing/routing-table.hxx b/code/seafire/routing/routing-table.hxx new file mode 100644 index 0000000..acf7f1a --- /dev/null +++ b/code/seafire/routing/routing-table.hxx @@ -0,0 +1,78 @@ +#ifndef code__seafire__routing__routing_table_hxx_ +#define code__seafire__routing__routing_table_hxx_ + +#include +#include +#include + +#include + +#include +#include +#include +#include + +namespace code::seafire::routing +{ + + class routing_table_t + { + public: + class builder_t; + + struct find_result_t + { + server::request_handler_t const& handler; + route_parameters_t params; + + }; + + explicit + routing_table_t(std::vector); + + std::vector const& + endpoints() const; + + std::optional + find_route(std::string const&) const; + + private: + std::vector 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 roots_; + + }; + +} // namespace code::seafire::routing + +#endif diff --git a/code/seafire/routing/version.hxx.in b/code/seafire/routing/version.hxx.in new file mode 100644 index 0000000..0daf0d6 --- /dev/null +++ b/code/seafire/routing/version.hxx.in @@ -0,0 +1,37 @@ +#ifndef code__seafire__routing__version_hxx_ +#define code__seafire__routing__version_hxx_ + +// The numeric version format is AAAAABBBBBCCCCCDDDE where: +// +// AAAAA - major version number +// BBBBB - minor version number +// CCCCC - bugfix version number +// DDD - alpha / beta (DDD + 500) version number +// E - final (0) / snapshot (1) +// +// When DDDE is not 0, 1 is subtracted from AAAAABBBBBCCCCC. For example: +// +// Version AAAAABBBBBCCCCCDDDE +// +// 0.1.0 0000000001000000000 +// 0.1.2 0000000001000020000 +// 1.2.3 0000100002000030000 +// 2.2.0-a.1 0000200001999990010 +// 3.0.0-b.2 0000299999999995020 +// 2.2.0-a.1.z 0000200001999990011 +// +#define LIBCODE_SEAFIRE_ROUTING_VERSION $libcode_seafire_routing.version.project_number$ULL +#define LIBCODE_SEAFIRE_ROUTING_VERSION_STR "$libcode_seafire_routing.version.project$" +#define LIBCODE_SEAFIRE_ROUTING_VERSION_ID "$libcode_seafire_routing.version.project_id$" +#define LIBCODE_SEAFIRE_ROUTING_VERSION_FULL "$libcode_seafire_routing.version$" + +#define LIBCODE_SEAFIRE_ROUTING_VERSION_MAJOR $libcode_seafire_routing.version.major$ +#define LIBCODE_SEAFIRE_ROUTING_VERSION_MINOR $libcode_seafire_routing.version.minor$ +#define LIBCODE_SEAFIRE_ROUTING_VERSION_PATCH $libcode_seafire_routing.version.patch$ + +#define LIBCODE_SEAFIRE_ROUTING_PRE_RELEASE $libcode_seafire_routing.version.pre_release$ + +#define LIBCODE_SEAFIRE_ROUTING_SNAPSHOT_SN $libcode_seafire_routing.version.snapshot_sn$ULL +#define LIBCODE_SEAFIRE_ROUTING_SNAPSHOT_ID "$libcode_seafire_routing.version.snapshot_id$" + +#endif diff --git a/manifest b/manifest new file mode 100644 index 0000000..22f7a5c --- /dev/null +++ b/manifest @@ -0,0 +1,15 @@ +: 1 +name: libcode-seafire-routing +version: 0.1.0-a.0.z +language: c++ +summary: libcode-seafire-routing C++ library +license: BSD-4-Clause +description-file: README.md +url: https://helloryan.se/code/ +email: ryan@helloryan.se +depends: * build2 >= 0.17.0 +depends: * bpkg >= 0.17.0 +depends: libcode-uri ^0.1.0- +depends: libcode-seafire-common ^0.1.0- +depends: libcode-seafire-protocol ^0.1.0- +depends: libcode-seafire-server ^0.1.0- diff --git a/repositories.manifest b/repositories.manifest new file mode 100644 index 0000000..9fb5647 --- /dev/null +++ b/repositories.manifest @@ -0,0 +1,23 @@ +: 1 +summary: libcode-seafire-representation project repository + +: +role: prerequisite +location: https://pkg.cppget.org/1/beta +trust: 70:64:FE:E4:E0:F3:60:F1:B4:51:E1:FA:12:5C:E0:B3:DB:DF:96:33:39:B9:2E:E5:C2:68:63:4C:A6:47:39:43 + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-uri.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-seafire-common.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-seafire-protocol.git##HEAD + +: +role: prerequisite +location: https://code.helloryan.se/code/libcode-seafire-server.git##HEAD diff --git a/tests/.gitignore b/tests/.gitignore new file mode 100644 index 0000000..662178d --- /dev/null +++ b/tests/.gitignore @@ -0,0 +1,8 @@ +# Test executables. +# +driver + +# Testscript output directories (can be symlinks). +# +test +test-* diff --git a/tests/build/.gitignore b/tests/build/.gitignore new file mode 100644 index 0000000..974e01d --- /dev/null +++ b/tests/build/.gitignore @@ -0,0 +1,4 @@ +/config.build +/root/ +/bootstrap/ +build/ diff --git a/tests/build/bootstrap.build b/tests/build/bootstrap.build new file mode 100644 index 0000000..a07b5ea --- /dev/null +++ b/tests/build/bootstrap.build @@ -0,0 +1,5 @@ +project = # Unnamed tests subproject. + +using config +using test +using dist diff --git a/tests/build/root.build b/tests/build/root.build new file mode 100644 index 0000000..a67b2fe --- /dev/null +++ b/tests/build/root.build @@ -0,0 +1,16 @@ +cxx.std = latest + +using cxx + +hxx{*}: extension = hxx +ixx{*}: extension = ixx +txx{*}: extension = txx +cxx{*}: extension = cxx + +# Every exe{} in this subproject is by default a test. +# +exe{*}: test = true + +# The test target for cross-testing (running tests under Wine, etc). +# +test.target = $cxx.target diff --git a/tests/buildfile b/tests/buildfile new file mode 100644 index 0000000..aeeab15 --- /dev/null +++ b/tests/buildfile @@ -0,0 +1 @@ +./: {*/ -build/}