Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion src/server/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)

Expand Down
111 changes: 109 additions & 2 deletions src/server/evaluate.h → src/server/action_jsonschema_evaluate.h
Original file line number Diff line number Diff line change
@@ -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 <sourcemeta/core/json.h>
#include <sourcemeta/core/jsonpointer.h>
Expand All @@ -9,8 +9,14 @@

#include <sourcemeta/one/shared.h>

#include "helpers.h"
#include "request.h"
#include "response.h"

#include <cassert> // assert
#include <filesystem> // std::filesystem::path
#include <sstream> // std::ostringstream
#include <string_view> // std::string_view
#include <type_traits> // std::underlying_type_t
#include <utility> // std::move

Expand Down Expand Up @@ -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
64 changes: 64 additions & 0 deletions src/server/action_jsonschema_serve.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#ifndef SOURCEMETA_ONE_SERVER_ACTION_JSONSCHEMA_SERVE_H
#define SOURCEMETA_ONE_SERVER_ACTION_JSONSCHEMA_SERVE_H

#include <sourcemeta/one/shared.h>

#include "action_serve_metapack_file.h"
#include "helpers.h"
#include "request.h"
#include "response.h"

#include <algorithm> // std::transform
#include <cctype> // std::tolower
#include <filesystem> // std::filesystem
#include <string> // std::string
#include <string_view> // 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
39 changes: 37 additions & 2 deletions src/server/search.h → src/server/action_schema_search.h
Original file line number Diff line number Diff line change
@@ -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 <sourcemeta/core/json.h>

#include <sourcemeta/one/shared.h>

#include "helpers.h"
#include "request.h"
#include "response.h"

#include <algorithm> // std::search
#include <cassert> // assert
#include <filesystem> // std::filesystem
#include <sstream> // std::ostringstream
#include <string> // std::string, std::getline
#include <string_view> // std::string_view

Expand Down Expand Up @@ -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
119 changes: 119 additions & 0 deletions src/server/action_serve_metapack_file.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
#ifndef SOURCEMETA_ONE_SERVER_ACTION_SERVE_METAPACK_FILE_H
#define SOURCEMETA_ONE_SERVER_ACTION_SERVE_METAPACK_FILE_H

#include <sourcemeta/core/time.h>

#include <sourcemeta/one/shared.h>

#include "helpers.h"
#include "request.h"
#include "response.h"

#include <chrono> // std::chrono::seconds
#include <filesystem> // std::filesystem
#include <optional> // std::optional
#include <sstream> // std::ostringstream
#include <string> // 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<std::string> &mime = std::nullopt,
const std::optional<std::string> &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
Loading