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

9
tjp/core/http/Component.h Executable file
View File

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

25
tjp/core/http/Component.hpp Executable file
View File

@@ -0,0 +1,25 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Component.h"
#include "Method.h"
#include "Types.h"
#include "Server.h"
#include <tjp/core/string/String.hpp>
namespace tjp::core::http {
struct Component
{
using Server = http::Server;
using method = http::method;
virtual void use(Server &, const String &urlPrefix) = 0;
virtual void onBegin() {}
virtual void onEnd() {}
} ;
} // namespace

21
tjp/core/http/Method.h Executable file
View File

@@ -0,0 +1,21 @@
// TJP COPYRIGHT HEADER
#pragma once
#include <tjp/core/string/String.h>
namespace tjp::core::http {
using Method = String;
struct method
{
static constexpr auto GET = "GET";
static constexpr auto PUT = "PUT";
static constexpr auto POST = "POST";
static constexpr auto HEAD = "HEAD";
static constexpr auto INFO = "INFO";
static constexpr auto OPTIONS = "OPTIONS";
} ;
} // namespace

9
tjp/core/http/Method.hpp Executable file
View File

@@ -0,0 +1,9 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Method.h"
namespace tjp::core::http {
} // namespace

9
tjp/core/http/Server.cpp Executable file
View File

@@ -0,0 +1,9 @@
// TJP COPYRIGHT HEADER
#if defined(TJP_CORE_HEADER_ONLY)
#pragma once
#endif
namespace tjp::core::http {
}

10
tjp/core/http/Server.h Executable file
View File

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

96
tjp/core/http/Server.hpp Executable file
View File

@@ -0,0 +1,96 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Server.h"
#include "Component.h"
#include "Types.hpp"
#include <tjp/core/header_only/compile.h>
#include <tjp/core/ptr/Ptr.h>
#include <tjp/core/string/String.h>
#include <tjp/core/string/StringView.h>
#include <tjp/core/containers/Function.h>
#include "as_future.hpp"
namespace tjp::core::http {
struct Server
{
virtual ~Server() {}
virtual void add(
const StrongPtr<Component> &component,
const String &urlPrefix = {}
) = 0;
template<typename ... Args>
void addAll(Args && ...args)
{
(add(std::forward<Args>(args)), ...);
}
virtual void setNumThreads(size_t) = 0;
virtual void start(const String &bindTo, int port) = 0;
virtual void run(const String &bindTo, int port) = 0;
virtual void stop() = 0;
virtual bool isRunning() = 0;
virtual void bind(
const Method &method,
Matcher &&matcher,
FutureHandler &&
) = 0;
void bind(
const Method &method,
const Pattern &pattern,
FutureHandler &&handler
)
{
bind(
method,
match_regex(String(pattern)),
std::move(handler)
);
}
template<typename F>
void bind_with(
const Method &method,
Matcher matcher,
F &&f
)
{
bind(
method,
std::move(matcher),
as_future(
std::forward<F>(f)
)
);
}
template<typename F>
void bind_with(
const Method &method,
const Pattern &pattern,
F &&f
)
{
bind_with(
method,
match_regex(String(pattern)),
std::forward<F>(f)
);
}
};
} // namespace
#ifdef TJP_CORE_HEADER_ONLY
#include "Server.cpp"
#endif

43
tjp/core/http/Types.h Executable file
View File

@@ -0,0 +1,43 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Method.h"
#include <tjp/core/containers/StackArray.h>
#include <tjp/core/containers/Function.h>
#include <tjp/core/containers/Optional.h>
#include <tjp/core/containers/Vector.h>
#include <tjp/core/string/String.h>
#include <tjp/core/string/StringView.h>
#include <tjp/core/future/Future.h>
namespace tjp::core::http {
using Status = int;
struct DataSink;
struct PipeState;
struct HeaderView;
struct Header;
using Headers = Vector<Header>;
using Matches = StackArray<StringView, 4>;
struct Request;
struct Response;
struct Result;
using BodyType = Result;
using FutureHandler = core::Function<Future<Result>(const Request &)>;
using Matcher = core::Function<Optional<Matches>(StringView)>;
using Pattern = StringView;
Matcher match_exact(const String &value);
Matcher match_prefix(const String &prefix);
Matcher match_regex(const String &pattern);
} // namespace

385
tjp/core/http/Types.hpp Executable file
View File

@@ -0,0 +1,385 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.h"
#include "Method.hpp"
#include <tjp/core/header_only/compile.h>
#include <tjp/core/ptr/Ptr.h>
#include <tjp/core/string/String.h>
#include <tjp/core/string/StringView.h>
#include <tjp/core/containers/Function.h>
#include <tjp/core/containers/Optional.h>
#include <tjp/core/containers/Map.h>
#include <tjp/core/containers/Tuple.h>
#include <tjp/core/containers/Vector.h>
#include <tjp/core/future/Future.h>
#include <tjp/core/containers/StackArray.hpp>
#include <tjp/core/string/cmp_case.hpp>
#include <any>
#include <condition_variable>
#include <deque>
#include <functional>
#include <memory>
#include <mutex>
#include <regex>
#include <tjp/core/string/starts_with.hpp>
#include "../internet/url_param_decode.hpp"
namespace tjp::core::http {
using Status = int;
struct DataSink
{
std::any preserve;
virtual ~DataSink() = default;
virtual void write(const char *data, size_t size) = 0;
virtual void done() = 0;
} ;
struct PipeState :
std::enable_shared_from_this<PipeState>
{
struct Sink : DataSink
{
std::weak_ptr<PipeState> state;
explicit Sink(std::weak_ptr<PipeState> state_) :
state(std::move(state_))
{}
void write(const char *data, size_t size) override
{
if (auto state_ = state.lock())
state_->onData(data, size);
}
void done() override
{
if (auto state_ = state.lock())
state_->onDone();
}
} ;
static std::shared_ptr<PipeState> make(Optional<size_t> requestSize = {})
{
auto state = std::shared_ptr<PipeState>(new PipeState(requestSize));
state->sink_ = std::make_shared<Sink>(state);
return state;
}
std::shared_ptr<DataSink> sink() const
{
return sink_;
}
void setWake(core::Function<void()> wake)
{
std::lock_guard<std::mutex> lock(mutex);
wake_ = std::move(wake);
}
std::shared_ptr<String> popChunk()
{
std::lock_guard<std::mutex> lock(mutex);
if (chunks.empty())
return {};
auto out = chunks.front();
chunks.pop_front();
return out;
}
bool popOrWait(String &chunk, bool &isEnded)
{
std::unique_lock<std::mutex> lock(mutex);
cv.wait(lock, [&]() {
return !chunks.empty() || ended;
});
if (!chunks.empty())
{
chunk = *chunks.front();
chunks.pop_front();
isEnded = false;
return true;
}
isEnded = ended;
return false;
}
bool isEnded() const
{
std::lock_guard<std::mutex> lock(mutex);
return ended;
}
Optional<size_t> size() const
{
std::lock_guard<std::mutex> lock(mutex);
return requestSize;
}
private:
explicit PipeState(Optional<size_t> requestSize_) :
requestSize(std::move(requestSize_))
{}
void onData(const char *data, size_t size)
{
if (size == 0)
return;
{
std::lock_guard<std::mutex> lock(mutex);
if (ended)
return;
chunks.emplace_back(std::make_shared<String>(data, size));
}
cv.notify_one();
notifyWake();
}
void onDone()
{
{
std::lock_guard<std::mutex> lock(mutex);
ended = true;
}
cv.notify_all();
notifyWake();
}
void notifyWake()
{
core::Function<void()> wake;
{
std::lock_guard<std::mutex> lock(mutex);
wake = wake_;
}
if (wake)
wake();
}
Optional<size_t> requestSize;
std::shared_ptr<Sink> sink_;
mutable std::mutex mutex;
std::condition_variable cv;
std::deque<std::shared_ptr<String>> chunks;
bool ended = false;
core::Function<void()> wake_;
} ;
struct HeaderView {
StringView key;
StringView value;
} ;
struct Header {
String key;
String value;
} ;
using Headers = Vector<Header>;
using Matches = StackArray<StringView, 4>;
inline
Optional<StringView> path_param_(const StringView &path, const StringView &param)
{
auto q = path.find('?');
if (q == path.npos)
return {};
auto l = q;
while (true)
{
auto p = path.find(param, l);
if (p == path.npos)
return {};
auto before = path[p-1];
if (before == '?' || before == '&')
{
// if the path ends with &param instead of &param=
if (path.size() <= p + param.size())
return { StringView() };
auto b = p + param.size();
auto after = path[b];
if (after == '=' || after == '&')
{
if (after == '=')
b++;
auto e = path.find('&', b);
if (e == path.npos)
e = path.size();
return path.substr(b, e - b);
}
}
l = p + param.size();
}
return {};
}
inline
Optional<String> path_param(const StringView &path, const StringView &param)
{
if (auto encoded_param = path_param_(path, param))
return internet::url_param_decode(*encoded_param);
return {};
}
struct Request
{
StringView method;
StringView target;
Vector<HeaderView> headers;
Matches matches;
StringView body;
Optional<StringView> header(const StringView &key_) const
{
for (auto &[key, value]: headers)
if (core::equal_case(key, key_))
return value;
return {};
}
Optional<String> param(const StringView &key_) const
{
return path_param(target, key_);
}
};
struct Response
{
Status status = 200;
Vector<Header> headers;
bool hasHeader(const StringView &key)
{
for (auto &[key_, _]: headers)
if (core::equal_case(key, key_))
return true;
return false;
}
void header(const String &key, const String &value)
{
headers.emplace_back(Header {key, value});
}
void content_length(size_t size)
{
header("Content-Length", std::to_string(size));
}
virtual void flushHeaders(const StringView &method) = 0;
virtual void write(const StringView &) = 0;
virtual std::shared_ptr<DataSink> stream() = 0;
std::shared_ptr<DataSink> stream(size_t size)
{
content_length(size);
return stream();
}
} ;
struct Result
{
Optional<String> body;
Optional<String> type;
Optional<Status> status;
Vector<Header> headers;
std::shared_ptr<DataSink> stream;
std::shared_ptr<PipeState> streamOwner;
Optional<size_t> streamSize;
std::shared_ptr<DataSink> stream_body()
{
streamOwner = PipeState::make();
streamSize = {};
stream = streamOwner->sink();
return stream;
}
std::shared_ptr<DataSink> stream_body(size_t size)
{
streamOwner = PipeState::make(size);
streamSize = size;
stream = streamOwner->sink();
return stream;
}
} ;
using FutureHandler = core::Function<Future<Result>(const Request &)>;
using Matcher = core::Function<Optional<Matches>(StringView)>;
using Pattern = StringView;
inline Matcher match_exact(const String &value)
{
auto value_ = String(value);
return [value_ = std::move(value_)](StringView url) -> Optional<Matches> {
if (url == value_)
return Matches({ url });
return {};
};
}
inline Matcher match_prefix(const String &prefix)
{
auto prefix_ = String(prefix);
return [prefix_ = std::move(prefix_)](StringView url) -> Optional<Matches> {
if (core::starts_with(url, prefix_))
return Matches { url.substr(prefix_.size()) };
return {};
};
}
inline Matcher match_regex(const String &pattern)
{
auto regex = std::make_shared<std::regex>(String(pattern));
return [regex = std::move(regex), pattern](StringView url) -> Optional<Matches> {
std::string s(url);
std::match_results<std::string::const_iterator> m;
if (!std::regex_match(s, m, *regex))
return {};
Matches out;
for (size_t i = 0; i < m.size(); ++i) {
auto pos = m.position(i);
auto len = m.length(i);
out.emplace_back(url.data() + pos, len);
}
return out;
};
}
using BodyType = Result;
} // namespace

