Skip to content

Commit

Permalink
Extract various server utilities to separate files (#23)
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Cruz Viotti <[email protected]>
  • Loading branch information
jviotti authored Nov 13, 2024
1 parent ece2df3 commit 8aa3207
Show file tree
Hide file tree
Showing 5 changed files with 244 additions and 156 deletions.
2 changes: 1 addition & 1 deletion src/server/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
add_executable(schema_registry_server server.cc configure.h.in)
add_executable(schema_registry_server server.cc error.h request.h resolver.h configure.h.in)

noa_add_default_options(PRIVATE schema_registry_server)
set_target_properties(schema_registry_server PROPERTIES OUTPUT_NAME sourcemeta-registry-server)
Expand Down
72 changes: 72 additions & 0 deletions src/server/error.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#ifndef SOURCEMETA_REGISTRY_SERVER_ERROR_H_
#define SOURCEMETA_REGISTRY_SERVER_ERROR_H_

#include <sourcemeta/hydra/http.h>
#include <sourcemeta/hydra/httpserver.h>

#include <sourcemeta/jsontoolkit/json.h>

#include <cstdint> // std::int64_t
#include <exception> // std::exception_ptr, std::rethrow_exception
#include <optional> // std::optional
#include <sstream> // std::ostringstream
#include <string> // std::string
#include <utility> // std::move

namespace sourcemeta::registry {

auto json_error(const sourcemeta::hydra::http::ServerLogger &logger,
const sourcemeta::hydra::http::ServerRequest &,
sourcemeta::hydra::http::ServerResponse &response,
const sourcemeta::hydra::http::Status code, std::string &&id,
std::string &&message) -> void {
auto object{sourcemeta::jsontoolkit::JSON::make_object()};
object.assign("request", sourcemeta::jsontoolkit::JSON{logger.id()});
object.assign("error", sourcemeta::jsontoolkit::JSON{std::move(id)});
object.assign("message", sourcemeta::jsontoolkit::JSON{std::move(message)});
object.assign("code",
sourcemeta::jsontoolkit::JSON{static_cast<std::int64_t>(code)});
response.status(code);
response.header("Content-Type", "application/json");
response.end(std::move(object));
}

auto on_error(std::exception_ptr exception_ptr,
const sourcemeta::hydra::http::ServerLogger &logger,
const sourcemeta::hydra::http::ServerRequest &request,
sourcemeta::hydra::http::ServerResponse &response) noexcept
-> void {
try {
std::rethrow_exception(exception_ptr);
} catch (const sourcemeta::jsontoolkit::SchemaResolutionError &error) {
std::ostringstream message;
message << error.what() << ": " << error.id();
json_error(logger, request, response,
sourcemeta::hydra::http::Status::BAD_REQUEST,
"schema-resolution-error", message.str());
} catch (const std::exception &error) {
json_error(logger, request, response,
sourcemeta::hydra::http::Status::METHOD_NOT_ALLOWED,
"uncaught-error", error.what());
}
}

auto on_otherwise(const std::optional<sourcemeta::jsontoolkit::JSON> &schema,
const sourcemeta::hydra::http::ServerLogger &logger,
const sourcemeta::hydra::http::ServerRequest &request,
sourcemeta::hydra::http::ServerResponse &response) -> void {
if (schema.has_value()) {
sourcemeta::registry::json_error(
logger, request, response,
sourcemeta::hydra::http::Status::METHOD_NOT_ALLOWED,
"method-not-allowed", "This HTTP method is invalid for this URL");
} else {
sourcemeta::registry::json_error(
logger, request, response, sourcemeta::hydra::http::Status::NOT_FOUND,
"not-found", "There is no schema at this URL");
}
}

} // namespace sourcemeta::registry

#endif
72 changes: 72 additions & 0 deletions src/server/request.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#ifndef SOURCEMETA_REGISTRY_SERVER_REQUEST_H_
#define SOURCEMETA_REGISTRY_SERVER_REQUEST_H_

#include <sourcemeta/jsontoolkit/json.h>
#include <sourcemeta/jsontoolkit/jsonschema.h>

#include <sourcemeta/hydra/crypto.h>
#include <sourcemeta/hydra/http.h>
#include <sourcemeta/hydra/httpserver.h>

#include <cassert> // assert
#include <sstream> // std::ostringstream
#include <string> // std::string

#include "error.h"

namespace sourcemeta::registry {

auto on_request(const std::string &schema_identifier,
const sourcemeta::jsontoolkit::SchemaResolver &resolver,
const sourcemeta::hydra::http::ServerLogger &logger,
const sourcemeta::hydra::http::ServerRequest &request,
sourcemeta::hydra::http::ServerResponse &response) -> void {
const auto maybe_schema{resolver(schema_identifier)};
if (!maybe_schema.has_value()) {
json_error(logger, request, response,
sourcemeta::hydra::http::Status::NOT_FOUND, "not-found",
"There is no schema at this URL");
return;
}

std::ostringstream payload;
sourcemeta::jsontoolkit::prettify(
maybe_schema.value(), payload,
sourcemeta::jsontoolkit::schema_format_compare);

std::ostringstream hash;
sourcemeta::hydra::md5(payload.str(), hash);

if (!request.header_if_none_match(hash.str())) {
response.status(sourcemeta::hydra::http::Status::NOT_MODIFIED);
response.end();
return;
}

response.status(sourcemeta::hydra::http::Status::OK);
response.header("Content-Type", "application/schema+json");

// See
// https://json-schema.org/draft/2020-12/json-schema-core.html#section-9.5.1.1
const auto dialect{sourcemeta::jsontoolkit::dialect(maybe_schema.value())};
assert(dialect.has_value());
std::ostringstream link;
link << "<" << dialect.value() << ">; rel=\"describedby\"";
response.header("Link", link.str());

// For HTTP caching, we only rely on ETag hashes, as Last-Modified
// can be tricky to obtain in all cases.
response.header_etag(hash.str());

if (request.method() == sourcemeta::hydra::http::Method::HEAD) {
response.head(payload.str());
return;
} else {
response.end(payload.str());
return;
}
}

} // namespace sourcemeta::registry

#endif
80 changes: 80 additions & 0 deletions src/server/resolver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#ifndef SOURCEMETA_REGISTRY_SERVER_RESOLVER_H_
#define SOURCEMETA_REGISTRY_SERVER_RESOLVER_H_

#include <sourcemeta/jsontoolkit/json.h>
#include <sourcemeta/jsontoolkit/jsonschema.h>
#include <sourcemeta/jsontoolkit/uri.h>

#include <cassert> // assert
#include <cctype> // std::tolower
#include <filesystem> // std::filesystem
#include <optional> // std::optional, std::nullopt
#include <sstream> // std::ostringstream
#include <string> // std::string
#include <string_view> // std::string_view

static auto path_join(const std::filesystem::path &base,
const std::filesystem::path &path)
-> std::filesystem::path {
if (path.is_absolute()) {
return (base / path.string().substr(1)).lexically_normal();
}

return (base / path).lexically_normal();
}

namespace sourcemeta::registry {

auto request_path_to_schema_uri(const std::string &server_base_url,
const std::string &request_path)
-> std::string {
assert(request_path.starts_with('/'));
assert(!server_base_url.ends_with('/'));
std::ostringstream schema_identifier;
schema_identifier << server_base_url;
// TODO: Can we avoid this copy?
auto path_copy{request_path};
path_copy.erase(path_copy.find_last_not_of('/') + 1);
for (const auto character : path_copy) {
schema_identifier << static_cast<char>(std::tolower(character));
}

if (request_path != "/" && !schema_identifier.str().ends_with(".json")) {
schema_identifier << ".json";
}

return schema_identifier.str();
}

auto resolver(const sourcemeta::jsontoolkit::URI &server_base_url,
const std::filesystem::path &schema_base_directory,
std::string_view identifier)
-> std::optional<sourcemeta::jsontoolkit::JSON> {
sourcemeta::jsontoolkit::URI uri{std::string{identifier}};
uri.canonicalize().relative_to(server_base_url);

// If so, this URI doesn't belong to us
// TODO: Have a more efficient way of checking that a URI is blank
if (uri.is_absolute() || uri.recompose().empty()) {
return sourcemeta::jsontoolkit::official_resolver(identifier);
}

assert(uri.path().has_value());
const auto schema_path{path_join(schema_base_directory, uri.path().value())};
if (!std::filesystem::exists(schema_path)) {
return std::nullopt;
}

auto schema{sourcemeta::jsontoolkit::from_file(schema_path)};
sourcemeta::jsontoolkit::reidentify(
schema, std::string{identifier},
[&server_base_url, &schema_base_directory](const auto &subidentifier) {
return resolver(server_base_url, schema_base_directory, subidentifier);
});

return schema;
}

} // namespace sourcemeta::registry

#endif
Loading

0 comments on commit 8aa3207

Please sign in to comment.