flatten 20260225

This commit is contained in:
Timothy Prepscius
2026-02-25 12:39:24 -05:00
commit fa54be052a
315 changed files with 49791 additions and 0 deletions

404
tjp/core/http/hirose/Server.cpp Executable file
View File

@@ -0,0 +1,404 @@
// TJP COPYRIGHT HEADER
#if defined(TJP_CORE_HEADER_ONLY)
#pragma once
#endif
#include <tjp/core/header_only/compile.h>
#include "Server.hpp"
#include <tjp/core/containers/Vector.h>
#include <tjp/core/containers/Optional.h>
#include <tjp/core/exception/Exception.hpp>
#include <tjp/core/algorithm/map_value.hpp>
#include <tjp/core/algorithm/remove_const_of_var.hpp>
#include "../../internet/Headers.hpp"
#include "httplib.h"
#include <condition_variable>
#include <deque>
#include <memory>
#include <mutex>
#include <string>
namespace tjp::core::http::hirose {
using ServerImp = ::httplib::Server;
using StreamState = http::PipeState;
struct Request : http::Request
{
httplib::Request *v;
Request(const httplib::Request &v) {
target = v.target;
method = v.method;
headers.reserve(v.headers.size());
for (auto &[k, v]: v.headers)
headers.emplace_back(HeaderView { k, v });
body = v.body;
};
} ;
struct Response : http::Response
{
httplib::Response *v;
String method;
bool headRequest = false;
std::shared_ptr<StreamState> streamState;
Optional<StringView> headerValue(const StringView &name) const
{
for (auto &header: headers)
if (header.key == name)
return header.value;
return {};
}
void writeHeaders(bool skipLength)
{
v->status = status;
for (auto &header: headers)
{
auto &key = header.key;
auto &value = header.value;
if (key == "Content-Type") {}
else
if (key == "Content-Length" && skipLength) {}
else
v->set_header(key, value);
}
}
void flushHeaders(const StringView &method) override
{
bool skipLength = method == "PUT" || method == "GET";
writeHeaders(skipLength);
}
void write(const StringView &data) override
{
auto type = headerValue("Content-Type").value_or("text/plain");
v->set_content(data.data(), data.size(), String(type));
}
std::shared_ptr<http::DataSink> stream() override
{
auto type = headerValue("Content-Type").value_or("text/plain");
Optional<size_t> size_;
if (auto contentLength = headerValue("Content-Length"))
{
try
{
size_ = static_cast<size_t>(std::stoull(String(*contentLength)));
}
catch (...)
{
// ignore malformed values
}
}
if (!size_ && streamState)
size_ = streamState->size();
if (!streamState)
streamState = StreamState::make(size_);
auto state = streamState;
if (headRequest)
{
if (size_)
{
v->set_content_provider(
*size_,
String(type),
[](size_t, size_t, httplib::DataSink &) {
return false;
}
);
}
else
{
v->set_content_provider(
String(type),
[](size_t, httplib::DataSink &) {
return false;
}
);
}
return state->sink();
}
if (size_)
{
v->set_content_provider(
*size_,
String(type),
[state](size_t, size_t, httplib::DataSink &sink_) {
String chunk;
bool ended = false;
if (state->popOrWait(chunk, ended))
{
sink_.write(chunk.data(), chunk.size());
return true;
}
if (ended)
{
// For fixed-length providers, returning true without writing
// bytes keeps offset unchanged and can spin the write loop.
return false;
}
return false;
},
[state](bool) {
if (auto sink = state->sink())
sink->done();
}
);
}
else
{
v->set_chunked_content_provider(
String(type),
[state](size_t, httplib::DataSink &sink_) {
String chunk;
bool ended = false;
if (state->popOrWait(chunk, ended))
{
sink_.write(chunk.data(), chunk.size());
return true;
}
if (ended)
{
if (sink_.done)
sink_.done();
return true;
}
return false;
},
[state](bool) {
if (auto sink = state->sink())
sink->done();
}
);
}
return state->sink();
}
} ;
struct Server::I
{
struct Route
{
Method method;
Matcher matcher;
FutureHandler handler;
} ;
ServerImp webServer;
std::thread thread;
bool running = false;
Optional<size_t> numThreads;
std::mutex routesMutex;
Vector<Route> routes;
Vector<StrongPtr<http::Component>> components;
bool findHandler(
Request &request,
FutureHandler &out
)
{
std::lock_guard<std::mutex> lock(routesMutex);
for (const auto &route: routes)
{
if (route.method != request.method)
continue;
if (route.matcher)
if (auto matches = route.matcher(request.target))
{
request.matches = *matches;
out = route.handler;
return true;
}
}
return false;
}
};
static void applyResult(Response &response, Result result)
{
response.status = result.status.value_or(200);
for (auto &[key, value]: result.headers)
response.header(key, value);
if (result.type)
response.header("Content-Type", *result.type);
if (result.streamSize && !response.hasHeader("Content-Length"))
response.content_length(*result.streamSize);
if (result.streamOwner)
{
response.streamState = std::move(result.streamOwner);
response.stream();
return;
}
if (result.stream)
{
response.status = 500;
response.header("Content-Type", "text/plain");
response.write("Invalid stream owner");
return;
}
response.flushHeaders(response.method);
response.write(result.body.value_or(String {}));
}
TJP_CORE_HEADER_ONLY_INLINE
Server::Server()
{
i = strong<I>();
auto dispatch = [state = i.get()](const httplib::Request &request_, httplib::Response &response_) {
Request request(request_);
if (request_.method == "HEAD")
core::remove_const_of_var(request_.ranges).clear();
Response response;
response.v = &response_;
response.method = request_.method;
response.headRequest = request_.method == "HEAD";
try
{
FutureHandler handler;
if (state->findHandler(request, handler))
applyResult(response, handler(request).get());
else
{
response.status = 404;
response.header("Content-Type", "text/plain");
response.write("Not found");
}
}
catch (Result &result)
{
applyResult(response, Result(result));
}
catch (Exception &e)
{
response.status = 400;
response.header("Content-Type", "text/plain");
response.write("Error: " + e.what());
}
catch (...)
{
response.status = 400;
response.header("Content-Type", "text/plain");
response.write("Error");
}
};
i->webServer.Get(".*", dispatch);
i->webServer.Post(".*", dispatch);
i->webServer.Put(".*", dispatch);
i->webServer.Options(".*", dispatch);
}
TJP_CORE_HEADER_ONLY_INLINE
Server::~Server()
{
stop();
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::add(const StrongPtr<Component> &component, const String &urlPrefix)
{
component->use(*this, urlPrefix);
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::run(const String &bindTo, int port)
{
if (i->numThreads)
i->webServer.new_task_queue = [n = *i->numThreads] { return new httplib::ThreadPool(n); };
i->webServer.listen(bindTo, port);
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::setNumThreads(size_t n)
{
i->numThreads = n;
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::start(const String &bindTo, int port)
{
if (i->running)
throw Exception { "Webserver already running" };
i->running = true;
i->thread = std::thread([this, bindTo, port]() {
run(bindTo, port);
});
}
TJP_CORE_HEADER_ONLY_INLINE
bool Server::isRunning ()
{
return i->webServer.is_running();
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::stop()
{
if (!i->running)
return;
i->webServer.stop();
if (i->thread.joinable())
i->thread.join();
i->running = false;
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::bind(const Method &method, Matcher &&matcher, FutureHandler &&handler)
{
if (!matcher)
throw Exception { "Invalid route matcher" };
std::lock_guard<std::mutex> lock(i->routesMutex);
i->routes.emplace_back(I::Route {
method,
std::move(matcher),
std::move(handler)
});
}
} // namespace

9
tjp/core/http/hirose/Server.h Executable file
View File

@@ -0,0 +1,9 @@
// TJP COPYRIGHT HEADER
#pragma once
namespace tjp::core::http::hirose {
struct Server;
} // namespace

49
tjp/core/http/hirose/Server.hpp Executable file
View File

@@ -0,0 +1,49 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Server.h"
#include "../Server.hpp"
#include "../Component.hpp"
#include <tjp/core/header_only/compile.h>
#include <tjp/core/ptr/Ptr.h>
#include <tjp/core/string/String.h>
namespace tjp::core::http::hirose {
struct Server : http::Server
{
using Super = http::Server;
using Super::bind;
struct I;
StrongPtr<I> i;
Server();
~Server();
void add(
const StrongPtr<Component> &component,
const String &urlPrefix = {}
) override;
void setNumThreads(size_t) override;
void start(const String &bindTo, int port) override;
void run(const String &bindTo, int port) override;
void stop() override;
bool isRunning() override;
void bind(
const Method &method,
Matcher &&matcher,
FutureHandler &&
) override;
};
} // namespace
#ifdef TJP_CORE_HEADER_ONLY
#include "Server.cpp"
#endif

View File

@@ -0,0 +1,356 @@
// TJP COPYRIGHT HEADER
#include "../Server.hpp"
#include "../httplib.h"
#include <tjp/core/testing/catch.hpp>
#include <tjp/core/testing/wait_until.h>
#include <tjp/core/internet/Headers.hpp>
#include <tjp/core/hash/Hash.h>
#include <tjp/core/types/Types.h>
#include <algorithm>
#include <thread>
namespace tjp::core::http::hirose {
SCENARIO("tjp::core::http::hirose")
{
GIVEN("server")
{
Server server;
auto make_large_payload = []() {
String data;
data.reserve(64 * 1024);
for (size_t i = 0; i < (64 * 1024); ++i)
data.push_back(static_cast<char>('a' + (i % 26)));
return data;
};
auto large_payload = make_large_payload();
server.bind_with("GET", "/hello", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "hello";
return result;
});
server.bind_with("POST", "/echo", [](const Request &req) -> Result {
Result result;
result.status = 201;
result.type = "text/plain";
result.body = String(req.body);
return result;
});
server.bind_with("PUT", "/replace", [](const Request &req) -> Result {
Result result;
result.type = "text/plain";
result.body = "updated:" + String(req.body);
return result;
});
server.bind_with("GET", "/range", [](const Request &req) -> Result
{
Result result;
String full = "0123456789";
String send;
size_t begin = 0;
size_t end = full.size() - 1;
if (auto range_ = req.header("range"))
if (auto range = core::internet::parseRange(String(*range_)))
{
auto &[begin_, end_] = *range;
begin = begin_;
end = end_;
send = full.substr(begin, (end - begin + 1));
}
result.headers.push_back(Header { "Content-Range", core::internet::makeContentRange({ begin, end, full.size() }) });
result.type = "text/plain";
result.body = send;
return result;
});
server.bind_with("GET", match_exact("/match/exact"), [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "exact";
return result;
});
server.bind_with("GET", match_prefix("/match/prefix"), [](const Request &req) -> Result {
Result result;
result.type = "text/plain";
result.body = "prefix:" + String(req.target);
return result;
});
server.bind_with("GET", match_regex("^/match/regex/[0-9]+$"), [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "regex";
return result;
});
server.bind_with("GET",
[](StringView url) -> Optional<Matches> {
return url == "/match/lambda" ? Matches() : Optional<Matches>();
},
[](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "lambda";
return result;
});
server.bind_with("GET", "/stream-known", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(6);
out->write("signal", 6);
out->done();
return result;
});
server.bind_with("GET", "/stream-unknown", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body();
out->write("sig", 3);
out->write("nal", 3);
out->done();
return result;
});
server.bind_with("HEAD", "/stream-head-known", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(6);
out->write("ABCDEF", 6);
out->done();
return result;
});
server.bind_with("HEAD", "/stream-head-unknown", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body();
out->write("ignored", 7);
out->done();
return result;
});
server.bind_with("GET", "/stream-large", [large_payload](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(large_payload.size());
const size_t chunk = 8192;
for (size_t offset = 0; offset < large_payload.size(); offset += chunk)
{
auto size = std::min(chunk, large_payload.size() - offset);
out->write(large_payload.data() + offset, size);
}
out->done();
return result;
});
server.bind_with("GET", "/future-stream-known", [](const Request &) -> Result {
Result result;
result.status = 200;
result.type = "text/plain";
auto out = result.stream_body(6);
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("nal", 3);
out->done();
}).detach();
return result;
});
server.bind("GET", "/future-stream-unknown", FutureHandler([](const Request &) {
Result result;
result.status = 200;
result.type = "text/plain";
auto out = result.stream_body();
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(15));
out->write("nal", 3);
out->done();
}).detach();
return future_with([result]() {
return result;
});
}));
server.start("127.0.0.1", 4790);
core::wait_until(std::chrono::seconds(10), [&]() { return server.isRunning();});
WHEN("a get is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/hello");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "hello");
}
WHEN("a post is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Post("/echo", "posted-body", "text/plain");
REQUIRE(bool(contents));
REQUIRE(contents->status == 201);
REQUIRE(contents->body == "posted-body");
}
WHEN("a put is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Put("/replace", "payload", "text/plain");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "updated:payload");
}
WHEN("a range request is made")
{
httplib::Client client("http://127.0.0.1:4790");
httplib::Headers headers;
headers.emplace("Range", "bytes=2-5");
auto contents = client.Get("/range", headers);
REQUIRE(bool(contents));
// REQUIRE(contents->status == 206);
REQUIRE(contents->get_header_value("Content-Range") == "bytes 2-5/10");
REQUIRE(contents->body == "2345");
}
WHEN("an exact matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/match/exact");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "exact");
}
WHEN("a prefix matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/match/prefix/child");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "prefix:/match/prefix/child");
}
WHEN("a regex matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/match/regex/42");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "regex");
}
WHEN("a lambda matcher route is requested")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/match/lambda");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "lambda");
}
WHEN("a known-sized stream get is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/stream-known");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length") == "6");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("an unknown-sized stream get is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/stream-unknown");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length").empty());
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
}
WHEN("a head of known-sized stream is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Head("/stream-head-known");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Length") == "6");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a head of unknown-sized stream is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Head("/stream-head-unknown");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Length").empty());
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a large stream get is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/stream-large");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.size() > 32 * 1024);
REQUIRE(contents->body.size() == large_payload.size());
auto body_hash = hash_of<u64>(contents->body);
auto expected_hash = hash_of<u64>(large_payload);
REQUIRE(body_hash == expected_hash);
REQUIRE(contents->get_header_value("Content-Length") == std::to_string(large_payload.size()));
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a future-backed known-size stream get is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/future-stream-known");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length") == "6");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a future-backed unknown-size stream get is made")
{
httplib::Client client("http://127.0.0.1:4790");
auto contents = client.Get("/future-stream-unknown");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "signal");
REQUIRE(contents->get_header_value("Content-Length").empty());
REQUIRE(contents->get_header_value("Transfer-Encoding") == "chunked");
}
server.stop();
}
}
} // namespace

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,3 @@
// TJP COPYRIGHT HEADER
#include "httplib-0_27_0.h"

View File

@@ -0,0 +1,36 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "http.hpp"
#include <tjp/core/exception/Exception.hpp>
//namespace tjp::core::http::hirose {}
namespace tjp::core::http::hirose {
template<typename F>
auto write_result(F &&f)
{
return
[f=std::move(f)](const Request& req, Response& res)
{
try
{
auto [body, type] = f(req);
res.set_content(body, type);
}
catch (Exception &e)
{
res.set_content("Error: " + e.what(), "text/plain");
res.status = 400;
}
catch (...)
{
res.set_content("Error", "text/plain");
res.status = 400;
}
};
}
}