diff --git a/src/server/CMakeLists.txt b/src/server/CMakeLists.txt index 2da8cc34..2a65ebc7 100644 --- a/src/server/CMakeLists.txt +++ b/src/server/CMakeLists.txt @@ -1,6 +1,15 @@ sourcemeta_executable(NAMESPACE sourcemeta PROJECT one NAME server FOLDER "One/Server" - SOURCES search.h status.h evaluate.h helpers.h request.h server.cc uwebsockets.h) + SOURCES + action_jsonschema_evaluate.h + action_jsonschema_serve.h + action_schema_search.h + action_serve_metapack_file.h + helpers.h + request.h + response.h + server.cc + uwebsockets.h) set_target_properties(sourcemeta_one_server PROPERTIES OUTPUT_NAME sourcemeta-one-server) diff --git a/src/server/evaluate.h b/src/server/action_jsonschema_evaluate.h similarity index 54% rename from src/server/evaluate.h rename to src/server/action_jsonschema_evaluate.h index ab492c1d..64a5f248 100644 --- a/src/server/evaluate.h +++ b/src/server/action_jsonschema_evaluate.h @@ -1,5 +1,5 @@ -#ifndef SOURCEMETA_ONE_SERVER_EVALUATE_H -#define SOURCEMETA_ONE_SERVER_EVALUATE_H +#ifndef SOURCEMETA_ONE_SERVER_ACTION_JSONSCHEMA_EVALUATE_H +#define SOURCEMETA_ONE_SERVER_ACTION_JSONSCHEMA_EVALUATE_H #include #include @@ -9,8 +9,14 @@ #include +#include "helpers.h" +#include "request.h" +#include "response.h" + #include // assert #include // std::filesystem::path +#include // std::ostringstream +#include // std::string_view #include // std::underlying_type_t #include // std::move @@ -143,4 +149,105 @@ auto evaluate(const std::filesystem::path &template_path, } // namespace sourcemeta::one +static auto +action_jsonschema_evaluate_callback(const std::filesystem::path &template_path, + const sourcemeta::one::EvaluateType mode, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response, + std::string &&body, bool too_big) -> void { + if (too_big) { + json_error(request, response, sourcemeta::one::STATUS_PAYLOAD_TOO_LARGE, + "payload-too-large", "The request body is too large"); + return; + } + + if (body.empty()) { + json_error(request, response, sourcemeta::one::STATUS_BAD_REQUEST, + "no-instance", "You must pass an instance to validate against"); + return; + } + + try { + const auto result{sourcemeta::one::evaluate(template_path, body, mode)}; + response.write_status(sourcemeta::one::STATUS_OK); + response.write_header("Content-Type", "application/json"); + response.write_header("Access-Control-Allow-Origin", "*"); + if (mode == sourcemeta::one::EvaluateType::Trace) { + write_link_header(response, + "/self/v1/schemas/api/schemas/trace/response"); + } else { + write_link_header(response, + "/self/v1/schemas/api/schemas/evaluate/response"); + } + + std::ostringstream payload; + sourcemeta::core::prettify(result, payload); + send_response(sourcemeta::one::STATUS_OK, request, response, payload.str(), + sourcemeta::one::Encoding::Identity); + } catch (const std::exception &exception) { + json_error(request, response, sourcemeta::one::STATUS_INTERNAL_SERVER_ERROR, + "evaluation-error", exception.what()); + } +} + +static auto action_jsonschema_evaluate(const std::filesystem::path &base, + const std::string_view &path, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response, + const sourcemeta::one::EvaluateType mode) + -> void { + // A CORS pre-flight request + if (request.method() == "options") { + response.write_status(sourcemeta::one::STATUS_NO_CONTENT); + response.write_header("Access-Control-Allow-Origin", "*"); + response.write_header("Access-Control-Allow-Methods", "POST, OPTIONS"); + response.write_header("Access-Control-Allow-Headers", "Content-Type"); + response.write_header("Access-Control-Max-Age", "3600"); + send_response(sourcemeta::one::STATUS_NO_CONTENT, request, response); + } else if (request.method() == "post") { + auto template_path{base / "schemas"}; + template_path /= path; + template_path /= SENTINEL; + template_path /= "blaze-exhaustive.metapack"; + if (!std::filesystem::exists(template_path)) { + const auto schema_path{template_path.parent_path() / "schema.metapack"}; + if (std::filesystem::exists(schema_path)) { + json_error(request, response, + sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "no-template", + "This schema was not precompiled for schema evaluation"); + } else { + json_error(request, response, sourcemeta::one::STATUS_NOT_FOUND, + "not-found", "There is nothing at this URL"); + } + + return; + } + + request.body( + [mode, template_path = std::move(template_path)]( + sourcemeta::one::HTTPRequest &callback_request, + sourcemeta::one::HTTPResponse &callback_response, + std::string &&body, bool too_big) { + action_jsonschema_evaluate_callback( + template_path, mode, callback_request, callback_response, + std::move(body), too_big); + }, + [](sourcemeta::one::HTTPRequest &callback_request, + sourcemeta::one::HTTPResponse &callback_response, + std::exception_ptr error) { + try { + std::rethrow_exception(error); + } catch (const std::exception &exception) { + json_error(callback_request, callback_response, + sourcemeta::one::STATUS_INTERNAL_SERVER_ERROR, + "uncaught-error", exception.what()); + } + }); + } else { + json_error(request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, + "method-not-allowed", + "This HTTP method is invalid for this URL"); + } +} + #endif diff --git a/src/server/action_jsonschema_serve.h b/src/server/action_jsonschema_serve.h new file mode 100644 index 00000000..d4b0a44c --- /dev/null +++ b/src/server/action_jsonschema_serve.h @@ -0,0 +1,64 @@ +#ifndef SOURCEMETA_ONE_SERVER_ACTION_JSONSCHEMA_SERVE_H +#define SOURCEMETA_ONE_SERVER_ACTION_JSONSCHEMA_SERVE_H + +#include + +#include "action_serve_metapack_file.h" +#include "helpers.h" +#include "request.h" +#include "response.h" + +#include // std::transform +#include // std::tolower +#include // std::filesystem +#include // std::string +#include // std::string_view + +static auto action_jsonschema_serve(const std::filesystem::path &base, + const std::string_view &path, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + // Otherwise we may get unexpected results in case-sensitive file-systems + std::string lowercase_path{path}; + std::transform( + lowercase_path.begin(), lowercase_path.end(), lowercase_path.begin(), + [](const unsigned char character) { return std::tolower(character); }); + + // Because Visual Studio Code famously does not support `$id` or `id` + // See + // https://github.com/microsoft/vscode-json-languageservice/issues/224 + const auto &user_agent{request.header("user-agent")}; + const auto is_vscode{user_agent.starts_with("Visual Studio Code") || + user_agent.starts_with("VSCodium")}; + const auto is_deno{user_agent.starts_with("Deno/")}; + const auto bundle{!request.query("bundle").empty()}; + auto absolute_path{base / "schemas" / lowercase_path / SENTINEL}; + if (is_vscode) { + absolute_path /= "editor.metapack"; + } else if (bundle || is_deno) { + absolute_path /= "bundle.metapack"; + } else { + absolute_path /= "schema.metapack"; + } + + if (request.method() != "get" && request.method() != "head" && + !std::filesystem::exists(absolute_path)) { + json_error(request, response, sourcemeta::one::STATUS_NOT_FOUND, + "not-found", "There is nothing at this URL"); + return; + } + + if (is_deno) { + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true, + // For HTTP imports, as Deno won't like the + // `application/schema+json` one + "application/json"); + } else { + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true); + } +} + +#endif diff --git a/src/server/search.h b/src/server/action_schema_search.h similarity index 52% rename from src/server/search.h rename to src/server/action_schema_search.h index adf7d92b..b601bd26 100644 --- a/src/server/search.h +++ b/src/server/action_schema_search.h @@ -1,13 +1,18 @@ -#ifndef SOURCEMETA_ONE_SERVER_SEARCH_H -#define SOURCEMETA_ONE_SERVER_SEARCH_H +#ifndef SOURCEMETA_ONE_SERVER_ACTION_SCHEMA_SEARCH_H +#define SOURCEMETA_ONE_SERVER_ACTION_SCHEMA_SEARCH_H #include #include +#include "helpers.h" +#include "request.h" +#include "response.h" + #include // std::search #include // assert #include // std::filesystem +#include // std::ostringstream #include // std::string, std::getline #include // std::string_view @@ -52,4 +57,34 @@ static auto search(const std::filesystem::path &search_index, } // namespace sourcemeta::one +static auto action_schema_search(const std::filesystem::path &base, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + if (request.method() == "get") { + const auto query{request.query("q")}; + if (query.empty()) { + json_error(request, response, sourcemeta::one::STATUS_BAD_REQUEST, + "missing-query", + "You must provide a query parameter to search for"); + } else { + auto result{sourcemeta::one::search( + base / "explorer" / SENTINEL / "search.metapack", query)}; + response.write_status(sourcemeta::one::STATUS_OK); + response.write_header("Access-Control-Allow-Origin", "*"); + response.write_header("Content-Type", "application/json"); + write_link_header(response, + "/self/v1/schemas/api/schemas/search/response"); + std::ostringstream output; + sourcemeta::core::prettify(result, output); + send_response(sourcemeta::one::STATUS_OK, request, response, output.str(), + sourcemeta::one::Encoding::Identity); + } + } else { + json_error(request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, + "method-not-allowed", + "This HTTP method is invalid for this URL"); + } +} + #endif diff --git a/src/server/action_serve_metapack_file.h b/src/server/action_serve_metapack_file.h new file mode 100644 index 00000000..c1fe3af8 --- /dev/null +++ b/src/server/action_serve_metapack_file.h @@ -0,0 +1,119 @@ +#ifndef SOURCEMETA_ONE_SERVER_ACTION_SERVE_METAPACK_FILE_H +#define SOURCEMETA_ONE_SERVER_ACTION_SERVE_METAPACK_FILE_H + +#include + +#include + +#include "helpers.h" +#include "request.h" +#include "response.h" + +#include // std::chrono::seconds +#include // std::filesystem +#include // std::optional +#include // std::ostringstream +#include // std::string + +static auto action_serve_metapack_file( + const sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response, + const std::filesystem::path &absolute_path, const char *const code, + const bool enable_cors = false, + const std::optional &mime = std::nullopt, + const std::optional &link = std::nullopt) -> void { + if (request.method() != "get" && request.method() != "head") { + json_error(request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, + "method-not-allowed", + "This HTTP method is invalid for this URL"); + return; + } + + auto file{sourcemeta::one::read_stream_raw(absolute_path)}; + if (!file.has_value()) { + json_error(request, response, sourcemeta::one::STATUS_NOT_FOUND, + "not-found", "There is nothing at this URL"); + return; + } + + // Note that `If-Modified-Since` can only be used with a `GET` or `HEAD`. + // See + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since + const auto if_modified_since{request.header_gmt("if-modified-since")}; + // Time comparison can be flaky, but adding a bit of tolerance leads + // to more consistent behavior. + if (if_modified_since.has_value() && + (if_modified_since.value() + std::chrono::seconds(1)) >= + file.value().last_modified) { + response.write_status(sourcemeta::one::STATUS_NOT_MODIFIED); + if (enable_cors) { + response.write_header("Access-Control-Allow-Origin", "*"); + } + + send_response(sourcemeta::one::STATUS_NOT_MODIFIED, request, response); + return; + } + + const auto &checksum{file.value().checksum}; + std::ostringstream etag_value_strong; + std::ostringstream etag_value_weak; + etag_value_strong << '"' << checksum << '"'; + etag_value_weak << 'W' << '/' << '"' << checksum << '"'; + for (const auto &match : request.header_list("if-none-match")) { + // Cache hit + if (match.first == "*" || match.first == etag_value_weak.str() || + match.first == etag_value_strong.str()) { + response.write_status(sourcemeta::one::STATUS_NOT_MODIFIED); + if (enable_cors) { + response.write_header("Access-Control-Allow-Origin", "*"); + } + + send_response(sourcemeta::one::STATUS_NOT_MODIFIED, request, response); + return; + } + } + + response.write_status(code); + + // To support requests from web browsers + if (enable_cors) { + response.write_header("Access-Control-Allow-Origin", "*"); + } + + if (mime.has_value()) { + response.write_header("Content-Type", mime.value()); + } else { + response.write_header("Content-Type", file.value().mime); + } + + response.write_header("Last-Modified", + sourcemeta::core::to_gmt(file.value().last_modified)); + + std::ostringstream etag; + etag << '"' << checksum << '"'; + response.write_header("ETag", std::move(etag).str()); + + // See + // https://json-schema.org/draft/2020-12/json-schema-core.html#section-9.5.1.1 + if (link.has_value()) { + write_link_header(response, link.value()); + } else { + const auto &dialect{file.value().extension}; + if (dialect.is_string()) { + write_link_header(response, dialect.to_string()); + } + } + + std::ostringstream contents; + contents << file.value().data.rdbuf(); + + if (file.value().encoding == sourcemeta::one::Encoding::GZIP) { + send_response(code, request, response, contents.str(), + sourcemeta::one::Encoding::GZIP); + } else { + send_response(code, request, response, contents.str(), + sourcemeta::one::Encoding::Identity); + } +} + +#endif diff --git a/src/server/helpers.h b/src/server/helpers.h index aa37afaa..8f13a223 100644 --- a/src/server/helpers.h +++ b/src/server/helpers.h @@ -4,19 +4,15 @@ #include #include -#include #include #include "request.h" -#include "status.h" -#include "uwebsockets.h" +#include "response.h" #include // assert #include // std::chrono::system_clock -#include // std::filesystem #include // std::cerr #include // std::mutex, std::lock_guard -#include // std::optional #include // std::ostringstream #include // std::string #include // std::string_view @@ -25,11 +21,11 @@ constexpr auto SENTINEL{"%"}; -static auto write_link_header(uWS::HttpResponse *response, +static auto write_link_header(sourcemeta::one::HTTPResponse &response, const std::string_view schema_path) -> void { std::ostringstream link; link << "<" << schema_path << ">; rel=\"describedby\""; - response->writeHeader("Link", std::move(link).str()); + response.write_header("Link", std::move(link).str()); } static auto log(std::string_view message) -> void { @@ -40,72 +36,32 @@ static auto log(std::string_view message) -> void { << "] " << std::this_thread::get_id() << ' ' << message << "\n"; } -static auto send_response(const char *const code, const std::string_view method, - const std::string_view url, - uWS::HttpResponse *response) -> void { - response->end(); +static auto send_response(const char *const code, + const sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) -> void { + response.send_without_content(); std::ostringstream line; assert(code); - line << code << ' ' << method << ' ' << url; + line << code << ' ' << request.method() << ' ' << request.path(); log(std::move(line).str()); } -static auto -send_response(const char *const code, const std::string_view method, - const std::string_view url, uWS::HttpResponse *response, - const std::string &message, - const sourcemeta::one::HTTPRequest::Encoding expected_encoding, - const sourcemeta::one::HTTPRequest::Encoding current_encoding) +static auto send_response(const char *const code, + const sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response, + const std::string &message, + const sourcemeta::one::Encoding current_encoding) -> void { - if (expected_encoding == sourcemeta::one::HTTPRequest::Encoding::GZIP) { - response->writeHeader("Content-Encoding", "gzip"); - if (current_encoding == sourcemeta::one::HTTPRequest::Encoding::Identity) { - auto effective_message{sourcemeta::one::gzip(message)}; - if (method == "head") { - response->endWithoutBody(effective_message.size()); - response->end(); - } else { - response->end(std::move(effective_message)); - } - } else { - if (method == "head") { - response->endWithoutBody(message.size()); - response->end(); - } else { - response->end(message); - } - } - } else if (expected_encoding == - sourcemeta::one::HTTPRequest::Encoding::Identity) { - if (current_encoding == sourcemeta::one::HTTPRequest::Encoding::GZIP) { - auto effective_message{sourcemeta::one::gunzip(message)}; - if (method == "head") { - response->endWithoutBody(effective_message.size()); - response->end(); - } else { - response->end(effective_message); - } - } else { - if (method == "head") { - response->endWithoutBody(message.size()); - response->end(); - } else { - response->end(message); - } - } - } - + response.send(request, message, current_encoding); std::ostringstream line; assert(code); - line << code << ' ' << method << ' ' << url; + line << code << ' ' << request.method() << ' ' << request.path(); log(std::move(line).str()); } // See https://www.rfc-editor.org/rfc/rfc7807 -static auto json_error(const std::string_view method, - const std::string_view url, - uWS::HttpResponse *response, - const sourcemeta::one::HTTPRequest::Encoding encoding, +static auto json_error(const sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response, const char *const code, std::string &&id, std::string &&message) -> void { auto object{sourcemeta::core::JSON::make_object()}; @@ -114,131 +70,15 @@ static auto json_error(const std::string_view method, sourcemeta::core::JSON{"sourcemeta:one/" + std::move(id)}); object.assign("status", sourcemeta::core::JSON{std::atoi(code)}); object.assign("detail", sourcemeta::core::JSON{std::move(message)}); - response->writeStatus(code); - response->writeHeader("Content-Type", "application/problem+json"); - response->writeHeader("Access-Control-Allow-Origin", "*"); + response.write_status(code); + response.write_header("Content-Type", "application/problem+json"); + response.write_header("Access-Control-Allow-Origin", "*"); write_link_header(response, "/self/v1/schemas/api/error"); std::ostringstream output; sourcemeta::core::prettify(object, output); - send_response(code, method, url, response, output.str(), encoding, - sourcemeta::one::HTTPRequest::Encoding::Identity); -} - -static auto -serve_static_file(const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response, - const std::filesystem::path &absolute_path, - const char *const code, const bool enable_cors = false, - const std::optional &mime = std::nullopt, - const std::optional &link = std::nullopt) - -> void { - if (request.method() != "get" && request.method() != "head") { - if (std::filesystem::exists(absolute_path)) { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, - "method-not-allowed", - "This HTTP method is invalid for this URL"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), sourcemeta::one::STATUS_NOT_FOUND, - "not-found", "There is nothing at this URL"); - } - - return; - } - - auto file{sourcemeta::one::read_stream_raw(absolute_path)}; - if (!file.has_value()) { - json_error(request.method(), request.path(), response, - request.response_encoding(), sourcemeta::one::STATUS_NOT_FOUND, - "not-found", "There is nothing at this URL"); - return; - } - - // Note that `If-Modified-Since` can only be used with a `GET` or `HEAD`. - // See - // https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/If-Modified-Since - const auto if_modified_since{request.header_gmt("if-modified-since")}; - // Time comparison can be flaky, but adding a bit of tolerance leads - // to more consistent behavior. - if (if_modified_since.has_value() && - (if_modified_since.value() + std::chrono::seconds(1)) >= - file.value().last_modified) { - response->writeStatus(sourcemeta::one::STATUS_NOT_MODIFIED); - if (enable_cors) { - response->writeHeader("Access-Control-Allow-Origin", "*"); - } - - send_response(sourcemeta::one::STATUS_NOT_MODIFIED, request.method(), - request.path(), response); - return; - } - - const auto &checksum{file.value().checksum}; - std::ostringstream etag_value_strong; - std::ostringstream etag_value_weak; - etag_value_strong << '"' << checksum << '"'; - etag_value_weak << 'W' << '/' << '"' << checksum << '"'; - for (const auto &match : request.header_list("if-none-match")) { - // Cache hit - if (match.first == "*" || match.first == etag_value_weak.str() || - match.first == etag_value_strong.str()) { - response->writeStatus(sourcemeta::one::STATUS_NOT_MODIFIED); - if (enable_cors) { - response->writeHeader("Access-Control-Allow-Origin", "*"); - } - - send_response(sourcemeta::one::STATUS_NOT_MODIFIED, request.method(), - request.path(), response); - return; - } - } - - response->writeStatus(code); - - // To support requests from web browsers - if (enable_cors) { - response->writeHeader("Access-Control-Allow-Origin", "*"); - } - - if (mime.has_value()) { - response->writeHeader("Content-Type", mime.value()); - } else { - response->writeHeader("Content-Type", file.value().mime); - } - - response->writeHeader("Last-Modified", - sourcemeta::core::to_gmt(file.value().last_modified)); - - std::ostringstream etag; - etag << '"' << checksum << '"'; - response->writeHeader("ETag", std::move(etag).str()); - - // See - // https://json-schema.org/draft/2020-12/json-schema-core.html#section-9.5.1.1 - if (link.has_value()) { - write_link_header(response, link.value()); - } else { - const auto &dialect{file.value().extension}; - if (dialect.is_string()) { - write_link_header(response, dialect.to_string()); - } - } - - std::ostringstream contents; - contents << file.value().data.rdbuf(); - - if (file.value().encoding == sourcemeta::one::Encoding::GZIP) { - send_response(code, request.method(), request.path(), response, - contents.str(), request.response_encoding(), - sourcemeta::one::HTTPRequest::Encoding::GZIP); - } else { - send_response(code, request.method(), request.path(), response, - contents.str(), request.response_encoding(), - sourcemeta::one::HTTPRequest::Encoding::Identity); - } + send_response(code, request, response, output.str(), + sourcemeta::one::Encoding::Identity); } #endif diff --git a/src/server/request.h b/src/server/request.h index 381e5d40..5097da6b 100644 --- a/src/server/request.h +++ b/src/server/request.h @@ -3,10 +3,13 @@ #include +#include "response.h" #include "uwebsockets.h" #include // std::sort #include // std::chrono::system_clock +#include // std::exception_ptr, std::current_exception +#include // std::shared_ptr, std::make_shared #include // std::optional #include // std::istringstream #include // std::invalid_argument @@ -19,9 +22,17 @@ namespace sourcemeta::one { class HTTPRequest { public: - enum class Encoding { Identity, GZIP }; - - HTTPRequest(uWS::HttpRequest *request) noexcept : request_{request} {} + // Primary constructor from raw uWebSockets pointers + HTTPRequest(uWS::HttpRequest *request, + uWS::HttpResponse *response) noexcept + : request_{request}, response_{response} {} + + // Snapshot constructor for async contexts where uWS::HttpRequest is gone + HTTPRequest(std::string method, std::string path, + sourcemeta::one::Encoding encoding, + uWS::HttpResponse *response) noexcept + : request_{nullptr}, response_{response}, method_{std::move(method)}, + path_{std::move(path)}, response_encoding_{encoding} {} // If the identity;q=0 or *;q=0 directives explicitly forbid the identity // encoding, the server should return a 406 Not Acceptable error. See @@ -47,18 +58,18 @@ class HTTPRequest { // equivalent to "gzip". See // https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.5 rule.first == "x-gzip") { - this->response_encoding_ = Encoding::GZIP; + this->response_encoding_ = sourcemeta::one::Encoding::GZIP; break; } } } auto method() const noexcept -> std::string_view { - return this->request_->getMethod(); + return this->request_ ? this->request_->getMethod() : this->method_; } auto path() const noexcept -> std::string_view { - return this->request_->getUrl(); + return this->request_ ? this->request_->getUrl() : this->path_; } auto header(const std::string_view name) const noexcept -> std::string_view { @@ -136,14 +147,75 @@ class HTTPRequest { return this->satisfiable_encoding_; } - auto response_encoding() const noexcept -> Encoding { + auto response_encoding() const noexcept -> sourcemeta::one::Encoding { return this->response_encoding_; } + // Read the entire request body asynchronously. + // - callback: Invoked with (response, body, too_big) on completion + // - on_error: Invoked with (response, exception_ptr) on any exception, + // including exceptions thrown by the main callback + // - max_size: Maximum body size in bytes (default 1MB) + // Note: If the request is aborted, the callback is not invoked + template + auto body(Callback callback, ErrorCallback on_error, + std::size_t max_size = 1024 * 1024) -> void { + auto raw_response = this->response_; + auto snapshot = std::make_shared( + std::string{this->method()}, std::string{this->path()}, + this->response_encoding_, raw_response); + auto buffer = std::make_shared(); + auto completed = std::make_shared(false); + + raw_response->onAborted([completed]() mutable { *completed = true; }); + + raw_response->onData( + [raw_response, snapshot, buffer, completed, max_size, callback, + on_error](std::string_view chunk, bool is_last) mutable { + if (*completed) { + return; + } + + try { + if (buffer->size() + chunk.size() > max_size) { + *completed = true; + HTTPResponse response{raw_response}; + try { + callback(*snapshot, response, std::move(*buffer), true); + } catch (...) { + on_error(*snapshot, response, std::current_exception()); + } + + return; + } + + buffer->append(chunk); + + if (is_last) { + *completed = true; + HTTPResponse response{raw_response}; + try { + callback(*snapshot, response, std::move(*buffer), false); + } catch (...) { + on_error(*snapshot, response, std::current_exception()); + } + } + } catch (...) { + *completed = true; + HTTPResponse response{raw_response}; + on_error(*snapshot, response, std::current_exception()); + } + }); + } + private: uWS::HttpRequest *request_; + uWS::HttpResponse *response_; + std::string method_; + std::string path_; bool satisfiable_encoding_{true}; - Encoding response_encoding_{Encoding::Identity}; + sourcemeta::one::Encoding response_encoding_{ + sourcemeta::one::Encoding::Identity}; }; } // namespace sourcemeta::one diff --git a/src/server/status.h b/src/server/response.h similarity index 64% rename from src/server/status.h rename to src/server/response.h index 1e2df0d9..729ac9eb 100644 --- a/src/server/status.h +++ b/src/server/response.h @@ -1,5 +1,14 @@ -#ifndef SOURCEMETA_ONE_SERVER_STATUS_H -#define SOURCEMETA_ONE_SERVER_STATUS_H +#ifndef SOURCEMETA_ONE_SERVER_RESPONSE_H +#define SOURCEMETA_ONE_SERVER_RESPONSE_H + +#include +#include + +#include "uwebsockets.h" + +#include // std::string +#include // std::string_view +#include // std::move namespace sourcemeta::one { @@ -81,6 +90,73 @@ const char *STATUS_NOT_EXTENDED = "510 Not Extended"; const char *STATUS_NETWORK_AUTHENTICATION_REQUIRED = "511 Network Authentication Required"; +class HTTPResponse { +public: + HTTPResponse(uWS::HttpResponse *response) noexcept + : response_{response} {} + + auto write_status(const char *status) -> void { + this->response_->writeStatus(status); + } + + auto write_header(const std::string_view name, const std::string_view value) + -> void { + this->response_->writeHeader(name, value); + } + + auto handle() noexcept -> uWS::HttpResponse * { + return this->response_; + } + + auto send_without_content() -> void { this->response_->end(); } + + template + auto send(const Request &request, const std::string &message, + const Encoding current_encoding) -> void { + const auto method{request.method()}; + const auto expected_encoding{request.response_encoding()}; + if (expected_encoding == Encoding::GZIP) { + this->response_->writeHeader("Content-Encoding", "gzip"); + if (current_encoding == Encoding::Identity) { + auto effective_message{gzip(message)}; + if (method == "head") { + this->response_->endWithoutBody(effective_message.size()); + this->response_->end(); + } else { + this->response_->end(std::move(effective_message)); + } + } else { + if (method == "head") { + this->response_->endWithoutBody(message.size()); + this->response_->end(); + } else { + this->response_->end(message); + } + } + } else if (expected_encoding == Encoding::Identity) { + if (current_encoding == Encoding::GZIP) { + auto effective_message{gunzip(message)}; + if (method == "head") { + this->response_->endWithoutBody(effective_message.size()); + this->response_->end(); + } else { + this->response_->end(effective_message); + } + } else { + if (method == "head") { + this->response_->endWithoutBody(message.size()); + this->response_->end(); + } else { + this->response_->end(message); + } + } + } + } + +private: + uWS::HttpResponse *response_; +}; + } // namespace sourcemeta::one #endif diff --git a/src/server/server.cc b/src/server/server.cc index c3b2690c..7dfc32ee 100644 --- a/src/server/server.cc +++ b/src/server/server.cc @@ -1,393 +1,200 @@ -#include #include -#include "evaluate.h" +#include "action_jsonschema_evaluate.h" +#include "action_jsonschema_serve.h" +#include "action_schema_search.h" +#include "action_serve_metapack_file.h" + #include "helpers.h" -#include "request.h" -#include "search.h" #include // std::array -#include // std::tolower #include // std::signal, SIGINT, SIGTERM #include // std::uint32_t, std::stoul #include // EXIT_FAILURE, std::exit #include // std::filesystem #include // std::cout #include // std::numeric_limits -#include // std::unique_ptr #include // std::span #include // std::ostringstream #include // std::string #include // std::string_view -static auto on_evaluate(const std::filesystem::path &base, - const std::string_view &path, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response, - const sourcemeta::one::EvaluateType mode) -> void { - // A CORS pre-flight request - if (request.method() == "options") { - response->writeStatus(sourcemeta::one::STATUS_NO_CONTENT); - response->writeHeader("Access-Control-Allow-Origin", "*"); - response->writeHeader("Access-Control-Allow-Methods", "POST, OPTIONS"); - response->writeHeader("Access-Control-Allow-Headers", "Content-Type"); - response->writeHeader("Access-Control-Max-Age", "3600"); - send_response(sourcemeta::one::STATUS_NO_CONTENT, request.method(), - request.path(), response); - } else if (request.method() == "post") { - auto template_path{base / "schemas"}; - template_path /= path; - template_path /= SENTINEL; - template_path /= "blaze-exhaustive.metapack"; - if (!std::filesystem::exists(template_path)) { - const auto schema_path{template_path.parent_path() / "schema.metapack"}; - if (std::filesystem::exists(schema_path)) { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "no-template", - "This schema was not precompiled for schema evaluation"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_NOT_FOUND, "not-found", - "There is nothing at this URL"); - } - - return; - } - - response->onAborted([]() {}); - std::unique_ptr buffer; - // Because `request` gets de-allocated - std::string url{request.path()}; - const auto encoding{request.response_encoding()}; - response->onData([response, encoding, mode, buffer = std::move(buffer), - template_path = std::move(template_path), - url = std::move(url)](const std::string_view chunk, - const bool is_last) mutable { - try { - if (!buffer.get()) { - buffer = std::make_unique(chunk); - } else { - buffer->append(chunk); - } - - if (is_last) { - if (buffer->empty()) { - json_error("post", url, response, encoding, - sourcemeta::one::STATUS_BAD_REQUEST, "no-instance", - "You must pass an instance to validate against"); - } else { - const auto result{ - sourcemeta::one::evaluate(template_path, *buffer, mode)}; - response->writeStatus(sourcemeta::one::STATUS_OK); - response->writeHeader("Content-Type", "application/json"); - response->writeHeader("Access-Control-Allow-Origin", "*"); - if (mode == sourcemeta::one::EvaluateType::Trace) { - write_link_header(response, - "/self/v1/schemas/api/schemas/trace/response"); - } else { - write_link_header( - response, "/self/v1/schemas/api/schemas/evaluate/response"); - } - std::ostringstream payload; - sourcemeta::core::prettify(result, payload); - send_response(sourcemeta::one::STATUS_OK, "post", url, response, - payload.str(), encoding, - sourcemeta::one::HTTPRequest::Encoding::Identity); - } - } - } catch (const std::exception &error) { - json_error("post", url, response, encoding, - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "uncaught-error", - error.what()); - } - }); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } -} - -static auto on_schema(const std::filesystem::path &base, - const std::string_view &path, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - // Otherwise we may get unexpected results in case-sensitive file-systems - std::string lowercase_path{path}; - std::transform( - lowercase_path.begin(), lowercase_path.end(), lowercase_path.begin(), - [](const unsigned char character) { return std::tolower(character); }); - - // Because Visual Studio Code famously does not support `$id` or `id` - // See - // https://github.com/microsoft/vscode-json-languageservice/issues/224 - const auto &user_agent{request.header("user-agent")}; - const auto is_vscode{user_agent.starts_with("Visual Studio Code") || - user_agent.starts_with("VSCodium")}; - const auto is_deno{user_agent.starts_with("Deno/")}; - const auto bundle{!request.query("bundle").empty()}; - auto absolute_path{base / "schemas" / lowercase_path / SENTINEL}; - if (is_vscode) { - absolute_path /= "editor.metapack"; - } else if (bundle || is_deno) { - absolute_path /= "bundle.metapack"; - } else { - absolute_path /= "schema.metapack"; - } - - if (is_deno) { - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, - // For HTTP imports, as Deno won't like the - // `application/schema+json` one - "application/json"); - } else { - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true); - } -} - -static auto handle_root(const std::filesystem::path &base, - const std::span, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.prefers_html()) { - serve_static_file(request, response, - base / "explorer" / SENTINEL / "directory-html.metapack", - sourcemeta::one::STATUS_OK); - } else if (request.method() == "get" || request.method() == "head") { - json_error(request.method(), request.path(), response, - request.response_encoding(), sourcemeta::one::STATUS_NOT_FOUND, - "not-found", "There is nothing at this URL"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } -} - static auto handle_self_v1_api_list(const std::filesystem::path &base, const std::span, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - serve_static_file(request, response, - base / "explorer" / SENTINEL / "directory.metapack", - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/list/response"); + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + action_serve_metapack_file( + request, response, base / "explorer" / SENTINEL / "directory.metapack", + sourcemeta::one::STATUS_OK, true, std::nullopt, + "/self/v1/schemas/api/list/response"); } static auto handle_self_v1_api_list_path(const std::filesystem::path &base, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) -> void { const auto absolute_path{base / "explorer" / matches.front() / SENTINEL / "directory.metapack"}; - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/list/response"); + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true, std::nullopt, + "/self/v1/schemas/api/list/response"); } static auto handle_self_v1_api_schemas_dependencies( const std::filesystem::path &base, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.method() == "get" || request.method() == "head") { - const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / - "dependencies.metapack"}; - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/schemas/dependencies/response"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) -> void { + const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / + "dependencies.metapack"}; + action_serve_metapack_file( + request, response, absolute_path, sourcemeta::one::STATUS_OK, true, + std::nullopt, "/self/v1/schemas/api/schemas/dependencies/response"); } static auto handle_self_v1_api_schemas_health(const std::filesystem::path &base, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.method() == "get" || request.method() == "head") { - const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / - "health.metapack"}; - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/schemas/health/response"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / + "health.metapack"}; + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true, std::nullopt, + "/self/v1/schemas/api/schemas/health/response"); } -static auto handle_self_v1_api_schemas_locations( - const std::filesystem::path &base, - const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.method() == "get" || request.method() == "head") { - const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / - "locations.metapack"}; - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/schemas/locations/response"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } +static auto +handle_self_v1_api_schemas_locations(const std::filesystem::path &base, + const std::span matches, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / + "locations.metapack"}; + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true, std::nullopt, + "/self/v1/schemas/api/schemas/locations/response"); } -static auto handle_self_v1_api_schemas_positions( - const std::filesystem::path &base, - const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.method() == "get" || request.method() == "head") { - const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / - "positions.metapack"}; - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/schemas/positions/response"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } +static auto +handle_self_v1_api_schemas_positions(const std::filesystem::path &base, + const std::span matches, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / + "positions.metapack"}; + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true, std::nullopt, + "/self/v1/schemas/api/schemas/positions/response"); } static auto handle_self_v1_api_schemas_stats(const std::filesystem::path &base, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.method() == "get" || request.method() == "head") { - const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / - "stats.metapack"}; - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/schemas/stats/response"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + const auto absolute_path{base / "schemas" / matches.front() / SENTINEL / + "stats.metapack"}; + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true, std::nullopt, + "/self/v1/schemas/api/schemas/stats/response"); } static auto handle_self_v1_api_schemas_metadata(const std::filesystem::path &base, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.method() == "get" || request.method() == "head") { - const auto absolute_path{base / "explorer" / matches.front() / SENTINEL / - "schema.metapack"}; - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK, true, std::nullopt, - "/self/v1/schemas/api/schemas/metadata/response"); - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + const auto absolute_path{base / "explorer" / matches.front() / SENTINEL / + "schema.metapack"}; + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK, true, std::nullopt, + "/self/v1/schemas/api/schemas/metadata/response"); } static auto handle_self_v1_api_schemas_evaluate(const std::filesystem::path &base, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - on_evaluate(base, matches.front(), request, response, - sourcemeta::one::EvaluateType::Standard); + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + action_jsonschema_evaluate(base, matches.front(), request, response, + sourcemeta::one::EvaluateType::Standard); } static auto handle_self_v1_api_schemas_trace(const std::filesystem::path &base, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - on_evaluate(base, matches.front(), request, response, - sourcemeta::one::EvaluateType::Trace); + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + action_jsonschema_evaluate(base, matches.front(), request, response, + sourcemeta::one::EvaluateType::Trace); } -static auto -handle_self_v1_api_schemas_search(const std::filesystem::path &base, - const std::span, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - if (request.method() == "get") { - const auto query{request.query("q")}; - if (query.empty()) { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_BAD_REQUEST, "missing-query", - "You must provide a query parameter to search for"); - } else { - auto result{sourcemeta::one::search( - base / "explorer" / SENTINEL / "search.metapack", query)}; - response->writeStatus(sourcemeta::one::STATUS_OK); - response->writeHeader("Access-Control-Allow-Origin", "*"); - response->writeHeader("Content-Type", "application/json"); - write_link_header(response, - "/self/v1/schemas/api/schemas/search/response"); - std::ostringstream output; - sourcemeta::core::prettify(result, output); - send_response(sourcemeta::one::STATUS_OK, request.method(), - request.path(), response, output.str(), - request.response_encoding(), - sourcemeta::one::HTTPRequest::Encoding::Identity); - } - } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "method-not-allowed", - "This HTTP method is invalid for this URL"); - } +static auto handle_self_v1_api_schemas_search( + const std::filesystem::path &base, const std::span, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) -> void { + action_schema_search(base, request, response); } -static auto -handle_self_api_not_found(const std::filesystem::path &, - const std::span, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { - json_error(request.method(), request.path(), response, - request.response_encoding(), sourcemeta::one::STATUS_NOT_FOUND, - "not-found", "There is nothing at this URL"); +static auto handle_self_api_not_found(const std::filesystem::path &, + const std::span, + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { + json_error(request, response, sourcemeta::one::STATUS_NOT_FOUND, "not-found", + "There is nothing at this URL"); } static auto handle_self_static(const std::filesystem::path &, const std::span matches, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) + -> void { std::ostringstream absolute_path; absolute_path << SOURCEMETA_ONE_STATIC; absolute_path << '/'; absolute_path << matches.front(); - serve_static_file(request, response, absolute_path.str(), - sourcemeta::one::STATUS_OK); + action_serve_metapack_file(request, response, absolute_path.str(), + sourcemeta::one::STATUS_OK); } static auto handle_default(const std::filesystem::path &base, const std::span, - const sourcemeta::one::HTTPRequest &request, - uWS::HttpResponse *response) -> void { + sourcemeta::one::HTTPRequest &request, + sourcemeta::one::HTTPResponse &response) -> void { + if (request.path() == "/") { + if (request.prefers_html()) { + action_serve_metapack_file(request, response, + base / "explorer" / SENTINEL / + "directory-html.metapack", + sourcemeta::one::STATUS_OK); + return; + } else if (request.method() == "get" || request.method() == "head") { + json_error(request, response, sourcemeta::one::STATUS_NOT_FOUND, + "not-found", "There is nothing at this URL"); + return; + } else { + json_error(request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, + "method-not-allowed", + "This HTTP method is invalid for this URL"); + return; + } + } + if (request.path().ends_with(".json")) { - on_schema(base, request.path().substr(1, request.path().size() - 6), - request, response); + action_jsonschema_serve(base, + request.path().substr(1, request.path().size() - 6), + request, response); return; } @@ -396,37 +203,35 @@ static auto handle_default(const std::filesystem::path &base, if (request.prefers_html()) { auto absolute_path{base / "explorer" / path / SENTINEL}; if (std::filesystem::exists(absolute_path / "schema-html.metapack")) { - serve_static_file(request, response, - absolute_path / "schema-html.metapack", - sourcemeta::one::STATUS_OK); + action_serve_metapack_file(request, response, + absolute_path / "schema-html.metapack", + sourcemeta::one::STATUS_OK); } else { absolute_path /= "directory-html.metapack"; if (std::filesystem::exists(absolute_path)) { - serve_static_file(request, response, absolute_path, - sourcemeta::one::STATUS_OK); + action_serve_metapack_file(request, response, absolute_path, + sourcemeta::one::STATUS_OK); } else { - serve_static_file(request, response, - base / "explorer" / SENTINEL / "404.metapack", - sourcemeta::one::STATUS_NOT_FOUND); + action_serve_metapack_file( + request, response, base / "explorer" / SENTINEL / "404.metapack", + sourcemeta::one::STATUS_NOT_FOUND); } } } else { - on_schema(base, path, request, response); + action_jsonschema_serve(base, path, request, response); } } else { - json_error(request.method(), request.path(), response, - request.response_encoding(), sourcemeta::one::STATUS_NOT_FOUND, + json_error(request, response, sourcemeta::one::STATUS_NOT_FOUND, "not-found", "There is nothing at this URL"); } } using Handler = auto (*)(const std::filesystem::path &, const std::span, - const sourcemeta::one::HTTPRequest &, - uWS::HttpResponse *) -> void; + sourcemeta::one::HTTPRequest &, + sourcemeta::one::HTTPResponse &) -> void; static const Handler HANDLERS[] = {handle_default, - handle_root, handle_self_v1_api_list, handle_self_v1_api_list_path, handle_self_v1_api_schemas_dependencies, @@ -443,9 +248,10 @@ static const Handler HANDLERS[] = {handle_default, static auto dispatch(const sourcemeta::core::URITemplateRouter &router, const std::filesystem::path &base, - uWS::HttpResponse *const response, + uWS::HttpResponse *const raw_response, uWS::HttpRequest *const raw_request) noexcept -> void { - sourcemeta::one::HTTPRequest request{raw_request}; + sourcemeta::one::HTTPResponse response{raw_response}; + sourcemeta::one::HTTPRequest request{raw_request, raw_response}; try { request.negotiate(); if (request.satisfiable_encoding()) { @@ -461,17 +267,13 @@ static auto dispatch(const sourcemeta::core::URITemplateRouter &router, HANDLERS[handler](base, matches, request, response); } else { - json_error(request.method(), request.path(), response, - sourcemeta::one::HTTPRequest::Encoding::Identity, - sourcemeta::one::STATUS_NOT_ACCEPTABLE, + json_error(request, response, sourcemeta::one::STATUS_NOT_ACCEPTABLE, "cannot-satisfy-content-encoding", "The server cannot satisfy the request content encoding"); } } catch (const std::exception &error) { - json_error(request.method(), request.path(), response, - sourcemeta::one::HTTPRequest::Encoding::Identity, - sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, "uncaught-error", - error.what()); + json_error(request, response, sourcemeta::one::STATUS_METHOD_NOT_ALLOWED, + "uncaught-error", error.what()); } } @@ -507,8 +309,8 @@ auto main(int argc, char *argv[]) noexcept -> int { const auto is_headless{!std::filesystem::exists( base / "explorer" / SENTINEL / "directory-html.metapack")}; + // TODO: Restore this from a URI Template binary view sourcemeta::core::URITemplateRouter router; - router.add("/", sourcemeta::one::HANDLER_ROOT); router.add("/self/v1/api/list", sourcemeta::one::HANDLER_SELF_V1_API_LIST); router.add("/self/v1/api/list/{+path}", sourcemeta::one::HANDLER_SELF_V1_API_LIST_PATH); diff --git a/src/shared/include/sourcemeta/one/shared.h b/src/shared/include/sourcemeta/one/shared.h index e36768be..958cb708 100644 --- a/src/shared/include/sourcemeta/one/shared.h +++ b/src/shared/include/sourcemeta/one/shared.h @@ -10,20 +10,19 @@ namespace sourcemeta::one { -constexpr auto HANDLER_ROOT = 1; -constexpr auto HANDLER_SELF_V1_API_LIST = 2; -constexpr auto HANDLER_SELF_V1_API_LIST_PATH = 3; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_DEPENDENCIES = 4; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_HEALTH = 5; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_LOCATIONS = 6; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_POSITIONS = 7; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_STATS = 8; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_METADATA = 9; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_EVALUATE = 10; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_TRACE = 11; -constexpr auto HANDLER_SELF_V1_API_SCHEMAS_SEARCH = 12; -constexpr auto HANDLER_SELF_V1_API_DEFAULT = 13; -constexpr auto HANDLER_SELF_STATIC = 14; +constexpr auto HANDLER_SELF_V1_API_LIST = 1; +constexpr auto HANDLER_SELF_V1_API_LIST_PATH = 2; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_DEPENDENCIES = 3; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_HEALTH = 4; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_LOCATIONS = 5; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_POSITIONS = 6; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_STATS = 7; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_METADATA = 8; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_EVALUATE = 9; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_TRACE = 10; +constexpr auto HANDLER_SELF_V1_API_SCHEMAS_SEARCH = 11; +constexpr auto HANDLER_SELF_V1_API_DEFAULT = 12; +constexpr auto HANDLER_SELF_STATIC = 13; } // namespace sourcemeta::one