21
tjp/core/http/as_future.hpp Executable file
View File

@@ -0,0 +1,21 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include "future_response.hpp"
#include <tjp/core/future/Future.hpp>
namespace tjp::core::http {
template<typename F>
auto as_future(F &&f)
{
return
[f=std::move(f)](const Request& req)
{
return future_with(f, req);
};
}
} // namespace

View File

@@ -0,0 +1,985 @@
// TJP COPYRIGHT HEADER
#if defined(TJP_CORE_HEADER_ONLY)
#pragma once
#endif
#include <tjp/core/header_only/compile.h>
#include "Server.hpp"
#include "beast.hpp"
#include <tjp/core/containers/Map.h>
#include <tjp/core/containers/Vector.h>
#include <tjp/core/exception/Exception.hpp>
#include <tjp/core/future/Futures.hpp>
#include <atomic>
#include <deque>
#include <functional>
#include <iterator>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <cstdio>
namespace tjp::core::http::beast {
namespace net = boost::asio;
namespace beast_ = boost::beast;
namespace http_ = beast_::http;
using tcp = net::ip::tcp;
struct RequestAdapter :
http::Request,
std::enable_shared_from_this<RequestAdapter>
{
String methodStorage;
String targetStorage;
String bodyStorage;
Vector<Header> headerStorage;
RequestAdapter(const http_::request<http_::string_body> &in)
{
methodStorage = String(in.method_string());
targetStorage = String(in.target());
bodyStorage = in.body();
method = methodStorage;
target = targetStorage;
body = bodyStorage;
auto headerCount = static_cast<size_t>(std::distance(in.base().begin(), in.base().end()));
headerStorage.reserve(headerCount);
headers.reserve(headerCount);
for (const auto &field: in.base())
{
headerStorage.emplace_back(Header {
String(field.name_string()),
String(field.value())
});
auto &header = headerStorage.back();
headers.emplace_back(HeaderView {
header.key,
header.value
});
}
}
} ;
using StreamState = http::PipeState;
struct ResponseAdapter :
http::Response,
std::enable_shared_from_this<ResponseAdapter>
{
String body;
bool deferred = false;
std::shared_ptr<RequestAdapter> requestHolder;
std::shared_ptr<StreamState> streamState;
std::shared_ptr<core::Function<void(std::shared_ptr<ResponseAdapter>)>> onReady;
void notifyReady()
{
if (onReady && *onReady)
(*onReady)(shared_from_this());
}
void flushHeaders(const StringView &) override
{
}
void write(const StringView &data) override
{
body = String(data);
}
std::shared_ptr<http::DataSink> stream() override
{
Optional<size_t> size;
for (const auto &header: headers)
{
if (header.key != "Content-Length")
continue;
try
{
size = static_cast<size_t>(std::stoull(String(header.value)));
}
catch (...)
{
// ignore malformed values and keep scanning
}
}
deferred = true;
streamState = StreamState::make(size);
return streamState->sink();
}
http_::response<http_::string_body> toBeast(
unsigned version,
bool keepAlive
)
{
http_::response<http_::string_body> out {
static_cast<http_::status>(status),
version
};
std::optional<String> contentType;
std::optional<String> contentLength;
out.keep_alive(keepAlive);
for (auto &header: headers)
{
if (header.key == "Content-Length")
{
contentLength = header.value;
continue;
}
if (header.key == "Content-Type")
{
contentType = header.value;
continue;
}
out.set(String(header.key), String(header.value));
}
if (!contentType)
contentType = "text/plain";
if (contentType)
out.set(http_::field::content_type, *contentType);
out.body() = body;
if (contentLength)
out.set(http_::field::content_length, *contentLength);
else
out.prepare_payload();
return out;
}
} ;
static Optional<size_t> readContentLengthFromHeaders(const Vector<Header> &headers)
{
for (const auto &header: headers)
{
if (header.key != "Content-Length")
continue;
try
{
return static_cast<size_t>(std::stoull(String(header.value)));
}
catch (...)
{
// Ignore malformed content-length and keep scanning.
}
}
return {};
}
static void applyResult(ResponseAdapter &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);
auto streamSize = result.streamSize;
if (!streamSize)
streamSize = readContentLengthFromHeaders(response.headers);
if (streamSize && !response.hasHeader("Content-Length"))
response.content_length(*streamSize);
if (result.streamOwner)
{
response.deferred = true;
response.streamState = std::move(result.streamOwner);
return;
}
if (result.stream)
{
response.status = 500;
response.header("Content-Type", "text/plain");
response.write("Invalid stream owner");
return;
}
response.write(result.body.value_or(String {}));
}
struct Server::I
{
struct Route
{
Method method;
Matcher matcher;
FutureHandler handler;
} ;
struct Session;
struct Listener;
net::io_context io;
std::unique_ptr<tcp::acceptor> acceptor;
std::shared_ptr<Listener> listener;
using WorkGuard = net::executor_work_guard<net::io_context::executor_type>;
std::unique_ptr<WorkGuard> workGuard;
std::mutex routesMutex;
Vector<Route> routes;
Vector<StrongPtr<Component>> components;
Futures futures;
std::vector<std::thread> threads;
std::optional<size_t> numThreads;
std::atomic<bool> running = false;
std::atomic<bool> listening = false;
size_t threadCount() const
{
if (numThreads && *numThreads > 0)
return *numThreads;
auto n = std::thread::hardware_concurrency();
return n == 0 ? 1 : n;
}
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;
}
};
struct Server::I::Session : std::enable_shared_from_this<Session>
{
I *i;
beast_::tcp_stream stream;
beast_::flat_buffer buffer;
http_::request<http_::string_body> request;
std::shared_ptr<ResponseAdapter> activeStreamResponse;
std::optional<http_::response<http_::empty_body>> activeStreamHeaderMessage;
std::optional<http_::response_serializer<http_::empty_body>> activeStreamHeader;
std::shared_ptr<String> activeStreamPayload;
std::shared_ptr<String> activeStreamChunkWire;
size_t activeStreamBytesWritten = 0;
bool activeStreamHeaderSent = false;
bool activeStreamWriteInFlight = false;
bool activeStreamChunked = false;
bool activeStreamHeadOnly = false;
Session(I *i_, tcp::socket &&socket) :
i(i_),
stream(std::move(socket))
{}
void run()
{
doRead();
}
void doRead()
{
request = {};
http_::async_read(
stream,
buffer,
request,
[self = shared_from_this()](beast_::error_code ec, size_t bytes) {
self->onRead(ec, bytes);
}
);
}
void onRead(beast_::error_code ec, size_t)
{
if (ec == http_::error::end_of_stream)
{
doClose();
return;
}
if (ec)
{
doClose();
return;
}
handleRequest();
}
void handleRequest()
{
auto request_ = std::make_shared<RequestAdapter>(request);
auto response = std::make_shared<ResponseAdapter>();
response->requestHolder = request_;
response->onReady = std::make_shared<core::Function<void(std::shared_ptr<ResponseAdapter>)>>(
[self = shared_from_this()](std::shared_ptr<ResponseAdapter> response_) {
net::dispatch(
self->stream.get_executor(),
[self, response_]() {
if (response_->streamState)
self->startStreaming(response_);
else
self->send(response_->toBeast(self->request.version(), self->request.keep_alive()));
}
);
}
);
try
{
FutureHandler handler;
if (i->findHandler(*request_, handler))
{
response->deferred = true;
auto future = handler(*request_).then([response, request_](auto &&future_) {
(void)request_;
try
{
applyResult(*response, future_.get());
}
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");
}
response->notifyReady();
});
i->futures.add(future);
}
else
{
response->status = 404;
response->header("Content-Type", "text/plain");
response->write("Not found");
response->notifyReady();
}
}
catch (Result &result)
{
applyResult(*response, Result(result));
response->notifyReady();
}
catch (Exception &e)
{
response->status = 400;
response->header("Content-Type", "text/plain");
response->write("Error: " + e.what());
response->notifyReady();
}
catch (...)
{
response->status = 400;
response->header("Content-Type", "text/plain");
response->write("Error");
response->notifyReady();
}
}
void send(http_::response<http_::string_body> &&response)
{
auto keepAlive = response.keep_alive();
auto sharedResponse = std::make_shared<http_::response<http_::string_body>>(std::move(response));
http_::async_write(
stream,
*sharedResponse,
[self = shared_from_this(), sharedResponse, keepAlive](beast_::error_code ec, size_t bytes) {
self->onWrite(keepAlive, ec, bytes);
}
);
}
void onWrite(bool keepAlive, beast_::error_code ec, size_t)
{
if (ec)
{
doClose();
return;
}
if (!keepAlive)
{
doClose();
return;
}
doRead();
}
void clearStreamingState()
{
activeStreamResponse.reset();
activeStreamHeaderMessage.reset();
activeStreamHeader.reset();
activeStreamPayload.reset();
activeStreamChunkWire.reset();
activeStreamBytesWritten = 0;
activeStreamHeaderSent = false;
activeStreamWriteInFlight = false;
activeStreamChunked = false;
activeStreamHeadOnly = false;
}
void startStreaming(const std::shared_ptr<ResponseAdapter> &response)
{
if (!response || !response->streamState)
return;
if (activeStreamResponse)
return;
activeStreamResponse = response;
activeStreamHeaderSent = false;
activeStreamWriteInFlight = false;
activeStreamHeaderMessage.reset();
activeStreamHeader.reset();
activeStreamPayload.reset();
activeStreamChunkWire.reset();
activeStreamBytesWritten = 0;
activeStreamHeadOnly = request.method() == http_::verb::head;
activeStreamChunked = !response->streamState->size().has_value() && !activeStreamHeadOnly;
response->streamState->setWake([self = shared_from_this()]() {
net::dispatch(
self->stream.get_executor(),
[self]() {
self->pumpStreaming();
}
);
});
pumpStreaming();
}
void pumpStreaming()
{
if (!activeStreamResponse || !activeStreamResponse->streamState)
return;
if (activeStreamWriteInFlight)
return;
auto response = activeStreamResponse;
auto state = response->streamState;
auto knownSize = state->size();
if (!activeStreamHeaderSent)
{
http_::response<http_::empty_body> header {
static_cast<http_::status>(response->status),
request.version()
};
header.keep_alive(request.keep_alive());
if (knownSize)
header.content_length(*knownSize);
else
if (activeStreamChunked)
header.chunked(true);
std::optional<String> contentType;
for (auto &header_: response->headers)
{
if (header_.key == "Content-Length")
continue;
if (header_.key == "Transfer-Encoding")
continue;
if (header_.key == "Content-Type")
{
contentType = header_.value;
continue;
}
header.set(String(header_.key), String(header_.value));
}
if (!contentType)
contentType = "text/plain";
if (contentType)
header.set(http_::field::content_type, *contentType);
activeStreamHeaderMessage.emplace(std::move(header));
activeStreamHeader.emplace(*activeStreamHeaderMessage);
activeStreamWriteInFlight = true;
http_::async_write_header(
stream,
*activeStreamHeader,
[self = shared_from_this()](beast_::error_code ec, size_t) {
self->onStreamHeaderWrite(ec);
}
);
return;
}
if (activeStreamHeadOnly)
{
auto keepAlive = request.keep_alive();
clearStreamingState();
if (keepAlive)
doRead();
else
doClose();
return;
}
if (!activeStreamChunked)
{
if (!knownSize)
{
clearStreamingState();
doClose();
return;
}
if (activeStreamBytesWritten >= *knownSize)
{
auto keepAlive = request.keep_alive();
clearStreamingState();
if (keepAlive)
doRead();
else
doClose();
return;
}
auto chunk = state->popChunk();
if (chunk)
{
auto remaining = *knownSize - activeStreamBytesWritten;
if (chunk->size() > remaining)
chunk->resize(remaining);
activeStreamPayload = chunk;
activeStreamWriteInFlight = true;
net::async_write(
stream,
net::buffer(*activeStreamPayload),
[self = shared_from_this(), payload = activeStreamPayload](beast_::error_code ec, size_t bytes) {
(void)payload;
self->onStreamChunkWrite(ec, bytes);
}
);
return;
}
if (state->isEnded())
{
// Producer ended early without delivering promised size.
clearStreamingState();
doClose();
}
return;
}
auto chunk = state->popChunk();
if (chunk)
{
char sizeHex[32] = {};
auto length = std::snprintf(sizeHex, sizeof(sizeHex), "%zx", chunk->size());
if (length < 0)
{
clearStreamingState();
doClose();
return;
}
activeStreamChunkWire = std::make_shared<String>();
activeStreamChunkWire->append(sizeHex, static_cast<size_t>(length));
activeStreamChunkWire->append("\r\n", 2);
activeStreamChunkWire->append(*chunk);
activeStreamChunkWire->append("\r\n", 2);
activeStreamWriteInFlight = true;
net::async_write(
stream,
net::buffer(*activeStreamChunkWire),
[self = shared_from_this(), chunkWire = activeStreamChunkWire](beast_::error_code ec, size_t) {
(void)chunkWire;
self->onStreamChunkedWrite(ec);
}
);
return;
}
if (state->isEnded())
{
activeStreamChunkWire = std::make_shared<String>("0\r\n\r\n");
activeStreamWriteInFlight = true;
net::async_write(
stream,
net::buffer(*activeStreamChunkWire),
[self = shared_from_this(), chunkWire = activeStreamChunkWire](beast_::error_code ec, size_t) {
(void)chunkWire;
self->onStreamChunkedDoneWrite(ec);
}
);
}
}
void onStreamHeaderWrite(beast_::error_code ec)
{
activeStreamWriteInFlight = false;
activeStreamHeaderSent = true;
activeStreamHeaderMessage.reset();
activeStreamHeader.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
pumpStreaming();
}
void onStreamChunkWrite(beast_::error_code ec, size_t bytes)
{
activeStreamWriteInFlight = false;
activeStreamBytesWritten += bytes;
activeStreamPayload.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
pumpStreaming();
}
void onStreamChunkedWrite(beast_::error_code ec)
{
activeStreamWriteInFlight = false;
activeStreamChunkWire.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
pumpStreaming();
}
void onStreamChunkedDoneWrite(beast_::error_code ec)
{
activeStreamWriteInFlight = false;
activeStreamChunkWire.reset();
if (ec)
{
clearStreamingState();
doClose();
return;
}
auto keepAlive = request.keep_alive();
clearStreamingState();
if (keepAlive)
doRead();
else
doClose();
}
void doClose()
{
clearStreamingState();
beast_::error_code ec;
stream.socket().shutdown(tcp::socket::shutdown_send, ec);
stream.socket().close(ec);
}
};
struct Server::I::Listener : std::enable_shared_from_this<Listener>
{
I *i;
Listener(I *i_) :
i(i_)
{}
void run()
{
doAccept();
}
void doAccept()
{
if (!i->acceptor || !i->running)
return;
i->acceptor->async_accept(
net::make_strand(i->io),
[self = shared_from_this()](beast_::error_code ec, tcp::socket socket) {
self->onAccept(ec, std::move(socket));
}
);
}
void onAccept(beast_::error_code ec, tcp::socket socket)
{
if (!i->running)
return;
if (!ec)
std::make_shared<Session>(i, std::move(socket))->run();
else
if (ec == net::error::operation_aborted)
return;
doAccept();
}
};
static tcp::endpoint toEndpoint(const String &bindTo, int port)
{
beast_::error_code ec;
auto address = net::ip::make_address(bindTo, ec);
if (ec)
throw Exception { "Invalid bind address" };
if (port < 0 || port > 65535)
throw Exception { "Invalid port" };
return tcp::endpoint {
address,
static_cast<unsigned short>(port)
};
}
static void throwIfError(beast_::error_code ec, const char *message)
{
if (ec)
throw Exception { message };
}
TJP_CORE_HEADER_ONLY_INLINE
Server::Server()
{
i = strong<I>();
}
TJP_CORE_HEADER_ONLY_INLINE
Server::~Server()
{
stop();
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::add(const StrongPtr<Component> &component, const String &urlPrefix)
{
i->components.emplace_back(component);
component->use(*this, urlPrefix);
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::setNumThreads(size_t n)
{
i->numThreads = n == 0 ? 1 : n;
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::start(const String &bindTo, int port)
{
if (i->running.exchange(true))
throw Exception { "Webserver already running" };
try
{
i->io.restart();
i->acceptor = std::make_unique<tcp::acceptor>(i->io);
i->workGuard = std::make_unique<I::WorkGuard>(i->io.get_executor());
auto endpoint = toEndpoint(bindTo, port);
beast_::error_code ec;
i->acceptor->open(endpoint.protocol(), ec);
throwIfError(ec, "Could not open socket");
i->acceptor->set_option(net::socket_base::reuse_address(true), ec);
throwIfError(ec, "Could not set socket options");
i->acceptor->bind(endpoint, ec);
throwIfError(ec, "Could not bind to endpoint");
i->acceptor->listen(net::socket_base::max_listen_connections, ec);
throwIfError(ec, "Could not listen");
i->listener = std::make_shared<I::Listener>(i.get());
i->listener->run();
i->listening = true;
auto count = i->threadCount();
i->threads.reserve(count);
for (size_t idx = 0; idx < count; ++idx)
{
i->threads.emplace_back([state = i.get()]() {
state->io.run();
});
}
}
catch (...)
{
stop();
throw;
}
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::run(const String &bindTo, int port)
{
if (i->running.exchange(true))
throw Exception { "Webserver already running" };
try
{
i->io.restart();
i->acceptor = std::make_unique<tcp::acceptor>(i->io);
i->workGuard = std::make_unique<I::WorkGuard>(i->io.get_executor());
auto endpoint = toEndpoint(bindTo, port);
beast_::error_code ec;
i->acceptor->open(endpoint.protocol(), ec);
throwIfError(ec, "Could not open socket");
i->acceptor->set_option(net::socket_base::reuse_address(true), ec);
throwIfError(ec, "Could not set socket options");
i->acceptor->bind(endpoint, ec);
throwIfError(ec, "Could not bind to endpoint");
i->acceptor->listen(net::socket_base::max_listen_connections, ec);
throwIfError(ec, "Could not listen");
i->listener = std::make_shared<I::Listener>(i.get());
i->listener->run();
i->listening = true;
auto count = i->threadCount();
if (count > 1)
{
i->threads.reserve(count - 1);
for (size_t idx = 1; idx < count; ++idx)
{
i->threads.emplace_back([state = i.get()]() {
state->io.run();
});
}
}
i->io.run();
for (auto &thread: i->threads)
if (thread.joinable())
thread.join();
i->threads.clear();
i->listener.reset();
i->acceptor.reset();
i->workGuard.reset();
i->listening = false;
i->running = false;
}
catch (...)
{
stop();
throw;
}
}
TJP_CORE_HEADER_ONLY_INLINE
void Server::stop()
{
if (!i->running.exchange(false))
return;
i->listening = false;
if (i->acceptor)
{
beast_::error_code ec;
i->acceptor->cancel(ec);
i->acceptor->close(ec);
}
i->listener.reset();
i->workGuard.reset();
i->io.stop();
for (auto &thread: i->threads)
if (thread.joinable())
thread.join();
i->threads.clear();
i->acceptor.reset();
i->io.restart();
}
TJP_CORE_HEADER_ONLY_INLINE
bool Server::isRunning()
{
return i->running && i->listening;
}
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

View File

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

View File

@@ -0,0 +1,50 @@
// 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::beast {
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,505 @@
// TJP COPYRIGHT HEADER
#include "../Server.hpp"
#include "../../hirose/httplib.h"
#include <tjp/core/testing/catch.hpp>
#include <tjp/core/testing/wait_until.h>
#include <tjp/core/hash/Hash.h>
#include <tjp/core/types/Types.h>
#include <algorithm>
#include <string>
#include <thread>
namespace tjp::core::http::beast {
SCENARIO("tjp::core::http::beast")
{
GIVEN("server")
{
Server server;
server.setNumThreads(2);
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("HEAD", "/meta", [](const Request &) -> Result {
Result result;
result.headers.push_back(Header { "X-Test-Head", "ok" });
return result;
});
server.bind_with("GET", "/empty", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.body = "";
return result;
});
server.bind_with("HEAD", "/empty", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.headers.push_back(Header { "Content-Length", "0" });
return result;
});
server.bind_with("HEAD", "/head-sized", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
result.headers.push_back(Header { "Content-Length", "6"} );
return result;
});
server.bind_with("GET", "/range", [](const Request &req) -> Result {
Result result;
String full = "0123456789";
auto range = req.header("Range");
if (range && *range == "bytes=2-5")
{
result.status = 206;
result.headers.push_back(Header {"Content-Range", "bytes 2-5/10"});
result.type = "text/plain";
result.body = "2345";
return result;
}
result.type = "text/plain";
result.body = full;
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", [](const Request &) -> Result {
Result result;
const String expected = "streamed-body";
result.type = "text/plain";
auto out = result.stream_body(expected.size());
std::this_thread::sleep_for(std::chrono::milliseconds(10));
out->write(expected.data(), expected.size());
out->done();
return result;
});
server.bind_with("GET", "/stream-signal", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(6);
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("nal", 3);
out->done();
}).detach();
return result;
});
server.bind_with("GET", "/stream-empty", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body(0);
out->done();
return result;
});
server.bind_with("GET", "/stream-unknown", [](const Request &) -> Result {
Result result;
result.type = "text/plain";
auto out = result.stream_body();
std::thread([out]() {
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("sig", 3);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
out->write("nal", 3);
out->done();
}).detach();
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", [](const Request &) -> Result {
std::this_thread::sleep_for(std::chrono::milliseconds(25));
Result result;
result.status = 202;
result.type = "text/plain";
result.body = "future-ok";
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;
});
}));
auto before = std::chrono::steady_clock::now();
server.start("127.0.0.1", 4792);
auto after = std::chrono::steady_clock::now();
REQUIRE((after - before) < std::chrono::seconds(1));
core::wait_until(std::chrono::seconds(10), [&]() { return server.isRunning(); });
WHEN("a get is made")
{
httplib::Client client("http://127.0.0.1:4792");
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:4792");
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:4792");
auto contents = client.Put("/replace", "payload", "text/plain");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "updated:payload");
}
WHEN("a head is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/meta");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->get_header_value("X-Test-Head") == "ok");
REQUIRE(contents->body.empty());
}
WHEN("a get of empty content is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/empty");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "0");
}
WHEN("a head of empty content is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/empty");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Length") == "0");
}
WHEN("a head of non-empty content is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Head("/head-sized");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "6");
}
WHEN("a range request is made")
{
httplib::Client client("http://127.0.0.1:4792");
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:4792");
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:4792");
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:4792");
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:4792");
auto contents = client.Get("/match/lambda");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "lambda");
}
WHEN("an unknown route is requested")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/missing");
REQUIRE(bool(contents));
REQUIRE(contents->status == 404);
}
WHEN("a future backed get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/future");
REQUIRE(bool(contents));
REQUIRE(contents->status == 202);
REQUIRE(contents->body == "future-ok");
}
WHEN("a future-backed known-size stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
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:4792");
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");
}
WHEN("a stream backed get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body == "streamed-body");
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "13");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("a signal-backed stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream-signal");
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 zero-sized stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
auto contents = client.Get("/stream-empty");
REQUIRE(bool(contents));
REQUIRE(contents->status == 200);
REQUIRE(contents->body.empty());
REQUIRE(contents->get_header_value("Content-Type") == "text/plain");
REQUIRE(contents->get_header_value("Content-Length") == "0");
REQUIRE(contents->get_header_value("Transfer-Encoding").empty());
}
WHEN("an unknown-sized stream get is made")
{
httplib::Client client("http://127.0.0.1:4792");
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:4792");
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:4792");
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:4792");
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());
}
server.stop();
}
}
} // namespace

