From 9b521b1cf42c2787e77e85cc85d79286fe3716e6 Mon Sep 17 00:00:00 2001 From: Juan Cruz Viotti Date: Tue, 7 Jan 2025 15:18:38 -0400 Subject: [PATCH] Introduce a basic search API Signed-off-by: Juan Cruz Viotti --- cmake/FindJSONToolkit.cmake | 1 + src/server/server.cc | 60 ++++++++++++++- test/e2e/ce/search.hurl | 10 +++ test/e2e/ee/no-meta.hurl | 21 ----- test/e2e/ee/search.hurl | 76 +++++++++++++++++++ .../schemas/example/bundling/single.json | 2 + 6 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 test/e2e/ce/search.hurl delete mode 100644 test/e2e/ee/no-meta.hurl create mode 100644 test/e2e/ee/search.hurl diff --git a/cmake/FindJSONToolkit.cmake b/cmake/FindJSONToolkit.cmake index d8b5678..b65f6d3 100644 --- a/cmake/FindJSONToolkit.cmake +++ b/cmake/FindJSONToolkit.cmake @@ -1,5 +1,6 @@ if(NOT JSONToolkit_FOUND) set(JSONTOOLKIT_INSTALL OFF CACHE BOOL "disable installation") + set(JSONTOOLKIT_JSONL OFF CACHE BOOL "disable JSONL support") add_subdirectory("${PROJECT_SOURCE_DIR}/vendor/jsontoolkit") set(JSONToolkit_FOUND ON) endif() diff --git a/src/server/server.cc b/src/server/server.cc index 6c681b6..a07ff6f 100644 --- a/src/server/server.cc +++ b/src/server/server.cc @@ -8,6 +8,7 @@ #include "configure.h" +#include // std::search #include // assert #include // std::tolower #include // std::uint32_t, std::int64_t @@ -17,7 +18,7 @@ #include // std::unique_ptr #include // std::optional, std::nullopt #include // std::ostringstream -#include // std::string +#include // std::string, std::getline #include // std::string_view #include // std::move @@ -106,6 +107,52 @@ auto on_index(const sourcemeta::hydra::http::ServerLogger &, sourcemeta::hydra::http::serve_file( *(__global_data) / "generated" / "index.html", request, response); } + +auto on_search(const sourcemeta::hydra::http::ServerLogger &logger, + const sourcemeta::hydra::http::ServerRequest &request, + sourcemeta::hydra::http::ServerResponse &response) -> void { + const auto query{request.query("q")}; + if (!query.has_value()) { + json_error(logger, request, response, + sourcemeta::hydra::http::Status::BAD_REQUEST, "missing-query", + "You must provide a query parameter to search for"); + return; + } + + auto result{sourcemeta::jsontoolkit::JSON::make_array()}; + auto stream = sourcemeta::jsontoolkit::read_file( + *(__global_data) / "generated" / "search.jsonl"); + stream.exceptions(std::ifstream::badbit); + // TODO: Extend the JSON Toolkit JSONL iterators to be able + // to access the stringified contents of the current entry + // BEFORE parsing it as JSON, letting the client decide + // whether to parse or not. + std::string line; + const auto &query_value{query.value()}; + while (std::getline(stream, line)) { + if (std::search(line.cbegin(), line.cend(), query_value.cbegin(), + query_value.cend(), [](const auto left, const auto right) { + return std::tolower(left) == std::tolower(right); + }) == line.cend()) { + continue; + } + + auto entry{sourcemeta::jsontoolkit::JSON::make_object()}; + auto line_json{sourcemeta::jsontoolkit::parse(line)}; + entry.assign("url", std::move(line_json.at(0))); + entry.assign("title", std::move(line_json.at(1))); + entry.assign("description", std::move(line_json.at(2))); + result.push_back(std::move(entry)); + + constexpr auto MAXIMUM_SEARCH_COUNT{10}; + if (result.array_size() >= MAXIMUM_SEARCH_COUNT) { + break; + } + } + + response.status(sourcemeta::hydra::http::Status::OK); + response.end(std::move(result)); +} #endif static auto on_request(const sourcemeta::hydra::http::ServerLogger &logger, @@ -189,6 +236,16 @@ static auto on_otherwise(const sourcemeta::hydra::http::ServerLogger &logger, const sourcemeta::hydra::http::ServerRequest &request, sourcemeta::hydra::http::ServerResponse &response) -> void { +#ifdef SOURCEMETA_REGISTRY_ENTERPRISE + if (request.path() == "/search") { + json_error(logger, request, response, + sourcemeta::hydra::http::Status::METHOD_NOT_ALLOWED, + "method-not-allowed", + "This HTTP method is invalid for this URL"); + return; + } +#endif + const auto maybe_schema{resolver(request_path_to_schema_uri( configuration().at("url").to_string(), request.path()))}; @@ -250,6 +307,7 @@ auto main(int argc, char *argv[]) noexcept -> int { sourcemeta::hydra::http::Server server; #ifdef SOURCEMETA_REGISTRY_ENTERPRISE server.route(sourcemeta::hydra::http::Method::GET, "/", on_index); + server.route(sourcemeta::hydra::http::Method::GET, "/search", on_search); #endif server.route(sourcemeta::hydra::http::Method::GET, "/*", on_request); server.route(sourcemeta::hydra::http::Method::HEAD, "/*", on_request); diff --git a/test/e2e/ce/search.hurl b/test/e2e/ce/search.hurl new file mode 100644 index 0000000..88032a9 --- /dev/null +++ b/test/e2e/ce/search.hurl @@ -0,0 +1,10 @@ +GET {{base}}/search?q=foo +HTTP 404 +Content-Type: application/json +[Captures] +current_request_id: header "X-Request-id" +[Asserts] +jsonpath "$.code" == 404 +jsonpath "$.error" == "not-found" +jsonpath "$.message" == "There is no schema at this URL" +jsonpath "$.request" == "{{current_request_id}}" diff --git a/test/e2e/ee/no-meta.hurl b/test/e2e/ee/no-meta.hurl deleted file mode 100644 index d3f35f2..0000000 --- a/test/e2e/ee/no-meta.hurl +++ /dev/null @@ -1,21 +0,0 @@ -GET {{base}}/example/schemas/.meta.json -HTTP 404 -Content-Type: application/json -[Captures] -current_request_id: header "X-Request-id" -[Asserts] -jsonpath "$.code" == 404 -jsonpath "$.error" == "not-found" -jsonpath "$.message" == "There is no schema at this URL" -jsonpath "$.request" == "{{current_request_id}}" - -GET {{base}}/.meta.json -HTTP 404 -Content-Type: application/json -[Captures] -current_request_id: header "X-Request-id" -[Asserts] -jsonpath "$.code" == 404 -jsonpath "$.error" == "not-found" -jsonpath "$.message" == "There is no schema at this URL" -jsonpath "$.request" == "{{current_request_id}}" diff --git a/test/e2e/ee/search.hurl b/test/e2e/ee/search.hurl new file mode 100644 index 0000000..2728eb7 --- /dev/null +++ b/test/e2e/ee/search.hurl @@ -0,0 +1,76 @@ +GET {{base}}/search +HTTP 400 +Content-Type: application/json +[Captures] +current_request_id: header "X-Request-id" +[Asserts] +jsonpath "$.code" == 400 +jsonpath "$.error" == "missing-query" +jsonpath "$.message" == "You must provide a query parameter to search for" +jsonpath "$.request" == "{{current_request_id}}" + +GET {{base}}/search?q= +HTTP 400 +Content-Type: application/json +[Captures] +current_request_id: header "X-Request-id" +[Asserts] +jsonpath "$.code" == 400 +jsonpath "$.error" == "missing-query" +jsonpath "$.message" == "You must provide a query parameter to search for" +jsonpath "$.request" == "{{current_request_id}}" + +POST {{base}}/search?q=foo +HTTP 405 +Content-Type: application/json +[Captures] +current_request_id: header "X-Request-id" +[Asserts] +jsonpath "$.code" == 405 +jsonpath "$.error" == "method-not-allowed" +jsonpath "$.message" == "This HTTP method is invalid for this URL" +jsonpath "$.request" == "{{current_request_id}}" + +# A string we know won't give any results +GET {{base}}/search?q=xxxxxxxxxxxx +HTTP 200 +[] + +# Results with title/description +GET {{base}}/search?q=bundling +HTTP 200 +[ + { + "url": "/example/bundling/single.json", + "title": "Bundling", + "description": "A bundling example" + } +] + +# Test casing +GET {{base}}/search?q=bUNdLing +HTTP 200 +[ + { + "url": "/example/bundling/single.json", + "title": "Bundling", + "description": "A bundling example" + } +] + +# Results without title/description +GET {{base}}/search?q=camel +HTTP 200 +[ + { + "url": "/example/schemas/camelcase.json", + "title": "", + "description": "" + } +] + +# No matter what, we impose a limit on the results +GET {{base}}/search?q=e +HTTP 200 +[Asserts] +jsonpath "$" count <= 10 diff --git a/test/sandbox/schemas/example/bundling/single.json b/test/sandbox/schemas/example/bundling/single.json index 8d98d6e..28c3db6 100644 --- a/test/sandbox/schemas/example/bundling/single.json +++ b/test/sandbox/schemas/example/bundling/single.json @@ -1,6 +1,8 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://example.com/bundling/single.json", + "title": "Bundling", + "description": "A bundling example", "properties": { "foo": { "$ref": "http://localhost:8000/example/v2.0/schema.json"