-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Extract various server utilities to separate files (#23)
Signed-off-by: Juan Cruz Viotti <[email protected]>
- Loading branch information
Showing
5 changed files
with
244 additions
and
156 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.