View File

@@ -0,0 +1,25 @@
// TJP COPYRIGHT HEADER
#if defined __GNUC__
#pragma GCC diagnostic push
//#pragma GCC diagnostic ignored "-Wcomma"
//#pragma GCC diagnostic ignored "-Wdocumentation"
//#pragma GCC diagnostic ignored "-Wshorten-64-to-32"
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
//#pragma GCC diagnostic ignored "-W#pragma-messages"
#pragma clang diagnostic ignored "-Wcomma"
#pragma clang diagnostic ignored "-Wdocumentation"
#pragma clang diagnostic ignored "-Wshorten-64-to-32"
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
#pragma clang diagnostic ignored "-W#pragma-messages"
#pragma clang diagnostic ignored "-Wdocumentation"
#endif
#include <boost/asio.hpp>
#include <boost/beast.hpp>
#if defined __GNUC__
#pragma GCC diagnostic pop
#endif

View File

@@ -0,0 +1,26 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "http.hpp"
#include "future_response.hpp"
#include <tjp/core/future/Future.hpp>
namespace tjp::core::http {
template<typename F>
auto collapse_future(F &&f)
{
return
[f=std::move(f)](const Request& req)
{
using R = decltype(f(req));
if constexpr (is_future<R>::value)
return f(req).get();
else
return f(req);
};
}
}

