flatten 20260225
This commit is contained in:
404
tjp/core/http/hirose/Server.cpp
Executable file
404
tjp/core/http/hirose/Server.cpp
Executable 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
9
tjp/core/http/hirose/Server.h
Executable 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
49
tjp/core/http/hirose/Server.hpp
Executable 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
|
||||
356
tjp/core/http/hirose/_test/Server.cpp
Executable file
356
tjp/core/http/hirose/_test/Server.cpp
Executable 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
|
||||
12067
tjp/core/http/hirose/httplib-0_27_0.h
Normal file
12067
tjp/core/http/hirose/httplib-0_27_0.h
Normal file
File diff suppressed because it is too large
Load Diff
3
tjp/core/http/hirose/httplib.h
Normal file
3
tjp/core/http/hirose/httplib.h
Normal file
@@ -0,0 +1,3 @@
|
||||
// TJP COPYRIGHT HEADER
|
||||
|
||||
#include "httplib-0_27_0.h"
|
||||
36
tjp/core/http/hirose/write_result.hpp
Executable file
36
tjp/core/http/hirose/write_result.hpp
Executable 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;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user