فهرست منبع

feat(filters): parse field:op:value query filters into client Filter

Fszontagh 2 ماه پیش
والد
کامیت
4e2993b437
5فایلهای تغییر یافته به همراه97 افزوده شده و 0 حذف شده
  1. 1 0
      src/CMakeLists.txt
  2. 42 0
      src/filters.cpp
  3. 16 0
      src/filters.hpp
  4. 4 0
      tests/CMakeLists.txt
  5. 34 0
      tests/test_filters.cpp

+ 1 - 0
src/CMakeLists.txt

@@ -2,6 +2,7 @@ add_library(vectorapi_core STATIC
     config.cpp
     errors.cpp
     json_http.cpp
+    filters.cpp
 )
 target_include_directories(vectorapi_core PUBLIC
     ${CMAKE_CURRENT_SOURCE_DIR}

+ 42 - 0
src/filters.cpp

@@ -0,0 +1,42 @@
+#include "filters.hpp"
+#include "errors.hpp"
+#include <unordered_map>
+
+namespace svapi {
+
+using Op = smartbotic::database::Client::FilterOp;
+
+Op opFromSlug(const std::string& slug) {
+    static const std::unordered_map<std::string, Op> m = {
+        {"eq", Op::EQ}, {"ne", Op::NE}, {"gt", Op::GT}, {"gte", Op::GTE},
+        {"lt", Op::LT}, {"lte", Op::LTE}, {"in", Op::IN}, {"contains", Op::CONTAINS},
+        {"exists", Op::EXISTS}, {"regex", Op::REGEX}, {"search", Op::SEARCH},
+    };
+    auto it = m.find(slug);
+    if (it == m.end())
+        throw ApiError(ErrCode::BadRequest, "bad_filter", "unknown filter operator: " + slug);
+    return it->second;
+}
+
+smartbotic::database::Client::Filter parseFilter(const std::string& spec) {
+    auto p1 = spec.find(':');
+    if (p1 == std::string::npos)
+        throw ApiError(ErrCode::BadRequest, "bad_filter", "filter must be field:op:value: " + spec);
+    auto p2 = spec.find(':', p1 + 1);
+    if (p2 == std::string::npos)
+        throw ApiError(ErrCode::BadRequest, "bad_filter", "filter must be field:op:value: " + spec);
+
+    std::string field = spec.substr(0, p1);
+    std::string opSlug = spec.substr(p1 + 1, p2 - p1 - 1);
+    std::string rawValue = spec.substr(p2 + 1);
+
+    nlohmann::json value;
+    try {
+        value = nlohmann::json::parse(rawValue);
+    } catch (...) {
+        value = rawValue;  // fall back to string
+    }
+    return {field, opFromSlug(opSlug), value};
+}
+
+} // namespace svapi

+ 16 - 0
src/filters.hpp

@@ -0,0 +1,16 @@
+#pragma once
+#include <string>
+#include <smartbotic/database/client.hpp>
+
+namespace svapi {
+
+// Map an operator slug ("eq","ne","gt","gte","lt","lte","in","contains",
+// "exists","regex","search") to the client FilterOp. Throws ApiError(BadRequest).
+smartbotic::database::Client::FilterOp opFromSlug(const std::string& slug);
+
+// Parse "field:op:value". The value is everything after the second ':' and is
+// parsed as JSON when possible, else treated as a string. Throws
+// ApiError(BadRequest) on malformed input or unknown op.
+smartbotic::database::Client::Filter parseFilter(const std::string& spec);
+
+} // namespace svapi

+ 4 - 0
tests/CMakeLists.txt

@@ -12,3 +12,7 @@ gtest_discover_tests(test_config)
 add_executable(test_errors test_errors.cpp)
 target_link_libraries(test_errors PRIVATE vectorapi_core GTest::gtest_main)
 gtest_discover_tests(test_errors)
+
+add_executable(test_filters test_filters.cpp)
+target_link_libraries(test_filters PRIVATE vectorapi_core GTest::gtest_main)
+gtest_discover_tests(test_filters)

+ 34 - 0
tests/test_filters.cpp

@@ -0,0 +1,34 @@
+#include <gtest/gtest.h>
+#include "filters.hpp"
+#include "errors.hpp"
+
+using svapi::parseFilter;
+using FilterOp = smartbotic::database::Client::FilterOp;
+
+TEST(Filters, ParsesNumericGt) {
+    auto f = parseFilter("age:gt:18");
+    EXPECT_EQ(f.field, "age");
+    EXPECT_EQ(f.op, FilterOp::GT);
+    EXPECT_EQ(f.value, 18);            // parsed as JSON number
+}
+
+TEST(Filters, ParsesStringEq) {
+    auto f = parseFilter("status:eq:active");
+    EXPECT_EQ(f.field, "status");
+    EXPECT_EQ(f.op, FilterOp::EQ);
+    EXPECT_EQ(f.value, "active");      // non-JSON -> string
+}
+
+TEST(Filters, ValueMayContainColons) {
+    auto f = parseFilter("url:eq:http://x:8080/y");
+    EXPECT_EQ(f.field, "url");
+    EXPECT_EQ(f.value, "http://x:8080/y");
+}
+
+TEST(Filters, UnknownOpThrows) {
+    EXPECT_THROW(parseFilter("a:wat:1"), svapi::ApiError);
+}
+
+TEST(Filters, MalformedThrows) {
+    EXPECT_THROW(parseFilter("nope"), svapi::ApiError);
+}