View File

@@ -0,0 +1,19 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include <tjp/core/future/Future.hpp>
namespace tjp::core::http {
template<typename F>
auto future_response(F &&f, const Request& req)
{
return
future_with(
[&]() { return f(req); }
);
}
}

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;
}
};
}
}

11
tjp/core/http/imp.hpp Executable file
View File

@@ -0,0 +1,11 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "hirose/Server.hpp"
namespace tjp::core::http {
namespace imp = hirose;
} // namespace

8309
tjp/core/http/old/httplib.h Normal file

File diff suppressed because it is too large Load Diff

38
tjp/core/http/remote-run Executable file
View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./remote-run <command>
Environment variables:
REMOTE_HOST Default: host.docker.internal
REMOTE_PORT Default: 6666
REMOTE_TOKEN Optional shared token
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ $# -lt 1 ]]; then
usage
exit 2
fi
cmd="$1"
host="${REMOTE_HOST:-host.docker.internal}"
port="${REMOTE_PORT:-6666}"
token="${REMOTE_TOKEN:-}"
url="http://${host}:${port}/run/${cmd}"
curl_args=(--silent --show-error --no-buffer "$url")
if [[ -n "$token" ]]; then
curl_args=(-H "X-Remote-Token: ${token}" "${curl_args[@]}")
fi
exec curl "${curl_args[@]}"

View File

@@ -0,0 +1,18 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
namespace tjp::core::http {
inline
auto require_header(const Request& req, const String &key)
{
if (auto header = req.header(key))
return String(*header);
throw Exception { "Missing header" };
}
} // namespace

View File

@@ -0,0 +1,588 @@
// TJP COPYRIGHT HEADER
#include "zlib.hpp"
#include <tjp/core/threads/Lock.hpp>
#include <tjp/core/containers/Map.hpp>
#include <tjp/core/algorithm/mem_copy.hpp>
#include <zlib/zlib.h>
namespace tjp::core::compression::zlib {
struct CompressContexts {
Mutex mutex;
Map<int, Vector<void *>> contextsByLevel;
~CompressContexts()
{
for (auto &[l, contexts] : contextsByLevel)
for (auto *c : contexts)
free((z_streamp)c);
}
void *getContext(int level)
{
{
auto lock = lock_of(mutex);
auto &contexts = contextsByLevel[level];
if (!contexts.empty())
{
auto *v = contexts.back();
contexts.pop_back();
return v;
}
}
auto z = (z_streamp)calloc(1, sizeof(z_stream));
deflateInit(z, level);
return z;
}
void putContext(int level, void *v)
{
auto lock = lock_of(mutex);
contextsByLevel[level].push_back(v);
}
} ;
struct DecompressContexts
{
Mutex mutex;
Vector<void *> contexts;
~DecompressContexts()
{
for (auto *c : contexts)
free((z_streamp)c);
}
void *getContext()
{
{
auto lock = lock_of(mutex);
if (!contexts.empty())
{
auto *v = contexts.back();
contexts.pop_back();
return v;
}
}
auto z = (z_streamp)calloc(1, sizeof(z_stream));
inflateInit(z);
return z;
}
void putContext(void *v)
{
auto lock = lock_of(mutex);
contexts.push_back(v);
}
} ;
CompressContexts compressContexts;
DecompressContexts decompressContexts;
// --------------
bool clearStream(void *p, bool inflate)
{
auto z = (z_streamp)p;
if (inflate)
return (inflateReset(z) == Z_OK);
else
return (deflateReset(z) == Z_OK);
}
bool setDict(void *p, bool inflate, char *dict, size_t size)
{
auto z = (z_streamp)p;
int error = 0;
if (inflate)
error = inflateSetDictionary(z, (const Bytef *)dict, (uInt)size);
else
error = deflateSetDictionary(z, (const Bytef *)dict, (uInt)size);
return error == Z_OK;
}
bool getDict(void *p, bool inflate, char *dict, size_t size)
{
auto z = (z_streamp)p;
auto size_ = (uInt)size;
char store[32768];
int error = 0;
if (inflate)
error = inflateGetDictionary(z, (Bytef *)&store[0], &size_);
else
error = deflateGetDictionary(z, (Bytef *)&store[0], &size_);
// zlib seems to store from end to front
auto offset =
size_ > size ?
size_ - size :
0;
mem_copy(dict, &store[offset], size);
return error == Z_OK;
}
// --------------
CompressUnordered::CompressUnordered(int level_) :
level(level_)
{
}
size_t CompressUnordered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
{
const Bytef *source = (const Bytef *)source_;
uLongf sourceLen = sourceLen_;
Bytef *dest = (Bytef *)dest_;
uLongf destLen = destLen_;
auto context = (z_streamp)compressContexts.getContext(level);
context->next_in = (Bytef *)source;
context->avail_in = (uInt)sourceLen;
context->next_out = dest;
context->avail_out = (uInt)destLen;
auto result = deflate(context, Z_FINISH);
auto toSize = context->total_out;
compressContexts.putContext(level, context);
if (result != Z_STREAM_END)
return 0;
return toSize;
}
size_t UncompressUnordered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
{
const Bytef *source = (const Bytef *)source_;
uLongf sourceLen = sourceLen_;
Bytef *dest = (Bytef *)dest_;
uLongf destLen = destLen_;
auto context = (z_streamp)decompressContexts.getContext();
context->next_in = (Bytef *)source;
context->avail_in = (uInt)sourceLen;
context->next_out = dest;
context->avail_out = (uInt)destLen;
auto result = inflate(context, Z_FINISH);
auto toSize = context->total_out;
decompressContexts.putContext(context);
if (result != Z_STREAM_END)
return 0;
return toSize;
}
// --------
CompressOrdered::CompressOrdered(int level_) :
level(level_)
{
memset(dictionary, 0, dictionarySize);
}
CompressOrdered::~CompressOrdered()
{
}
size_t CompressOrdered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
{
const Bytef *source = (const Bytef *)source_;
uLongf sourceLen = sourceLen_;
Bytef *dest = (Bytef *)dest_;
uLongf destLen = destLen_;
auto context = (z_streamp)compressContexts.getContext(level);
if (!clearStream(context, false))
return 0;
if (!setDict(context, false, dictionary, dictionarySize))
return 0;
context->next_in = (Bytef *)source;
context->avail_in = (uInt)sourceLen;
context->next_out = dest;
context->avail_out = (uInt)destLen;
auto result = deflate(context, Z_FINISH);
auto toSize = context->total_out;
if (result == Z_STREAM_END)
getDict(context, false, dictionary, dictionarySize);
compressContexts.putContext(level, context);
if (result != Z_STREAM_END)
return 0;
return toSize;
}
size_t CompressOrdered::execute(Vector<char> &dest, const containers::MemorySegment<char> &source)
{
auto context = (z_streamp)compressContexts.getContext(level);
if (!clearStream(context, false))
return 0;
if (!setDict(context, false, dictionary, dictionarySize))
return 0;
context->next_in = (Bytef *)source.data();
context->avail_in = (uInt)source.size();
auto sizeOffset = dest.size();
dest.resize(dest.size() + sizeof(u32));
auto bos = dest.size();
int result = 0;
while(1)
{
auto newSize = std::max(bos + context->total_out + 2048, (size_t)16384);
dest.resize(newSize);
auto eos = bos + context->total_out;
context->next_out = (Bytef *)dest.data() + eos;
context->avail_out = (uInt)dest.size() - eos;
result = deflate(context, Z_FINISH);
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
break;
}
getDict(context, false, dictionary, dictionarySize);
u32 toSize = context->total_out;
mem_copy(dest.data() + sizeOffset, (char *)&toSize, sizeof(u32));
auto eos = bos + context->total_out;
dest.resize(eos);
compressContexts.putContext(level, context);
if (result != Z_STREAM_END)
return 0;
return toSize;
}
UncompressOrdered::UncompressOrdered()
{
memset(dictionary, 0, dictionarySize);
}
UncompressOrdered::~UncompressOrdered()
{
}
size_t UncompressOrdered::execute(char *dest_, size_t destLen_, const char *source_, size_t sourceLen_)
{
const Bytef *source = (const Bytef *)source_;
uLongf sourceLen = sourceLen_;
Bytef *dest = (Bytef *)dest_;
uLongf destLen = destLen_;
auto context = (z_streamp)decompressContexts.getContext();
if (!clearStream(context, true))
return 0;
context->next_in = (Bytef *)source;
context->avail_in = (uInt)sourceLen;
context->next_out = dest;
context->avail_out = (uInt)destLen;
auto result = inflate(context, Z_FINISH);
if (result == Z_NEED_DICT)
{
if (!setDict(context, true, dictionary, dictionarySize))
return 0;
result = inflate(context, Z_FINISH);
}
auto toSize = context->total_out;
if (result == Z_STREAM_END)
getDict(context, true, dictionary, dictionarySize);
decompressContexts.putContext(context);
if (result != Z_STREAM_END)
return 0;
return toSize;
}
size_t UncompressOrdered::execute(const containers::MemorySegment<char> &dest, const containers::MemorySegment<char> &source)
{
u32 size = 0;
mem_copy((char *)&size, source.data(), sizeof(u32));
auto dataOffset = sizeof(u32);
auto context = (z_streamp)decompressContexts.getContext();
if (!clearStream(context, true))
return 0;
context->next_in = (Bytef *)source.data() + dataOffset;
context->avail_in = size;
int result = 0;
while(1)
{
context->next_out = (Bytef *)dest.data() + context->total_out;
context->avail_out = (uInt)dest.size() - context->total_out;
result = inflate(context, Z_FINISH);
if (result == Z_NEED_DICT)
{
if (!setDict(context, true, dictionary, dictionarySize))
return 0;
result = inflate(context, Z_FINISH);
}
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
break;
}
auto toSize = context->total_out;
debug_assert(toSize == dest.size());
getDict(context, true, dictionary, dictionarySize);
decompressContexts.putContext(context);
if (result != Z_STREAM_END)
return 0;
return size + sizeof(32);
}
size_t UncompressOrdered::execute(Vector<char> &dest, const containers::MemorySegment<char> &source)
{
u32 size = 0;
mem_copy((char *)&size, source.data(), sizeof(u32));
auto dataOffset = sizeof(u32);
auto context = (z_streamp)decompressContexts.getContext();
if (!clearStream(context, true))
return 0;
context->next_in = (Bytef *)source.data() + dataOffset;
context->avail_in = size;
auto offset = 0;
int result = 0;
while(1)
{
auto newSize = std::max(offset + context->total_out + 2048, (size_t)16384);
dest.resize(newSize);
context->next_out = (Bytef *)dest.data() + offset + context->total_out;
context->avail_out = (uInt)dest.size() - offset - context->total_out;
result = inflate(context, Z_FINISH);
if (result == Z_NEED_DICT)
{
if (!setDict(context, true, dictionary, DictionarySize))
return 0;
result = inflate(context, Z_FINISH);
}
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
break;
}
auto toSize = context->total_out;
dest.resize(toSize);
getDict(context, true, dictionary, dictionarySize);
decompressContexts.putContext(context);
if (result != Z_STREAM_END)
return 0;
return size + sizeof(32);
}
// ---------
CompressStream::CompressStream()
{
memset(dictionary, 0, dictionarySize);
}
size_t CompressStream::with(Vector<char> &v, Function<void (CompressStream &)> &&f)
{
auto context = (z_streamp)decompressContexts.getContext();
if (!clearStream(context, true))
return 0;
if (!setDict(context, true, dictionary, dictionarySize))
return 0;
i = (I *)context;
v_ = &v;
p = v.size();
auto initialSize = v.size();
f(*this);
v.resize(p);
auto finalSize = v.size();
decompressContexts.putContext(context);
return finalSize - initialSize;
}
size_t CompressStream::write(const containers::MemorySegment<char> &s)
{
auto &v = *v_;
auto context = (z_streamp)i;
context->next_in = (Bytef *)s.data();
context->avail_in = (uInt)s.size();
// beginning of stream
auto bos = p;
int result = 0;
while(1)
{
auto newSize = std::max(bos + context->total_out + 2048, (size_t)16384);
v.resize(newSize);
context->next_out = (Bytef *)v.data() + bos + context->total_out;
context->avail_out = (uInt)v.size() - bos - context->total_out;
result = deflate(context, Z_FINISH);
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
break;
}
auto toSize = context->total_out;
p += toSize;
return toSize;
}
DecompressStream::DecompressStream()
{
memset(dictionary, 0, dictionarySize);
}
size_t DecompressStream::with(const containers::MemorySegment<char> &v, Function<void (DecompressStream &)> &&f)
{
auto context = (z_streamp)decompressContexts.getContext();
if (!clearStream(context, true))
return 0;
if (!setDict(context, true, dictionary, dictionarySize))
return 0;
i = (I *)context;
context->next_in = (Bytef *)v.data();
context->avail_in = (uInt)v.size();
f(*this);
auto sizeRead = context->total_in;
decompressContexts.putContext(context);
return sizeRead;
}
size_t DecompressStream::read(const containers::MemorySegment<char> &s)
{
// beginning of stream
int result = 0;
auto context = (z_streamp)i;
context->next_out = (Bytef *)s.data();
context->avail_out = (uInt)s.size();
while(1)
{
result = inflate(context, Z_FINISH);
if (result == Z_NEED_DICT)
{
if (!setDict(context, true, dictionary, DictionarySize))
return 0;
result = inflate(context, Z_FINISH);
}
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
break;
}
auto toSize = context->total_out;
return toSize;
}
} // namespace

