Skip to content
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

Implement routing #31

Merged
merged 69 commits into from
Nov 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
6baf848
Create basic route type
Jacquwes Oct 18, 2024
f6b8275
Create a function to add a route to the server
Jacquwes Oct 18, 2024
c67de15
Store server reference in server connections
Jacquwes Oct 18, 2024
c4dc79d
Create get route by path function
Jacquwes Oct 18, 2024
655c698
Make connection call the handler associated with the request uri if f…
Jacquwes Oct 18, 2024
c8c5f00
Fix an issue where std::expected was used incorrectly in coroutines
Jacquwes Oct 18, 2024
c3ab06a
Close the connection after sending the response
Jacquwes Oct 18, 2024
9929348
Add information to the not found response so clients can detect it
Jacquwes Oct 18, 2024
1980f1a
Check for errors while sending the response
Jacquwes Oct 18, 2024
abe3b6f
Improve example by demonstrating add_route
Jacquwes Oct 18, 2024
00b85b6
Rename server_route to route
Jacquwes Oct 28, 2024
cbed0be
Create route_base type inherited by route
Jacquwes Oct 28, 2024
c99d7b5
Add header guards
Jacquwes Oct 28, 2024
0eeb48c
Create customizable route matching function for future route types
Jacquwes Oct 28, 2024
3056c70
Use route_base instead of route
Jacquwes Oct 28, 2024
9538687
Create static route to serve files and directories without overhead.
Jacquwes Oct 28, 2024
d49502c
Simplify execute function
Jacquwes Oct 28, 2024
942770e
Add method not allowed http status code
Jacquwes Oct 29, 2024
f3f950d
Move the path responsibility to route_base
Jacquwes Oct 29, 2024
a32f9cc
Move request handling to separate function
Jacquwes Oct 29, 2024
ef03c59
Add string for method not allowed http status code
Jacquwes Oct 29, 2024
65c42b4
Add HTTP methods support to route_base
Jacquwes Oct 29, 2024
d81fcbd
Allow instanciation of routes with methods
Jacquwes Oct 29, 2024
1cd2496
Update server example to add methods
Jacquwes Oct 29, 2024
17e3ed6
Modify server logic allowing for different routes using the same path…
Jacquwes Oct 29, 2024
fa6fb03
Merge pull request #21 from Jacquwes/19-implement-request-methods-for…
Jacquwes Oct 29, 2024
8548771
Create route_path class for compilation time route path check
Jacquwes Oct 30, 2024
b5de39a
Use route_path type for paths
Jacquwes Oct 30, 2024
f42b804
Merge pull request #27 from Jacquwes/22-compile-time-route-path-valid…
Jacquwes Oct 30, 2024
3f29563
Add comments to example
Jacquwes Oct 30, 2024
4fb0b59
Add compile commands for sonarlint
Jacquwes Nov 5, 2024
ce5160d
Implement function to split path parts with /
Jacquwes Nov 7, 2024
064d04e
Add all missing HTTP methods
Jacquwes Nov 7, 2024
1111b5d
Add new error for path parameter conflicts
Jacquwes Nov 7, 2024
ccfb5b3
Create types for storing routes in a radix tree
Jacquwes Nov 7, 2024
58ec239
Remove old route types
Jacquwes Nov 8, 2024
be43aa3
Remove unused variable
Jacquwes Nov 8, 2024
7ae91b2
Add operator for implicit cast
Jacquwes Nov 8, 2024
d7fc97a
Simplify tree route logic
Jacquwes Nov 8, 2024
e23e602
Use new route tree
Jacquwes Nov 8, 2024
a8f5486
Change vcpkg dependency from gtest to doctest
Jacquwes Nov 9, 2024
c1c387e
Change GTest code for doctest code
Jacquwes Nov 9, 2024
604a51a
Remove unused file
Jacquwes Nov 9, 2024
34fb347
Merge pull request #29 from Jacquwes/use-doctest-instead-of-gtest
Jacquwes Nov 9, 2024
e55e276
Add tests for routes
Jacquwes Nov 9, 2024
aaf0c86
Add missing HTTP method strings
Jacquwes Nov 9, 2024
ecbd92b
Delete unused file
Jacquwes Nov 9, 2024
2dc85d5
Add getters
Jacquwes Nov 9, 2024
307b736
Add error when trying to add two path parameters children to one node
Jacquwes Nov 9, 2024
3668e92
Improve child matching logic
Jacquwes Nov 9, 2024
29b195c
Include for std::min
Jacquwes Nov 9, 2024
d06fa57
Constructor now adds all children when trying to create a multiple se…
Jacquwes Nov 9, 2024
4fca783
Make get_deepest_node also return the depth of the found node
Jacquwes Nov 9, 2024
5e6fc73
Ignore segment separators (/)
Jacquwes Nov 9, 2024
826dba8
Use depth to add children to the correct node
Jacquwes Nov 9, 2024
e966ca5
Update server example to match new architecture
Jacquwes Nov 9, 2024
0537aa9
Merge branch '28-routes-should-be-stored-in-an-efficient-data-structu…
Jacquwes Nov 9, 2024
850de0e
Merge pull request #30 from Jacquwes/28-routes-should-be-stored-in-an…
Jacquwes Nov 9, 2024
672b810
Improve coherence of body content
Jacquwes Nov 9, 2024
4a3947e
Add error codes for path params
Jacquwes Nov 9, 2024
311f388
Fix documentation
Jacquwes Nov 9, 2024
45cc088
Implement function to get path params
Jacquwes Nov 9, 2024
c21a857
Implement function to parse path params in request
Jacquwes Nov 9, 2024
f679eb6
Add path params to request in handler
Jacquwes Nov 9, 2024
3c14569
Update example to showcase use of path params
Jacquwes Nov 9, 2024
cf4408c
Simplify unknown route handling
Jacquwes Nov 9, 2024
a499123
Fix logic which was matching incorrect routes (/mike/wrong was ok for…
Jacquwes Nov 9, 2024
4e3ba9b
Fix index increment logic in route_tree.cpp
Jacquwes Nov 9, 2024
b798cac
Update github workflow to enable tests
Jacquwes Nov 9, 2024
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
5 changes: 3 additions & 2 deletions .github/workflows/ctest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,12 @@ jobs:
if: matrix.os == 'windows-latest'
run: ${{ github.workspace }}/vcpkg/bootstrap-vcpkg.bat -disableMetrics

- name: Install asio and gtest linux
- name: Install dependencies
# Install dependencies using the appropriate triplet for the current runner operating system
run: ${{ github.workspace }}/vcpkg/vcpkg install
if: matrix.os == 'ubuntu-latest'

- name: Install asio and gtest windows
- name: Install dependencies
# Install dependencies using the appropriate triplet for the current runner operating system
run: ${{ github.workspace }}/vcpkg/vcpkg install
if: matrix.os == 'windows-latest'
Expand All @@ -90,6 +90,7 @@ jobs:
-DCMAKE_C_COMPILER=${{ matrix.c_compiler }}
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }}
-DCMAKE_TOOLCHAIN_FILE=${{ github.workspace }}/vcpkg/scripts/buildsystems/vcpkg.cmake
-DENABLE_TESTS=ON
-S ${{ github.workspace }}

