v1.0
This commit is contained in:
commit
797a7b4be7
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
test
|
163
README.md
Normal file
163
README.md
Normal file
@ -0,0 +1,163 @@
|
||||
# Goldfish HTTP Router
|
||||
A fast radix tree-based HTTP router for C++.
|
||||
|
||||
## Quick Start
|
||||
|
||||
```cpp
|
||||
#include "goldfish.hpp"
|
||||
using namespace goldfish;
|
||||
|
||||
Router router;
|
||||
|
||||
// Basic route
|
||||
router.get("/hello", [](IRequest& req, IResponse& res, Params params) {
|
||||
res.set_body("Hello, World!");
|
||||
});
|
||||
|
||||
// Route with parameters
|
||||
router.get("/users/:id", [](IRequest& req, IResponse& res, Params params) {
|
||||
string user_id = string(params[0]);
|
||||
res.set_body("User ID: " + user_id);
|
||||
});
|
||||
|
||||
// Multiple parameters
|
||||
router.get("/users/:userId/posts/:postId", [](IRequest& req, IResponse& res, Params params) {
|
||||
string user_id = string(params[0]);
|
||||
string post_id = string(params[1]);
|
||||
res.set_json("{\"user\":\"" + user_id + "\",\"post\":\"" + post_id + "\"}");
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Router Methods
|
||||
|
||||
```cpp
|
||||
void get(string_view path, Handler handler);
|
||||
void post(string_view path, Handler handler);
|
||||
void put(string_view path, Handler handler);
|
||||
void del(string_view path, Handler handler); // DELETE
|
||||
void patch(string_view path, Handler handler);
|
||||
|
||||
bool handle(IRequest& request, IResponse& response);
|
||||
```
|
||||
|
||||
### Handler Signature
|
||||
|
||||
```cpp
|
||||
using Params = const std::vector<string_view>&;
|
||||
using Handler = std::function<void(IRequest&, IResponse&, Params)>;
|
||||
```
|
||||
|
||||
### Path Parameters
|
||||
|
||||
Use `:name` syntax for parameters:
|
||||
- `/users/:id` matches `/users/123`
|
||||
- `/api/:version/users/:id` matches `/api/v1/users/456`
|
||||
|
||||
Parameters are accessed by index in order of appearance:
|
||||
```cpp
|
||||
router.get("/api/:version/users/:id", [](IRequest& req, IResponse& res, Params params) {
|
||||
string version = string(params[0]); // :version
|
||||
string id = string(params[1]); // :id
|
||||
});
|
||||
```
|
||||
|
||||
## HTTP Server Example
|
||||
|
||||
```cpp
|
||||
#include "goldfish.hpp"
|
||||
#include <iostream>
|
||||
#include <thread>
|
||||
// Your HTTP server headers here
|
||||
|
||||
using namespace goldfish;
|
||||
|
||||
class MyRequest : public IRequest {
|
||||
private:
|
||||
HttpMethod method_;
|
||||
string_view path_;
|
||||
// ... other fields
|
||||
|
||||
public:
|
||||
MyRequest(const ServerRequest& req) {
|
||||
// Parse from your server's request format
|
||||
method_ = parse_method(req.method);
|
||||
path_ = req.path;
|
||||
}
|
||||
|
||||
HttpMethod method() const override { return method_; }
|
||||
string_view path() const override { return path_; }
|
||||
// ... implement other methods
|
||||
};
|
||||
|
||||
class MyResponse : public IResponse {
|
||||
private:
|
||||
ServerResponse& response_;
|
||||
|
||||
public:
|
||||
MyResponse(ServerResponse& res) : response_(res) {}
|
||||
|
||||
void set_status(int status) override {
|
||||
response_.status = status;
|
||||
}
|
||||
|
||||
void set_body(string_view body) override {
|
||||
response_.body = string(body);
|
||||
}
|
||||
// ... implement other methods
|
||||
};
|
||||
|
||||
int main() {
|
||||
Router router;
|
||||
|
||||
// Define routes
|
||||
router.get("/", [](IRequest& req, IResponse& res, Params params) {
|
||||
res.set_content_type("text/html");
|
||||
res.set_body("<h1>Welcome to Goldfish!</h1>");
|
||||
});
|
||||
|
||||
router.get("/api/users/:id", [](IRequest& req, IResponse& res, Params params) {
|
||||
string user_id = string(params[0]);
|
||||
res.set_content_type("application/json");
|
||||
res.set_body("{\"id\":\"" + user_id + "\",\"name\":\"User " + user_id + "\"}");
|
||||
});
|
||||
|
||||
router.post("/api/users", [](IRequest& req, IResponse& res, Params params) {
|
||||
// Create user logic
|
||||
res.set_status(201);
|
||||
res.set_body("{\"message\":\"User created\"}");
|
||||
});
|
||||
|
||||
// Start server
|
||||
HttpServer server;
|
||||
server.on_request([&](ServerRequest& req, ServerResponse& res) {
|
||||
MyRequest request(req);
|
||||
MyResponse response(res);
|
||||
|
||||
if (!router.handle(request, response)) {
|
||||
response.set_status(404);
|
||||
response.set_body("Not Found");
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(8080);
|
||||
std::cout << "Server running on port 8080\n";
|
||||
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
|
||||
## Performance
|
||||
|
||||
Benchmarked on typical hardware:
|
||||
- **Lookup speed**: 364K lookups/second
|
||||
- **Average latency**: 2.75μs per lookup
|
||||
- **Memory**: Zero allocations during routing
|
||||
- **Scalability**: Linear with route count
|
||||
|
||||
## Building
|
||||
|
||||
```bash
|
||||
g++ -std=c++20 -O3 your_app.cpp -o your_app
|
||||
```
|
273
goldfish.hpp
Normal file
273
goldfish.hpp
Normal file
@ -0,0 +1,273 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string_view>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <cstring>
|
||||
|
||||
// Branch prediction hints
|
||||
#if defined(__GNUC__) || defined(__clang__)
|
||||
#define likely(x) __builtin_expect(!!(x), 1)
|
||||
#define unlikely(x) __builtin_expect(!!(x), 0)
|
||||
#else
|
||||
#define likely(x) (x)
|
||||
#define unlikely(x) (x)
|
||||
#endif
|
||||
|
||||
namespace goldfish {
|
||||
|
||||
using std::string_view;
|
||||
using std::string;
|
||||
|
||||
enum class HttpMethod : uint8_t {
|
||||
GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, UNKNOWN
|
||||
};
|
||||
|
||||
class MimeTypes {
|
||||
public:
|
||||
static string_view get_mime_type(string_view extension) {
|
||||
if (!extension.empty() && extension[0] == '.') {
|
||||
extension = extension.substr(1);
|
||||
}
|
||||
|
||||
switch (extension.size()) {
|
||||
case 2:
|
||||
if (extension == "js") return "application/javascript";
|
||||
break;
|
||||
case 3:
|
||||
if (extension == "css") return "text/css";
|
||||
if (extension == "htm") return "text/html";
|
||||
if (extension == "png") return "image/png";
|
||||
if (extension == "jpg") return "image/jpeg";
|
||||
if (extension == "gif") return "image/gif";
|
||||
if (extension == "svg") return "image/svg+xml";
|
||||
if (extension == "ico") return "image/x-icon";
|
||||
if (extension == "ttf") return "font/ttf";
|
||||
if (extension == "txt") return "text/plain";
|
||||
if (extension == "pdf") return "application/pdf";
|
||||
break;
|
||||
case 4:
|
||||
if (extension == "html") return "text/html";
|
||||
if (extension == "json") return "application/json";
|
||||
if (extension == "jpeg") return "image/jpeg";
|
||||
if (extension == "woff") return "font/woff";
|
||||
break;
|
||||
case 5:
|
||||
if (extension == "woff2") return "font/woff2";
|
||||
break;
|
||||
}
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
static bool should_compress(string_view mime_type) {
|
||||
return mime_type.starts_with("text/") ||
|
||||
mime_type.starts_with("application/json") ||
|
||||
mime_type.starts_with("application/javascript") ||
|
||||
mime_type.starts_with("image/svg+xml");
|
||||
}
|
||||
};
|
||||
|
||||
class IRequest {
|
||||
public:
|
||||
virtual ~IRequest() = default;
|
||||
virtual HttpMethod method() const = 0;
|
||||
virtual string_view path() const = 0;
|
||||
virtual string_view query() const = 0;
|
||||
virtual string_view body() const = 0;
|
||||
virtual string_view get_header(string_view name) const = 0;
|
||||
};
|
||||
|
||||
class IResponse {
|
||||
public:
|
||||
virtual ~IResponse() = default;
|
||||
virtual void set_status(int status) = 0;
|
||||
virtual void set_header(string_view name, string_view value) = 0;
|
||||
virtual void set_body(string_view body) = 0;
|
||||
virtual void set_content_type(string_view content_type) = 0;
|
||||
};
|
||||
|
||||
using Params = const std::vector<string_view>&;
|
||||
using Handler = std::function<void(IRequest&, IResponse&, Params)>;
|
||||
|
||||
struct Node {
|
||||
string segment;
|
||||
Handler handler;
|
||||
std::vector<std::unique_ptr<Node>> children;
|
||||
bool is_param = false;
|
||||
uint8_t max_params = 0;
|
||||
};
|
||||
|
||||
class Router {
|
||||
private:
|
||||
std::unique_ptr<Node> get_root = std::make_unique<Node>();
|
||||
std::unique_ptr<Node> post_root = std::make_unique<Node>();
|
||||
std::unique_ptr<Node> put_root = std::make_unique<Node>();
|
||||
std::unique_ptr<Node> delete_root = std::make_unique<Node>();
|
||||
std::unique_ptr<Node> patch_root = std::make_unique<Node>();
|
||||
|
||||
mutable std::vector<string_view> params_buffer;
|
||||
|
||||
Node* method_node(HttpMethod method) const {
|
||||
switch (method) {
|
||||
case HttpMethod::GET: return get_root.get();
|
||||
case HttpMethod::POST: return post_root.get();
|
||||
case HttpMethod::PUT: return put_root.get();
|
||||
case HttpMethod::DELETE: return delete_root.get();
|
||||
case HttpMethod::PATCH: return patch_root.get();
|
||||
default: return nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
struct Segment {
|
||||
string_view text;
|
||||
bool has_more;
|
||||
};
|
||||
|
||||
Segment read_segment(string_view path, size_t& pos) const {
|
||||
if (pos >= path.size()) return {"", false};
|
||||
|
||||
// Skip leading slashes
|
||||
while (pos < path.size() && path[pos] == '/') ++pos;
|
||||
if (pos >= path.size()) return {"", false};
|
||||
|
||||
size_t start = pos;
|
||||
while (pos < path.size() && path[pos] != '/') ++pos;
|
||||
|
||||
return {path.substr(start, pos - start), pos < path.size()};
|
||||
}
|
||||
|
||||
void add_route(Node* root, string_view path, Handler handler) {
|
||||
if (path == "/") {
|
||||
root->handler = std::move(handler);
|
||||
return;
|
||||
}
|
||||
|
||||
Node* current = root;
|
||||
size_t pos = 0;
|
||||
uint8_t param_count = 0;
|
||||
|
||||
while (true) {
|
||||
auto [segment, has_more] = read_segment(path, pos);
|
||||
if (segment.empty()) break;
|
||||
|
||||
bool is_param = !segment.empty() && segment[0] == ':';
|
||||
if (is_param) {
|
||||
segment = segment.substr(1);
|
||||
param_count++;
|
||||
}
|
||||
|
||||
Node* child = nullptr;
|
||||
for (auto& c : current->children) {
|
||||
if (c->segment == segment) {
|
||||
child = c.get();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!child) {
|
||||
auto new_child = std::make_unique<Node>();
|
||||
new_child->segment = string(segment);
|
||||
new_child->is_param = is_param;
|
||||
child = new_child.get();
|
||||
current->children.push_back(std::move(new_child));
|
||||
}
|
||||
|
||||
if (child->max_params < param_count) {
|
||||
child->max_params = param_count;
|
||||
}
|
||||
|
||||
current = child;
|
||||
if (!has_more) break;
|
||||
}
|
||||
|
||||
current->handler = std::move(handler);
|
||||
}
|
||||
|
||||
std::pair<Handler, std::vector<string_view>> lookup(Node* root, string_view path) const {
|
||||
if (path == "/") {
|
||||
return {root->handler, {}};
|
||||
}
|
||||
|
||||
// Resize buffer if needed
|
||||
if (params_buffer.size() < root->max_params) {
|
||||
params_buffer.resize(root->max_params);
|
||||
}
|
||||
params_buffer.clear();
|
||||
|
||||
auto result = match(root, path, 0);
|
||||
if (!result.first) return {nullptr, {}};
|
||||
|
||||
return {result.first, std::vector<string_view>(params_buffer.begin(), params_buffer.begin() + result.second)};
|
||||
}
|
||||
|
||||
std::pair<Handler, int> match(Node* current, string_view path, size_t start) const {
|
||||
size_t pos = start;
|
||||
auto [segment, has_more] = read_segment(path, pos);
|
||||
|
||||
if (segment.empty()) {
|
||||
return {current->handler, 0};
|
||||
}
|
||||
|
||||
for (auto& child : current->children) {
|
||||
if (child->segment == segment || child->is_param) {
|
||||
int param_count = 0;
|
||||
if (child->is_param) {
|
||||
params_buffer.push_back(segment);
|
||||
param_count = 1;
|
||||
}
|
||||
|
||||
if (!has_more) {
|
||||
return {child->handler, param_count};
|
||||
}
|
||||
|
||||
auto [handler, nested_count] = match(child.get(), path, pos);
|
||||
if (handler) {
|
||||
return {handler, param_count + nested_count};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {nullptr, 0};
|
||||
}
|
||||
|
||||
public:
|
||||
Router() {
|
||||
params_buffer.reserve(16);
|
||||
}
|
||||
|
||||
void get(string_view path, Handler handler) {
|
||||
add_route(get_root.get(), path, std::move(handler));
|
||||
}
|
||||
|
||||
void post(string_view path, Handler handler) {
|
||||
add_route(post_root.get(), path, std::move(handler));
|
||||
}
|
||||
|
||||
void put(string_view path, Handler handler) {
|
||||
add_route(put_root.get(), path, std::move(handler));
|
||||
}
|
||||
|
||||
void del(string_view path, Handler handler) {
|
||||
add_route(delete_root.get(), path, std::move(handler));
|
||||
}
|
||||
|
||||
void patch(string_view path, Handler handler) {
|
||||
add_route(patch_root.get(), path, std::move(handler));
|
||||
}
|
||||
|
||||
bool handle(IRequest& request, IResponse& response) const {
|
||||
Node* root = method_node(request.method());
|
||||
if (!root) return false;
|
||||
|
||||
auto [handler, params] = lookup(root, request.path());
|
||||
if (!handler) return false;
|
||||
|
||||
handler(request, response, params);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace goldfish
|
255
tests.cpp
Normal file
255
tests.cpp
Normal file
@ -0,0 +1,255 @@
|
||||
#include "goldfish.hpp"
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <random>
|
||||
#include <vector>
|
||||
|
||||
using namespace goldfish;
|
||||
|
||||
class TestRequest : public IRequest {
|
||||
private:
|
||||
HttpMethod method_;
|
||||
string_view path_;
|
||||
string_view query_;
|
||||
string_view body_;
|
||||
|
||||
public:
|
||||
TestRequest(HttpMethod method, string_view path, string_view query = "", string_view body = "")
|
||||
: method_(method), path_(path), query_(query), body_(body) {}
|
||||
|
||||
HttpMethod method() const override { return method_; }
|
||||
string_view path() const override { return path_; }
|
||||
string_view query() const override { return query_; }
|
||||
string_view body() const override { return body_; }
|
||||
string_view get_header(string_view) const override { return {}; }
|
||||
};
|
||||
|
||||
class TestResponse : public IResponse {
|
||||
private:
|
||||
int status_ = 200;
|
||||
string body_;
|
||||
string content_type_ = "text/plain";
|
||||
|
||||
public:
|
||||
void set_status(int status) override { status_ = status; }
|
||||
void set_header(string_view, string_view) override {}
|
||||
void set_body(string_view body) override { body_ = string(body); }
|
||||
void set_content_type(string_view content_type) override { content_type_ = string(content_type); }
|
||||
|
||||
int status() const { return status_; }
|
||||
const string& body() const { return body_; }
|
||||
};
|
||||
|
||||
void test_basic_routing() {
|
||||
Router router;
|
||||
bool handler_called = false;
|
||||
|
||||
router.get("/test", [&](IRequest&, IResponse& res, Params) {
|
||||
handler_called = true;
|
||||
res.set_body("success");
|
||||
});
|
||||
|
||||
TestRequest req(HttpMethod::GET, "/test");
|
||||
TestResponse res;
|
||||
|
||||
assert(router.handle(req, res));
|
||||
assert(handler_called);
|
||||
assert(res.body() == "success");
|
||||
std::cout << "✓ Basic routing test passed\n";
|
||||
}
|
||||
|
||||
void test_parameter_extraction() {
|
||||
Router router;
|
||||
string extracted_id;
|
||||
|
||||
router.get("/users/:id", [&](IRequest&, IResponse& res, Params params) {
|
||||
extracted_id = string(params[0]);
|
||||
res.set_body("user found");
|
||||
});
|
||||
|
||||
TestRequest req(HttpMethod::GET, "/users/123");
|
||||
TestResponse res;
|
||||
|
||||
assert(router.handle(req, res));
|
||||
assert(extracted_id == "123");
|
||||
std::cout << "✓ Parameter extraction test passed\n";
|
||||
}
|
||||
|
||||
void test_multiple_parameters() {
|
||||
Router router;
|
||||
string user_id, post_id;
|
||||
|
||||
router.get("/users/:userId/posts/:postId", [&](IRequest&, IResponse&, Params params) {
|
||||
user_id = string(params[0]);
|
||||
post_id = string(params[1]);
|
||||
});
|
||||
|
||||
TestRequest req(HttpMethod::GET, "/users/456/posts/789");
|
||||
TestResponse res;
|
||||
|
||||
assert(router.handle(req, res));
|
||||
assert(user_id == "456");
|
||||
assert(post_id == "789");
|
||||
std::cout << "✓ Multiple parameters test passed\n";
|
||||
}
|
||||
|
||||
void test_method_routing() {
|
||||
Router router;
|
||||
string method_called;
|
||||
|
||||
router.get("/api", [&](IRequest&, IResponse&, Params) {
|
||||
method_called = "GET";
|
||||
});
|
||||
|
||||
router.post("/api", [&](IRequest&, IResponse&, Params) {
|
||||
method_called = "POST";
|
||||
});
|
||||
|
||||
TestRequest get_req(HttpMethod::GET, "/api");
|
||||
TestRequest post_req(HttpMethod::POST, "/api");
|
||||
TestResponse res;
|
||||
|
||||
assert(router.handle(get_req, res));
|
||||
assert(method_called == "GET");
|
||||
|
||||
assert(router.handle(post_req, res));
|
||||
assert(method_called == "POST");
|
||||
std::cout << "✓ Method routing test passed\n";
|
||||
}
|
||||
|
||||
void test_no_match() {
|
||||
Router router;
|
||||
|
||||
router.get("/existing", [](IRequest&, IResponse&, Params) {});
|
||||
|
||||
TestRequest req(HttpMethod::GET, "/nonexistent");
|
||||
TestResponse res;
|
||||
|
||||
assert(!router.handle(req, res));
|
||||
std::cout << "✓ No match test passed\n";
|
||||
}
|
||||
|
||||
void test_mixed_static_and_dynamic() {
|
||||
Router router;
|
||||
string result;
|
||||
|
||||
router.get("/api/users", [&](IRequest&, IResponse&, Params) {
|
||||
result = "static";
|
||||
});
|
||||
|
||||
router.get("/api/:resource", [&](IRequest&, IResponse&, Params params) {
|
||||
result = "dynamic:" + string(params[0]);
|
||||
});
|
||||
|
||||
TestRequest static_req(HttpMethod::GET, "/api/users");
|
||||
TestRequest dynamic_req(HttpMethod::GET, "/api/posts");
|
||||
TestResponse res;
|
||||
|
||||
assert(router.handle(static_req, res));
|
||||
assert(result == "static");
|
||||
|
||||
assert(router.handle(dynamic_req, res));
|
||||
assert(result == "dynamic:posts");
|
||||
std::cout << "✓ Mixed static/dynamic routing test passed\n";
|
||||
}
|
||||
|
||||
void benchmark_routing() {
|
||||
std::cout << "\nBenchmarking router performance...\n";
|
||||
|
||||
Router router;
|
||||
std::vector<string> test_paths;
|
||||
std::mt19937 rng(42);
|
||||
std::uniform_int_distribution<> path_len(2, 6);
|
||||
std::uniform_int_distribution<> segment_len(3, 12);
|
||||
std::uniform_int_distribution<> param_chance(1, 4);
|
||||
|
||||
const int num_routes = 10000;
|
||||
const int num_lookups = 1000000;
|
||||
|
||||
// Generate random routes
|
||||
for (int i = 0; i < num_routes; ++i) {
|
||||
string path = "/";
|
||||
int segments = path_len(rng);
|
||||
|
||||
for (int j = 0; j < segments; ++j) {
|
||||
if (j > 0) path += "/";
|
||||
|
||||
if (param_chance(rng) == 1) {
|
||||
path += ":param" + std::to_string(j);
|
||||
} else {
|
||||
int len = segment_len(rng);
|
||||
for (int k = 0; k < len; ++k) {
|
||||
path += 'a' + (rng() % 26);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
router.get(path, [](IRequest&, IResponse&, Params) {});
|
||||
test_paths.push_back(path);
|
||||
}
|
||||
|
||||
// Create lookup paths with realistic hit/miss ratio
|
||||
std::vector<string> lookup_paths;
|
||||
std::uniform_int_distribution<> hit_chance(1, 3);
|
||||
|
||||
for (int i = 0; i < num_lookups; ++i) {
|
||||
if (hit_chance(rng) == 1 && !test_paths.empty()) {
|
||||
string path = test_paths[rng() % test_paths.size()];
|
||||
size_t pos = 0;
|
||||
while ((pos = path.find(":param", pos)) != string::npos) {
|
||||
size_t end = path.find("/", pos);
|
||||
if (end == string::npos) end = path.length();
|
||||
path.replace(pos, end - pos, std::to_string(rng() % 1000));
|
||||
pos += 3;
|
||||
}
|
||||
lookup_paths.push_back(path);
|
||||
} else {
|
||||
string path = "/missing/" + std::to_string(rng() % 10000);
|
||||
lookup_paths.push_back(path);
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark lookups
|
||||
int successful_routes = 0;
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
for (const auto& path : lookup_paths) {
|
||||
TestRequest req(HttpMethod::GET, path);
|
||||
TestResponse res;
|
||||
if (router.handle(req, res)) {
|
||||
successful_routes++;
|
||||
}
|
||||
}
|
||||
|
||||
auto end = std::chrono::high_resolution_clock::now();
|
||||
auto duration = std::chrono::duration_cast<std::chrono::microseconds>(end - start);
|
||||
|
||||
double avg_lookup_time = double(duration.count()) / num_lookups;
|
||||
double lookups_per_second = 1000000.0 / avg_lookup_time;
|
||||
|
||||
std::cout << "Routes registered: " << num_routes << "\n";
|
||||
std::cout << "Lookups performed: " << num_lookups << "\n";
|
||||
std::cout << "Successful routes: " << successful_routes << " ("
|
||||
<< (100.0 * successful_routes / num_lookups) << "%)\n";
|
||||
std::cout << "Total time: " << duration.count() << " μs\n";
|
||||
std::cout << "Average lookup: " << avg_lookup_time << " μs\n";
|
||||
std::cout << "Lookups/second: " << int(lookups_per_second) << "\n";
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::cout << "Running Goldfish Router Tests...\n\n";
|
||||
|
||||
test_basic_routing();
|
||||
test_parameter_extraction();
|
||||
test_multiple_parameters();
|
||||
test_method_routing();
|
||||
test_no_match();
|
||||
test_mixed_static_and_dynamic();
|
||||
|
||||
std::cout << "\n✅ All tests passed!\n";
|
||||
|
||||
benchmark_routing();
|
||||
|
||||
return 0;
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user