View File

@@ -0,0 +1,88 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "zlib.h"
#include <tjp/core/containers/Vector.h>
#include <tjp/core/containers/MemorySegment.hpp>
#include <tjp/core/containers/Function.hpp>
namespace tjp::core::compression::zlib {
constexpr size_t DictionarySize = 256;
using Dictionary = char[DictionarySize];
struct CompressUnordered
{
static constexpr auto dictionarySize = DictionarySize;
CompressUnordered(int level);
int level;
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
};
struct UncompressUnordered
{
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
} ;
struct CompressOrdered {
static constexpr auto dictionarySize = DictionarySize;
CompressOrdered(int level);
~CompressOrdered();
int level;
Dictionary dictionary;
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
size_t execute(Vector<char> &, const containers::MemorySegment<char> &);
};
struct UncompressOrdered
{
static constexpr auto dictionarySize = DictionarySize;
UncompressOrdered();
~UncompressOrdered();
Dictionary dictionary;
size_t execute(char *dest, size_t destLen, const char *source, size_t sourceLen);
size_t execute(const containers::MemorySegment<char> &, const containers::MemorySegment<char> &);
size_t execute(Vector<char> &, const containers::MemorySegment<char> &);
} ;
struct CompressStream
{
static constexpr auto dictionarySize = DictionarySize;
Dictionary dictionary;
using I = void;
I *i;
Vector<char> *v_;
size_t p;
CompressStream();
size_t with(Vector<char> &, Function<void(CompressStream &)> &&f);
size_t write(const containers::MemorySegment<char> &);
} ;
struct DecompressStream
{
static constexpr auto dictionarySize = DictionarySize;
Dictionary dictionary;
using I = void;
I *i;
DecompressStream();
size_t with(const containers::MemorySegment<char> &, Function<void(DecompressStream &)> &&f);
size_t read(const containers::MemorySegment<char> &);
} ;
} // namespace

View File

@@ -0,0 +1,282 @@
// TJP COPYRIGHT HEADER
#include <tjp/core/testing/catch.hpp>
#include <tjp/core/compression/zlib.hpp>
#include <tjp/core/hash/Hash.h>
#include <tjp/core/types/Types.h>
namespace tjp::core::compression::zlib {
namespace {
SCENARIO("tjp::core::compression::zlib")
{
GIVEN("read and write")
{
CompressStream out(9);
Vector<char> packet;
struct Values {
s16 a;
Vector<int> b;
u32 c;
Vector<s16> d;
};
Values expected {
.a = 42,
.b = { 1, 2, 3, 4, 5 },
.c = 13
};
auto largeNumber = 235324;
expected.d.assign(largeNumber, 12345);
out.with(packet, [&](auto &s) {
s.value(expected.a);
s.vector(expected.b);
s.value(expected.c);
s.vector(expected.d);
});
Values expectedB {
.a = 124,
.b = { 5, 2, 3, 4, 5 },
.c = 23
};
expectedB.d.assign(largeNumber, 6666);
out.with(packet, [&](auto &s) {
s.value(expectedB.a);
s.vector(expectedB.b);
s.value(expectedB.c);
s.vector(expectedB.d);
});
Values real;
real.b.resize(5);
real.d.resize(largeNumber);
DecompressStream in;
auto segment = containers::memory_segment_vector<char>(packet);
in.with(segment, [&](auto &s) {
s.value(real.a);
s.vector(real.b);
s.value(real.c);
s.vector(real.d);
});
REQUIRE(real.a == expected.a);
REQUIRE(real.b == expected.b);
REQUIRE(real.c == expected.c);
auto d_hash = hash_of<u64>(real.d);
auto d_expectedHash = hash_of<u64>(expected.d);
REQUIRE(d_hash == d_expectedHash);
Values realB;
realB.b.resize(5);
realB.d.resize(largeNumber);
in.with(segment, [&](auto &s) {
s.value(realB.a);
s.vector(realB.b);
s.value(realB.c);
s.vector(realB.d);
});
REQUIRE(realB.a == expectedB.a);
REQUIRE(realB.b == expectedB.b);
REQUIRE(realB.c == expectedB.c);
auto bd_hash = hash_of<u64>(realB.d);
auto bd_expectedHash = hash_of<u64>(expectedB.d);
REQUIRE(bd_hash == bd_expectedHash);
}
GIVEN("back-to-back streams")
{
CompressStream out(9);
Vector<char> packet;
Vector<s16> expectedA;
Vector<s16> expectedB;
expectedA.assign(32768, 1111);
expectedB.assign(32768, 2222);
auto firstCompressedSize = out.with(packet, [&](auto &s) {
s.vector(expectedA);
});
auto secondCompressedSize = out.with(packet, [&](auto &s) {
s.vector(expectedB);
});
REQUIRE(firstCompressedSize > 0);
REQUIRE(secondCompressedSize > 0);
DecompressStream in;
auto segment = containers::memory_segment_vector<char>(packet);
Vector<s16> realA;
realA.resize(expectedA.size());
in.with(segment, [&](auto &s) {
s.vector(realA);
});
auto a_hash = hash_of<u64>(realA);
auto a_expectedHash = hash_of<u64>(expectedA);
REQUIRE(a_hash == a_expectedHash);
REQUIRE(segment.size() == secondCompressedSize);
Vector<s16> realB;
realB.resize(expectedB.size());
in.with(segment, [&](auto &s) {
s.vector(realB);
});
auto b_hash = hash_of<u64>(realB);
auto b_expectedHash = hash_of<u64>(expectedB);
REQUIRE(b_hash == b_expectedHash);
REQUIRE(segment.size() == 0);
}
GIVEN("gzip read and write")
{
CompressStream out(9, Format::Gzip);
Vector<char> packet;
struct Values {
s16 a;
Vector<int> b;
u32 c;
Vector<s16> d;
};
Values expected {
.a = 17,
.b = { 9, 8, 7, 6, 5 },
.c = 99
};
auto largeNumber = 32768;
expected.d.assign(largeNumber, 4321);
Values expectedB {
.a = 18,
.b = { 1, 3, 5, 7, 9 },
.c = 100
};
expectedB.d.assign(largeNumber, 8765);
out.with(packet, [&](auto &s) {
s.value(expected.a);
s.vector(expected.b);
s.value(expected.c);
s.vector(expected.d);
});
out.with(packet, [&](auto &s) {
s.value(expectedB.a);
s.vector(expectedB.b);
s.value(expectedB.c);
s.vector(expectedB.d);
});
REQUIRE(packet.size() >= 2);
REQUIRE((unsigned char)packet[0] == 0x1f);
REQUIRE((unsigned char)packet[1] == 0x8b);
Values real;
real.b.resize(expected.b.size());
real.d.resize(largeNumber);
DecompressStream in;
auto segment = containers::memory_segment_vector<char>(packet);
in.with(segment, [&](auto &s) {
s.value(real.a);
s.vector(real.b);
s.value(real.c);
s.vector(real.d);
});
REQUIRE(real.a == expected.a);
REQUIRE(real.b == expected.b);
REQUIRE(real.c == expected.c);
auto d_hash = hash_of<u64>(real.d);
auto d_expectedHash = hash_of<u64>(expected.d);
REQUIRE(d_hash == d_expectedHash);
Values realB;
realB.b.resize(expectedB.b.size());
realB.d.resize(largeNumber);
in.with(segment, [&](auto &s) {
s.value(realB.a);
s.vector(realB.b);
s.value(realB.c);
s.vector(realB.d);
});
REQUIRE(realB.a == expectedB.a);
REQUIRE(realB.b == expectedB.b);
REQUIRE(realB.c == expectedB.c);
auto bd_hash = hash_of<u64>(realB.d);
auto bd_expectedHash = hash_of<u64>(expectedB.d);
REQUIRE(bd_hash == bd_expectedHash);
REQUIRE(segment.size() == 0);
}
GIVEN("gzip back-to-back streams")
{
CompressStream out(9, Format::Gzip);
Vector<char> packet;
Vector<s16> expectedA;
Vector<s16> expectedB;
expectedA.assign(32768, 1111);
expectedB.assign(32768, 2222);
auto firstCompressedSize = out.with(packet, [&](auto &s) {
s.vector(expectedA);
});
auto secondCompressedSize = out.with(packet, [&](auto &s) {
s.vector(expectedB);
});
REQUIRE(firstCompressedSize > 0);
REQUIRE(secondCompressedSize > 0);
REQUIRE(packet.size() >= 2);
REQUIRE((unsigned char)packet[0] == 0x1f);
REQUIRE((unsigned char)packet[1] == 0x8b);
DecompressStream in;
auto segment = containers::memory_segment_vector<char>(packet);
Vector<s16> realA;
realA.resize(expectedA.size());
in.with(segment, [&](auto &s) {
s.vector(realA);
});
auto a_hash = hash_of<u64>(realA);
auto a_expectedHash = hash_of<u64>(expectedA);
REQUIRE(a_hash == a_expectedHash);
REQUIRE(segment.size() == secondCompressedSize);
Vector<s16> realB;
realB.resize(expectedB.size());
in.with(segment, [&](auto &s) {
s.vector(realB);
});
auto b_hash = hash_of<u64>(realB);
auto b_expectedHash = hash_of<u64>(expectedB);
REQUIRE(b_hash == b_expectedHash);
REQUIRE(segment.size() == 0);
}
}
} // namespace
} // namespace