- name: Build
Expand Down
15 changes: 9 additions & 6 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,20 @@ project(Pine)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

enable_testing()

find_package(GTest CONFIG REQUIRED)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

if (WIN32)
add_definitions(-D_WIN32_WINNT=0x0A00)
endif()

add_subdirectory(client)
add_subdirectory(examples)
add_subdirectory(shared)
add_subdirectory(server)
add_subdirectory(tests)

if (ENABLE_EXAMPLES)
add_subdirectory(examples)
endif()

if (ENABLE_TESTS)
add_subdirectory(tests)
endif()
2 changes: 1 addition & 1 deletion client/src/client_connection.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,6 @@ namespace pine
if (!send_result)
co_return send_result.error();

co_return error(error_code::success);
co_return{};
}
}
50 changes: 50 additions & 0 deletions examples/server_example.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Purpose: Source file for server example.

#include <filesystem>
#include <iostream>

#include <server.h>
Expand All @@ -8,6 +9,55 @@ int main()
{
pine::server server;

// Add a route that responds to GET and HEAD requests to the root path.
// If the path is not valid, the compilation will fail.
server.add_route("/",
[](const pine::http_request&,
pine::http_response& response)
{
response.set_status(pine::http_status::ok);
response.set_body("Hello, world!");
});

// Add a route that responds to POST request with a path parameter.
server.add_route("/:name",
[](const pine::http_request& request,
pine::http_response& response)
{
const auto& name = request.get_path_param<std::string>("name");
if (!name)
{
switch (name.error().code())
{
case pine::error_code::parameter_not_found:
response.set_status(pine::http_status::bad_request);
response.set_body("The parameter 'name' is required.");
return;
case pine::error_code::invalid_parameter:
response.set_status(pine::http_status::bad_request);
response.set_body("The parameter 'name' is invalid.");
return;
default:
response.set_status(pine::http_status::internal_server_error);
response.set_body("An error occurred.");
return;
}
}

response.set_status(pine::http_status::ok);
response.set_body("Hello, " + name.value() + "!");
},
{ pine::http_method::post });

// Add a route that responds to GET requests to /public_directory/*.
// This route will serve files from the public directory.
server.add_static_route("/public_directory", std::filesystem::current_path() / "public");

// Add a route that responds to GET requests to /public_file.
// This route will serve the file about.html from the public directory.
server.add_static_route("/public_file", std::filesystem::current_path() / "public" / "about.html");

// Start the server
if (auto server_result = server.start(); !server_result)
{
std::cerr << "Error: " << server_result.error().message() << std::endl;
Expand Down
6 changes: 6 additions & 0 deletions server/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@ add_library(server STATIC)

target_sources(server
PRIVATE
"src/route_node.cpp"
"src/route_tree.cpp"
"src/server.cpp"
"src/server_connection.cpp"
"src/route_node.cpp"

PUBLIC
"include/route_node.h"
"include/route_tree.h"
"include/route_path.h"
"include/server.h"
"include/server_connection.h"
)
Expand Down
126 changes: 126 additions & 0 deletions server/include/route_node.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#pragma once

#include <array>
#include <cstdint>
#include <error.h>
#include <expected.h>
#include <functional>
#include <http.h>
#include <http_request.h>
#include <http_response.h>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include <filesystem>

namespace pine
{
/// @brief A node in a radix tree that represents a route or a part of a
/// route.
class route_node
{
public:

/// @brief The type of the handler function.
using handler_type =
std::function<void(const http_request&, http_response&)>;

/// @brief Construct a new base route node. The path of the node
/// corresponds to one part of a route (e.g. a segment of the URI).
///
/// If the path contains a colon followed by a name (e.g. "/users/:id"),
/// the node will be a path parameter node. Path parameter nodes are used to
/// represent routes with path parameters. The path parameter name is the
/// name following the colon, and can be used to extract the path parameter
/// with the pine::http_request::get_path_param<T>(std::string_view name)
/// function.
///
/// If the path contains a /, the node will contain children. The children
/// are used to represent the rest of the route.
///
/// @param path The path of the node.
/// @return A new base route node.
explicit(false) route_node(std::string_view path);

void handle(const http_request& request,
http_response& response) const noexcept
{
handlers_[static_cast<size_t>(request.get_method())]->operator()(request, response);
}

/// @brief Get the path of the node. The path of the node corresponds to one
/// part of a route (e.g. a segment of the URI).
/// @return The path of the node.
constexpr std::string_view path() const noexcept { return path_; }

/// @brief Get the handlers of the node, indexed by pine::http_method.
/// @return The handlers of the node.
constexpr
const std::array<std::unique_ptr<handler_type>, http_method_count>&
handlers() const noexcept { return handlers_; }

/// @brief Add a child to the node. The child will be a part of the route
/// that the node represents.
/// @param path The path of the child.
/// @return A reference to the child. If the child already exists, a
/// reference to the existing child.
route_node&
add_child(std::string_view path);

/// @brief Add a handler to the node. The handler will be called when the
/// route represented by the node is requested and the method matches.
/// Calling this function will overwrite any existing handler for the method.
/// @param method The HTTP method to handle.
/// @param handler The handler to call.
void add_handler(http_method method,
std::unique_ptr<handler_type> handler) noexcept;

/// @brief Find a child of the node by path.
/// @param path The path of the child to find. The path can be a segment of
/// the URI or the rest of the URI.
/// @return If the child was found, a reference to the child. If the child
/// was not found, an error code.
route_node&
find_child(std::string_view path) const noexcept;

/// @brief Check if the node is a path parameter node.
/// @return True if the node is a path parameter node, otherwise false.
constexpr bool is_path_parameter() const noexcept
{
return is_path_parameter_;
}

/// @brief Check if the node has children that are path parameter nodes.
/// @return True if the node has children that are path parameter nodes,
constexpr bool has_path_parameter_children() const noexcept
{
return has_path_parameter_children_;
}

/// @brief Get the list of children of the node.
/// @return The list of children of the node.
constexpr const std::vector<std::unique_ptr<route_node>>& children() const noexcept
{
return children_;
}

route_node& serve_files(std::filesystem::path&& location);

private:
// Optimization: Store the start of path alongside the children for faster
// comparison.
// Optimization: Maybe make a small cache for the children?
// Optimization: Store whether the node is an endpoint or not.

std::array<std::unique_ptr<handler_type>, http_method_count> handlers_{};
uint16_t http_method_mask_ = 0;

std::vector<std::unique_ptr<route_node>> children_;
std::string path_;

bool is_path_parameter_ = false;
bool has_path_parameter_children_ = false;
route_node* path_parameter_child_ = nullptr;
};
}
111 changes: 111 additions & 0 deletions server/include/route_path.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
#pragma once

#include <format>
#include <string_view>
#include <vector>

namespace pine
{
/// @brief Represents a route path. Used to validate paths at compile time
/// and to store paths as string views. Also implements helper functions.
class route_path
{
public:
template <typename T>
explicit(false) consteval route_path(const T& path)
: path_{ path }
{
if (!validate_path(path_))
{
throw std::format_error("Invalid path");
}
}

/// @brief Get the parts of the path as a range of string views.
constexpr std::vector<std::string_view> parts() const noexcept
{
size_t start = 1;
std::vector<std::string_view> parts;

for (size_t i = 1; i < path_.size(); i++)
{
if (path_[i] == '/')
{
parts.push_back(path_.substr(start, i - start));
start = i + 1;
}
}

if (start < path_.size())
{
parts.push_back(path_.substr(start));
}

return parts;
}

/// @brief Validates a path. A path must start with a forward slash and may
/// contain only the following characters:
///
/// - Lowercase letters (a-z)
///
/// - Uppercase letters (A-Z)
///
/// - Digits (0-9)
///
/// - Following special characters: - _ . ~ ! $ & ' ( ) * + , ; = : @ /
///
/// @param path The path to validate.
/// @return True if the path is valid; false otherwise.
static constexpr bool validate_path(std::string_view path)
{
if (path.empty())
return false;

if (path[0] != '/')
return false;

for (size_t i = 1; i < path.size(); ++i)
{
if ((path[i] < 'a' || path[i] > 'z') &&
(path[i] < 'A' || path[i] > 'Z') &&
(path[i] < '0' || path[i] > '9') &&
path[i] != '-' &&
path[i] != '_' &&
path[i] != '.' &&
path[i] != '~' &&
path[i] != '!' &&
path[i] != '$' &&
path[i] != '&' &&
path[i] != '\'' &&
path[i] != '(' &&
path[i] != ')' &&
path[i] != '*' &&
path[i] != '+' &&
path[i] != ',' &&
path[i] != ';' &&
path[i] != '=' &&
path[i] != ':' &&
path[i] != '@' &&
path[i] != '/')
return false;
}

return true;
}

/// @brief Get the path as a string view.
constexpr auto get() const noexcept
{
return path_;
}

explicit(false) constexpr operator std::string_view() const noexcept
{
return path_;
}

private:
std::string_view path_;
};
}
Loading
Loading