View File

@@ -0,0 +1,148 @@
// TJP COPYRIGHT HEADER
#include <tjp/core/testing/catch.hpp>
#include <tjp/core/compression/zstd.hpp>
#include <tjp/core/hash/Hash.h>
#include <tjp/core/types/Types.h>
namespace tjp::core::compression::zstd {
namespace {
SCENARIO("tjp::core::compression::zstd")
{
GIVEN("read and write")
{
CompressStream out(9);
Vector<char> packet;
struct Values {
s16 a;
Vector<int> b;
u32 c;
Vector<s16> d;
};
Values expected {
.a = 42,
.b = { 1, 2, 3, 4, 5 },
.c = 13
};
auto largeNumber = 235324;
expected.d.assign(largeNumber, 12345);
out.with(packet, [&](auto &s) {
s.value(expected.a);
s.vector(expected.b);
s.value(expected.c);
s.vector(expected.d);
});
Values expectedB {
.a = 124,
.b = { 5, 2, 3, 4, 5 },
.c = 23
};
expectedB.d.assign(largeNumber, 6666);
out.with(packet, [&](auto &s) {
s.value(expectedB.a);
s.vector(expectedB.b);
s.value(expectedB.c);
s.vector(expectedB.d);
});
Values real;
real.b.resize(5);
real.d.resize(largeNumber);
DecompressStream in;
auto segment = containers::memory_segment_vector<char>(packet);
in.with(segment, [&](auto &s) {
s.value(real.a);
s.vector(real.b);
s.value(real.c);
s.vector(real.d);
});
REQUIRE(real.a == expected.a);
REQUIRE(real.b == expected.b);
REQUIRE(real.c == expected.c);
auto d_hash = hash_of<u64>(real.d);
auto d_expectedHash = hash_of<u64>(expected.d);
REQUIRE(d_hash == d_expectedHash);
Values realB;
realB.b.resize(5);
realB.d.resize(largeNumber);
in.with(segment, [&](auto &s) {
s.value(realB.a);
s.vector(realB.b);
s.value(realB.c);
s.vector(realB.d);
});
REQUIRE(realB.a == expectedB.a);
REQUIRE(realB.b == expectedB.b);
REQUIRE(realB.c == expectedB.c);
auto bd_hash = hash_of<u64>(realB.d);
auto bd_expectedHash = hash_of<u64>(expectedB.d);
REQUIRE(bd_hash == bd_expectedHash);
}
GIVEN("back-to-back streams")
{
CompressStream out(9);
Vector<char> packet;
Vector<s16> expectedA;
Vector<s16> expectedB;
expectedA.assign(32768, 1111);
expectedB.assign(32768, 2222);
auto firstCompressedSize = out.with(packet, [&](auto &s) {
s.vector(expectedA);
});
auto secondCompressedSize = out.with(packet, [&](auto &s) {
s.vector(expectedB);
});
REQUIRE(firstCompressedSize > 0);
REQUIRE(secondCompressedSize > 0);
DecompressStream in;
auto segment = containers::memory_segment_vector<char>(packet);
Vector<s16> realA;
realA.resize(expectedA.size());
in.with(segment, [&](auto &s) {
s.vector(realA);
});
auto a_hash = hash_of<u64>(realA);
auto a_expectedHash = hash_of<u64>(expectedA);
REQUIRE(a_hash == a_expectedHash);
REQUIRE(segment.size() == secondCompressedSize);
Vector<s16> realB;
realB.resize(expectedB.size());
in.with(segment, [&](auto &s) {
s.vector(realB);
});
auto b_hash = hash_of<u64>(realB);
auto b_expectedHash = hash_of<u64>(expectedB);
REQUIRE(b_hash == b_expectedHash);
REQUIRE(segment.size() == 0);
}
}
} // namespace
} // namespace

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
Usage:
./remote-run <command>
Environment variables:
REMOTE_HOST Default: host.docker.internal
REMOTE_PORT Default: 6666
REMOTE_TOKEN Optional shared token
EOF
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
if [[ $# -lt 1 ]]; then
usage
exit 2
fi
cmd="$1"
host="${REMOTE_HOST:-host.docker.internal}"
port="${REMOTE_PORT:-6666}"
token="${REMOTE_TOKEN:-}"
url="http://${host}:${port}/run/${cmd}"
curl_args=(--silent --show-error --no-buffer "$url")
if [[ -n "$token" ]]; then
curl_args=(-H "X-Remote-Token: ${token}" "${curl_args[@]}")
fi
exec curl "${curl_args[@]}"

View File

@@ -0,0 +1,525 @@
// TJP COPYRIGHT HEADER
#include "zlib.hpp"
#include <tjp/core/threads/Lock.hpp>
#include <tjp/core/containers/Map.hpp>
#include <tjp/core/algorithm/mem_copy.hpp>
#include <tjp/core/algorithm/ExecuteOnDestruct.hpp>
#include <zlib/zlib.h>
namespace tjp::core::compression::zlib {
enum class ContextType {
Compress,
Decompress
} ;
namespace {
constexpr int ZlibWindowBits = 15;
constexpr int GzipWindowBits = ZlibWindowBits + 16;
constexpr int InflateAutoWindowBits = ZlibWindowBits + 32;
int window_bits_for(Format format)
{
switch (format)
{
case Format::Zlib:
return ZlibWindowBits;
case Format::Gzip:
return GzipWindowBits;
default:
return ZlibWindowBits;
}
}
size_t compress_key(int level, Format format)
{
return (size_t(level) << 2) | size_t(format);
}
} // namespace
struct CompressContexts {
Mutex mutex;
Map<size_t, Vector<void *>> contextsByKey;
~CompressContexts()
{
for (auto &[k, contexts] : contextsByKey)
for (auto *c : contexts)
free((z_streamp)c);
}
void *getContext(int level, Format format)
{
auto key = compress_key(level, format);
{
auto lock = lock_of(mutex);
auto &contexts = contextsByKey[key];
if (!contexts.empty())
{
auto *v = contexts.back();
contexts.pop_back();
return v;
}
}
auto z = (z_streamp)calloc(1, sizeof(z_stream));
auto result = deflateInit2(
z,
level,
Z_DEFLATED,
window_bits_for(format),
8,
Z_DEFAULT_STRATEGY
);
if (result != Z_OK)
{
free((void *)z);
return nullptr;
}
return z;
}
void putContext(int level, Format format, void *v)
{
auto key = compress_key(level, format);
auto lock = lock_of(mutex);
contextsByKey[key].push_back(v);
}
} ;
struct DecompressContexts
{
Mutex mutex;
Vector<void *> contexts;
~DecompressContexts()
{
for (auto *c : contexts)
free((z_streamp)c);
}
void *getContext()
{
{
auto lock = lock_of(mutex);
if (!contexts.empty())
{
auto *v = contexts.back();
contexts.pop_back();
return v;
}
}
auto z = (z_streamp)calloc(1, sizeof(z_stream));
auto result = inflateInit2(z, InflateAutoWindowBits);
if (result != Z_OK)
{
free((void *)z);
return nullptr;
}
return z;
}
void putContext(void *v)
{
auto lock = lock_of(mutex);
contexts.push_back(v);
}
} ;
CompressContexts compressContexts;
DecompressContexts decompressContexts;
#ifdef TIMPREPSCIUS_ZLIB_USE_DICTIONARY
Dictionary::Dictionary()
{
memset(block, 0, size);
}
bool setDict(void *p, ContextType type, const Dictionary &d)
{
auto z = (z_streamp)p;
int error = 0;
if (type == ContextType::Decompress)
error = inflateSetDictionary(z, (const Bytef *)d.block, (uInt)d.size);
else
error = deflateSetDictionary(z, (const Bytef *)d.block, (uInt)d.size);
return error == Z_OK;
}
bool getDict(void *p, ContextType type, Dictionary &d)
{
auto z = (z_streamp)p;
constexpr auto DictStoreSize = 32768;
char store[DictStoreSize];
memset(store, 0, DictStoreSize);
auto size_ = (uInt)DictStoreSize;
int error = 0;
if (type == ContextType::Decompress)
error = inflateGetDictionary(z, (Bytef *)&store[0], &size_);
else
error = deflateGetDictionary(z, (Bytef *)&store[0], &size_);
// zlib seems to store from end to front
auto offset =
size_ > d.size ?
size_ - d.size :
0;
int firstIndex = -1, lastIndex = -1;
for (auto i=0; i<DictStoreSize; ++i)
{
if (store[i] != 0)
{
if (firstIndex < 0)
firstIndex = i;
lastIndex = i;
}
}
mem_copy(d.block, &store[offset], d.size);
return error == Z_OK;
}
#else
constexpr
bool setDict(void *p, ContextType type, const Dictionary &d) { return true; }
constexpr
bool getDict(void *p, ContextType type, Dictionary &d) { return true; }
#endif
// --------------
bool clearStream(void *p, ContextType type)
{
auto z = (z_streamp)p;
if (type == ContextType::Decompress)
return (inflateReset(z) == Z_OK);
else
return (deflateReset(z) == Z_OK);
}
// --------------
static size_t round_up_to_multiple(size_t number, size_t multiple) {
if (multiple == 0) {
return number; // Avoid division by zero
}
auto remainder = number % multiple;
if (remainder == 0) {
return number; // Already a multiple
}
return number + multiple - remainder;
}
Compressor::Compressor(int level_, Format format_) :
level(level_),
format(format_)
{
}
size_t Compressor::with(V &v, F &&f)
{
error_ = 0;
auto context = (z_streamp)compressContexts.getContext(level, format);
if (!context)
return 0;
auto _ = ExecuteOnDestruct([&]() {
compressContexts.putContext(level, format, context);
i = nullptr;
v_ = nullptr;
});
if (!clearStream(context, ContextType::Compress))
return 0;
if (!setDict(context, ContextType::Compress, dictionary))
return 0;
i = (I *)context;
v_ = &v;
p = v.size();
auto p_ = p;
f(*this);
// finish the stream
while(1)
{
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
v.resize(newSize);
context->next_out = (Bytef *)v.data() + p;
context->avail_out = uInt(newSize - p);
auto result = deflate(context, Z_FINISH);
p = size_t(context->next_out - (Bytef *)v.data());
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
{
error_ = result;
break;
}
}
getDict(context, ContextType::Compress, dictionary);
p = size_t(context->next_out - (Bytef *)v.data());
v.resize(p);
return p - p_;
}
size_t Compressor::write(const containers::MemorySegment<char> &s)
{
auto &v = *v_;
auto context = (z_streamp)i;
context->next_in = (Bytef *)s.data();
context->avail_in = (uInt)s.size();
// beginning of stream
int result = 0;
size_t written = 0;
while(1)
{
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
v.resize(newSize);
context->next_out = (Bytef *)v.data() + p;
context->avail_out = uInt(newSize - p);
result = deflate(context, Z_NO_FLUSH);
p = size_t(context->next_out - (Bytef *)v.data());
written = s.size() - (size_t)context->avail_in;
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
{
error_ = result;
break;
}
if (context->avail_in == 0)
break;
}
return written;
}
void Compressor::flush()
{
auto &v = *v_;
auto context = (z_streamp)i;
context->next_in = nullptr;
context->avail_in = 0;
// beginning of stream
int result = 0;
while(1)
{
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
v.resize(newSize);
context->next_out = (Bytef *)v.data() + p;
context->avail_out = uInt(newSize - p);
result = deflate(context, Z_PARTIAL_FLUSH);
p = size_t(context->next_out - (Bytef *)v.data());
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
{
error_ = result;
break;
}
if (context->avail_out > 0)
break;
}
}
size_t Compressor::size() const
{
return p;
}
bool Compressor::error () const
{
return error_;
}
CompressStream::CompressStream(int level, Format format) :
compressor(level, format)
{
}
size_t CompressStream::with(V &v, F &&f)
{
return compressor.with(v, std::move(f));
}
CompressPacket::CompressPacket(int level_, Format format_) :
level(level_),
format(format_)
{
}
size_t CompressPacket::with(V &v, F &&f)
{
Compressor compressor(level, format);
return compressor.with(v, std::move(f));
}
Decompressor::Decompressor()
{
}
size_t Decompressor::with(V &v, F &&f)
{
error_ = 0;
auto context = (z_streamp)decompressContexts.getContext();
if (!context)
return 0;
i = (I *)context;
v_ = &v;
auto _ = ExecuteOnDestruct([&]() {
decompressContexts.putContext(context);
i = nullptr;
v_ = nullptr;
});
if (!clearStream(context, ContextType::Decompress))
return 0;
context->next_in = (Bytef *)v.data();
context->avail_in = uInt(v.size());
p = 0;
f(*this);
getDict(context, ContextType::Decompress, dictionary);
auto written = context->total_out;
auto consumed = size_t(context->next_in - (Bytef *)v.data());
v.data_ = (char *)context->next_in;
if (consumed > v.size_)
v.size_ = 0;
else
v.size_ -= consumed;
return written;
}
size_t Decompressor::read(const containers::MemorySegment<char> &s)
{
auto &v = *v_;
auto context = (z_streamp)i;
context->next_out = (Bytef *)s.data();
context->avail_out = (uInt)s.size();
int result = 0;
size_t written = 0;
while(1)
{
result = inflate(context, Z_SYNC_FLUSH);
written = size_t(context->next_out - (Bytef *)s.data());
p = size_t(context->next_in - (Bytef *)v.data());
if (result == Z_NEED_DICT)
{
if (!setDict(context, ContextType::Decompress, dictionary))
return 0;
}
if (result == Z_STREAM_END)
break;
if (result <= Z_ERRNO)
{
error_ = result;
break;
}
if (written == s.size())
break;
}
return written;
}
bool Decompressor::eos() const
{
return error() || p >= v_->size();
}
bool Decompressor::error () const
{
return error_;
}
DecompressStream::DecompressStream()
{
}
size_t DecompressStream::with(V &v, F &&f)
{
return decompressor.with(v, std::move(f));
}
DecompressPacket::DecompressPacket()
{
}
size_t DecompressPacket::with(V &v, F &&f)
{
Decompressor decompressor;
return decompressor.with(v, std::move(f));
}
} // namespace

View File

@@ -0,0 +1,11 @@
// TJP COPYRIGHT HEADER
#pragma once
namespace tjp::core::compression::zlib {
struct CompressStream;
struct DecompressStream;
} // namespace

View File

@@ -0,0 +1,158 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "zlib.h"
#include <tjp/core/containers/Vector.h>
#include <tjp/core/containers/MemorySegment.hpp>
#include <tjp/core/containers/Function.hpp>
namespace tjp::core::compression::zlib {
enum class Format
{
Zlib,
Gzip
} ;
//#define TIMPREPSCIUS_ZLIB_USE_DICTIONARY
#ifdef TIMPREPSCIUS_ZLIB_USE_DICTIONARY
struct Dictionary
{
static constexpr auto size = 256;
char block[size];
Dictionary();
} ;
#else
struct Dictionary {};
#endif
struct Compressor
{
[[no_unique_address]]
Dictionary dictionary;
static constexpr auto sizeMultiples = 512;
using V = Vector<char>;
using F = Function<void(Compressor &)>;
int level;
Format format;
using I = void;
I *i = nullptr;
V *v_ = nullptr;
size_t p = 0;
int error_ = 0;
Compressor(int level, Format format = Format::Zlib);
size_t size() const;
size_t with(V &, F &&f);
size_t write(const containers::MemorySegment<char> &);
void flush();
template<typename T>
auto value(T &t) {
auto segment = containers::memory_segment_value<char>(t);
return write(segment) == segment.size();
}
template<typename T>
auto vector(T &t) {
auto segment = containers::memory_segment_vector<char>(t);
return write(segment) == segment.size();
}
bool error() const;
} ;
struct CompressStream
{
using V = Compressor::V;
using F = Compressor::F;
Compressor compressor;
CompressStream(int level, Format format = Format::Zlib);
size_t with(V &, F &&f);
} ;
struct CompressPacket
{
using V = Compressor::V;
using F = Compressor::F;
int level;
Format format;
CompressPacket(int level, Format format = Format::Zlib);
size_t with(V &, F &&f);
} ;
struct Decompressor
{
[[no_unique_address]]
Dictionary dictionary;
using V = containers::MemorySegment<char>;
using F = Function<void(Decompressor &)>;
using I = void;
I *i = nullptr;
V *v_ = nullptr;
size_t p = 0;
int error_ = 0;
Decompressor();
bool eos() const;
size_t with(V &, F &&f);
size_t read(const containers::MemorySegment<char> &);
template<typename T>
auto value(T &t) {
auto segment = containers::memory_segment_value<char>(t);
return read(segment) == segment.size();
}
template<typename T>
auto vector(T &t) {
auto segment = containers::memory_segment_vector<char>(t);
return read(segment) == segment.size();
}
bool error () const;
} ;
struct DecompressStream
{
using V = Decompressor::V;
using F = Decompressor::F;
Decompressor decompressor;
DecompressStream();
size_t with(V &, F &&f);
} ;
struct DecompressPacket
{
using V = Decompressor::V;
using F = Decompressor::F;
DecompressPacket();
size_t with(V &, F &&f);
} ;
} // namespace

View File

@@ -0,0 +1,356 @@
// TJP COPYRIGHT HEADER
#include "zstd.hpp"
#include <tjp/core/threads/Lock.hpp>
#include <tjp/core/containers/Map.hpp>
#include <tjp/core/algorithm/mem_copy.hpp>
#include <tjp/core/algorithm/ExecuteOnDestruct.hpp>
#include <zstd/zstd.h>
namespace tjp::core::compression::zstd {
enum class ContextType {
Compress,
Decompress
} ;
struct CompressContexts {
Mutex mutex;
Map<int, Vector<void *>> contextsByLevel;
~CompressContexts()
{
for (auto &[l, contexts] : contextsByLevel)
for (auto *c : contexts)
ZSTD_freeCStream((ZSTD_CStream *)c);
}
void *getContext(int level)
{
{
auto lock = lock_of(mutex);
auto &contexts = contextsByLevel[level];
if (!contexts.empty())
{
auto *v = contexts.back();
contexts.pop_back();
return v;
}
}
auto *v = ZSTD_createCStream();
ZSTD_initCStream(v, level);
return v;
}
void putContext(int level, void *v)
{
auto lock = lock_of(mutex);
contextsByLevel[level].push_back(v);
}
} ;
struct DecompressContexts
{
Mutex mutex;
Vector<void *> contexts;
~DecompressContexts()
{
for (auto *c : contexts)
ZSTD_freeDStream((ZSTD_DStream *)c);
}
void *getContext()
{
{
auto lock = lock_of(mutex);
if (!contexts.empty())
{
auto *v = contexts.back();
contexts.pop_back();
return v;
}
}
void *v = ZSTD_createDStream();
return v;
}
void putContext(void *v)
{
auto lock = lock_of(mutex);
contexts.push_back(v);
}
} ;
CompressContexts compressContexts;
DecompressContexts decompressContexts;
// --------------
bool clearStream(void *p, ContextType type)
{
return true;
}
// --------------
static size_t round_up_to_multiple(size_t number, size_t multiple) {
if (multiple == 0) {
return number; // Avoid division by zero
}
auto remainder = number % multiple;
if (remainder == 0) {
return number; // Already a multiple
}
return number + multiple - remainder;
}
Compressor::Compressor(int level_) :
level(level_)
{
}
size_t Compressor::with(V &v, F &&f)
{
auto lock = lock_of(mutex);
auto context = (ZSTD_CStream *)compressContexts.getContext(level);
auto _ = ExecuteOnDestruct([&]() {
compressContexts.putContext(level, context);
i = nullptr;
v_ = nullptr;
});
if (!clearStream(context, ContextType::Compress))
return 0;
i = (I *)context;
v_ = &v;
p = v.size();
auto p_ = p;
f(*this);
// finish the stream
while(1)
{
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
v.resize(newSize);
ZSTD_outBuffer out;
out.dst = v.data();
out.pos = p;
out.size = v.size();
auto result = ZSTD_endStream(context, &out);
p = out.pos;
if (ZSTD_isError(result))
{
error_ = result;
break;
}
if (result == 0)
break;
}
v.resize(p);
return p - p_;
}
size_t Compressor::write(const containers::MemorySegment<char> &s)
{
auto &v = *v_;
auto context = (ZSTD_CStream *)i;
ZSTD_inBuffer in;
in.size = s.size();
in.src = s.data();
in.pos = 0;
// beginning of stream
size_t result = 0;
while(1)
{
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
v.resize(newSize);
ZSTD_outBuffer out;
out.dst = v.data();
out.pos = p;
out.size = v.size();
result = ZSTD_compressStream(context, &out, &in);
p = out.pos;
if (ZSTD_isError(result))
{
error_ = result;
break;
}
if (in.pos == in.size)
break;
}
return in.pos;
}
void Compressor::flush()
{
auto &v = *v_;
auto context = (ZSTD_CStream *)i;
// beginning of stream
size_t result = 0;
while(1)
{
auto newSize = round_up_to_multiple(p + 1, sizeMultiples);
v.resize(newSize);
ZSTD_outBuffer out;
out.dst = v.data();
out.pos = p;
out.size = v.size();
result = ZSTD_flushStream(context, &out);
p = out.pos;
if (ZSTD_isError(result))
{
error_ = result;
break;
}
if (out.pos < out.size)
break;
}
}
size_t Compressor::size() const
{
return p;
}
bool Compressor::error () const
{
return error_;
}
CompressPacket::CompressPacket(int level_) :
level(level_)
{
}
size_t CompressPacket::with(V &v, F &&f)
{
Compressor compressor(level);
return compressor.with(v, std::move(f));
}
Decompressor::Decompressor()
{
}
size_t Decompressor::with(V &v, F &&f)
{
auto lock = lock_of(mutex);
auto context = (ZSTD_DCtx *)decompressContexts.getContext();
i = (I *)context;
v_ = &v;
auto _ = ExecuteOnDestruct([&]() {
decompressContexts.putContext(context);
i = nullptr;
v_ = nullptr;
});
if (!clearStream(context, ContextType::Decompress))
return 0;
p = 0;
f(*this);
v.data_ += p;
v.size_ -= p;
return p;
}
size_t Decompressor::read(const containers::MemorySegment<char> &s)
{
auto &v = *v_;
auto context = (ZSTD_DCtx *)i;
ZSTD_inBuffer in;
in.size = v.size();
in.src = v.data();
in.pos = p;
ZSTD_outBuffer out;
out.dst = (void *)s.data();
out.pos = 0;
out.size = s.size();
size_t result = 0;
while(1)
{
result = ZSTD_decompressStream(context, &out, &in);
p = in.pos;
if (ZSTD_isError(result))
{
error_ = result;
break;
}
if (in.pos == in.size)
break;
if (out.pos == out.size)
break;
}
return out.pos;
}
bool Decompressor::eos() const
{
return error() || p >= v_->size();
}
bool Decompressor::error () const
{
return error_;
}
DecompressPacket::DecompressPacket()
{
}
size_t DecompressPacket::with(V &v, F &&f)
{
Decompressor decompressor;
return decompressor.with(v, std::move(f));
}
} // namespace

View File

@@ -0,0 +1,14 @@
// TJP COPYRIGHT HEADER
#pragma once
namespace tjp::core::compression::zstd {
struct CompressPacket;
struct DecompressPacket;
using CompressStream = CompressPacket;
using DecompressStream = DecompressPacket;
} // namespace

View File

@@ -0,0 +1,111 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "zstd.h"
#include <tjp/core/containers/Vector.h>
#include <tjp/core/containers/MemorySegment.hpp>
#include <tjp/core/containers/Function.hpp>
#include <tjp/core/threads/TestMutex.hpp>
namespace tjp::core::compression::zstd {
struct Compressor
{
core::TestMutex mutex;
static constexpr auto sizeMultiples = 512;
using V = Vector<char>;
using I = void;
using F = Function<void(Compressor &)>;
int level;
I *i = nullptr;
V *v_ = nullptr;
size_t p = 0;
size_t error_ = 0;
Compressor(int level);
size_t with(V &, F &&f);
size_t size() const;
size_t write(const containers::MemorySegment<char> &);
void flush();
template<typename T>
auto value(const T &t) {
auto segment = containers::memory_segment_value<char>(t);
return write(segment) == segment.size();
}
template<typename T>
auto vector(const T &t) {
auto segment = containers::memory_segment_vector<char>(t);
return write(segment) == segment.size();
}
bool error () const;
} ;
struct CompressPacket
{
using V = Compressor::V;
using F = Compressor::F;
int level;
CompressPacket(int level);
size_t with(V &, F &&f);
} ;
struct Decompressor
{
core::TestMutex mutex;
using V = containers::MemorySegment<char>;
using I = void;
using F = Function<void(Decompressor &)>;
I *i = nullptr;
V *v_ = nullptr;
size_t p = 0;
size_t error_ = 0;
Decompressor();
bool eos() const;
size_t with(V &, F &&f);
size_t read(const containers::MemorySegment<char> &);
template<typename T>
auto value(T &t) {
auto segment = containers::memory_segment_value<char>(t);
return read(segment) == segment.size();
}
template<typename T>
auto vector(T &t) {
auto segment = containers::memory_segment_vector<char>(t);
return read(segment) == segment.size();
}
bool error () const;
};
struct DecompressPacket
{
using V = Decompressor::V;
using F = Decompressor::F;
DecompressPacket();
size_t with(V &, F &&);
} ;
} // namespace

55
tjp/core/http/transform.hpp Executable file
View File

@@ -0,0 +1,55 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include "future_response.hpp"
#include <tjp/core/future/Future.hpp>
namespace tjp::core::http {
template<typename F>
auto transform(F &&f)
{
return
[f=std::move(f)](const Request& req)
{
return future_response(f, req);
};
}
template<typename Serializer, typename F>
auto transform(F &&f)
{
return
transform(
[f=std::move(f)](const Request& req) {
auto future = future_response(f, req);
return future.then(
[](auto &&f) {
return Serializer{}(f.get());
});
}
);
}
//template<typename Serializer, typename F>
//auto transform(F &&f)
//{
// return
// [f=std::move(f)](const Request& req, Response& res)
// {
// auto future = future_response(f, req);
// return future.then(
// [](auto &&f) {
// auto [body, type] = Serializer{}(result);
// res.set_content(body, type);
// });
// };
//}
}

View File

@@ -0,0 +1,29 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include "../bases/Base64.h"
#include "transform.hpp"
namespace tjp::core::http {
struct ToBase64Text
{
template <typename T>
BodyType operator()(const T &value) const
{
return {
toBase64(value),
"application/base64"
};
}
} ;
template<typename F>
auto transform_base64_text(F &&f)
{
return transform<ToBase64Text>(std::forward<F>(f));
}
}

View File

@@ -0,0 +1,46 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include <tjp/core/string/as_string.hpp>
#include <tjp/core/string/to_string.hpp>
#include "transform.hpp"
namespace tjp::core::http {
struct ToBinary
{
template <typename T>
BodyType operator()(T &&value) const
{
return {
core::as_string(std::forward<T>(value)),
"application/binary"
};
}
} ;
template<typename Serializer, typename F>
auto transform_binary(F &&f)
{
return
[f=std::move(f)](const Request& req)
{
auto future = future_with(f(req));
return future.then([](auto &&f) {
return BodyType {
Serializer{}(f.get()),
"application/binary"
};
});
};
}
template<typename F>
auto transform_binary_text(F &&f)
{
return transform<ToBinary>(std::forward<F>(f));
}
}

View File

@@ -0,0 +1,46 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include <tjp/core/io/json/out.h>
#include <tjp/core/string/as_string.hpp>
#include "transform.hpp"
namespace tjp::core::http {
struct ToJson
{
template <typename T>
BodyType operator()(const T &value) const
{
return {
core::as_string(core::io::json::serialize(value)),
"application/json"
};
}
} ;
template<typename Serializer, typename F>
auto transform_json(F &&f)
{
return
[f=std::move(f)](const Request& req)
{
auto future = future_with(f(req));
return future.then([](auto &&f) {
return BodyType {
Serializer{}(f.get()),
"application/json"
};
});
};
}
template<typename F>
auto transform_json(F &&f)
{
return transform<ToJson>(std::forward<F>(f));
}
}

View File

@@ -0,0 +1,29 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include <tjp/core/string/as_string.hpp>
#include "transform.hpp"
namespace tjp::core::http {
struct ToJsonText
{
template <typename T>
BodyType operator()(const T &value) const
{
return {
core::as_string(value),
"application/json"
};
}
} ;
template<typename F>
auto transform_json_text(F &&f)
{
return transform<ToJsonText>(std::forward<F>(f));
}
}

30
tjp/core/http/transform_ok.hpp Executable file
View File

@@ -0,0 +1,30 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include <tjp/core/io/json/out.h>
#include <tjp/core/string/as_string.hpp>
#include "transform.hpp"
namespace tjp::core::http {
template<typename F>
auto transform_ok(F &&f)
{
return
[f=std::move(f)](const Request& req)
{
auto future = future_with(f, req);
return future.then([](auto &&f) {
f.get();
return BodyType {
"OK",
"text/plain"
};
});
};
}
}

View File

@@ -0,0 +1,46 @@
// TJP COPYRIGHT HEADER
#pragma once
#include "Types.hpp"
#include <tjp/core/string/as_string.hpp>
#include <tjp/core/string/to_string.hpp>
#include "transform.hpp"
namespace tjp::core::http {
struct ToPlainText
{
template <typename T>
BodyType operator()(T &&value) const
{
return {
core::to_string(std::forward<T>(value)),
"text/plain"
};
}
} ;
template<typename Serializer, typename F>
auto transform_plaintext(F &&f)
{
return
[f=std::move(f)](const Request& req)
{
auto future = future_with(f(req));
return future.then([](auto &&f) {
return BodyType {
Serializer{}(f.get()),
"text/plain"
};
});
};
}
template<typename F>
auto transform_plaintext(F &&f)
{
return transform<ToPlainText>(std::forward<F>(f));
}
}