diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 6f2e1cc1ad..4a7d3c7ab9 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -21,6 +21,7 @@ add_subdirectory (impl) add_subdirectory (storage) add_subdirectory (attr) add_subdirectory (datacell) +add_subdirectory (param) add_subdirectory (algorithm) add_subdirectory (utils) add_subdirectory (factory) @@ -62,7 +63,7 @@ foreach (_lib vsag vsag_static) $) endforeach () -set (VSAG_DEP_LIBS antlr4-autogen antlr4-runtime diskann simd io quantizer storage utils factory analyzer +set (VSAG_DEP_LIBS antlr4-autogen param antlr4-runtime diskann simd io quantizer storage utils factory analyzer datacell ${IMPL_LIBS} ${ALGORITHM_LIBS} attr pthread m dl fmt::fmt nlohmann_json::nlohmann_json roaring ${BLAS_LIBRARIES}) # `vsag_src_common` aggregates third-party include paths and link diff --git a/src/algorithm/bruteforce/bruteforce_parameter.cpp b/src/algorithm/bruteforce/bruteforce_parameter.cpp index ce6aaf8ced..4a31d617e7 100644 --- a/src/algorithm/bruteforce/bruteforce_parameter.cpp +++ b/src/algorithm/bruteforce/bruteforce_parameter.cpp @@ -15,7 +15,7 @@ #include "bruteforce_parameter.h" -#include +#include #include "datacell/flatten_datacell_parameter.h" #include "inner_string_params.h" @@ -24,23 +24,27 @@ namespace vsag { +VSAG_PARAM_SCHEMA(BruteForceParameter) +VSAG_PARAM_TYPE_TAG(TYPE_KEY, INDEX_TYPE_BRUTE_FORCE) +VSAG_PARAM_SUBPARAM_REQUIRED(FlattenInterfaceParameter, + base_codes_param, + BASE_CODES_KEY, + CreateFlattenParam) +VSAG_PARAM_SCHEMA_END() + BruteForceParameter::BruteForceParameter() : base_codes_param(nullptr) { } void BruteForceParameter::FromJson(const JsonType& json) { InnerIndexParameter::FromJson(json); - CHECK_ARGUMENT(json.Contains(BASE_CODES_KEY), - fmt::format("bruteforce parameters must contains {}", BASE_CODES_KEY)); - const auto& base_codes_json = json[BASE_CODES_KEY]; - this->base_codes_param = CreateFlattenParam(base_codes_json); + schema().Parse(this, json); } JsonType BruteForceParameter::ToJson() const { JsonType json = InnerIndexParameter::ToJson(); - json[TYPE_KEY].SetString(INDEX_TYPE_BRUTE_FORCE); - json[BASE_CODES_KEY].SetJson(this->base_codes_param->ToJson()); + schema().Serialize(this, json); return json; } @@ -50,7 +54,6 @@ BruteForceParameter::CheckCompatibility(const ParamPtr& other) const { return false; } PARAM_CAST_OR_RETURN(BruteForceParameter, p, other); - CHECK_SUB_PARAM(*this, *p, base_codes_param); - return true; + return schema().Equal(this, p.get()); } } // namespace vsag diff --git a/src/algorithm/bruteforce/bruteforce_parameter.h b/src/algorithm/bruteforce/bruteforce_parameter.h index 5ec35b70ad..c86a5c62f6 100644 --- a/src/algorithm/bruteforce/bruteforce_parameter.h +++ b/src/algorithm/bruteforce/bruteforce_parameter.h @@ -16,11 +16,12 @@ #pragma once #include "algorithm/index_search_parameter.h" #include "algorithm/inner_index_parameter.h" +#include "datacell/flatten_interface_parameter.h" +#include "param/schema.h" #include "typing.h" #include "utils/pointer_define.h" #include "vsag/constants.h" namespace vsag { -DEFINE_POINTER2(FlattenDataCellParam, FlattenDataCellParameter); class BruteForceParameter : public InnerIndexParameter { public: explicit BruteForceParameter(); @@ -34,8 +35,8 @@ class BruteForceParameter : public InnerIndexParameter { bool CheckCompatibility(const vsag::ParamPtr& other) const override; -public: FlattenInterfaceParamPtr base_codes_param{nullptr}; + VSAG_PARAM_SCHEMA_DECL(); }; DEFINE_POINTER(BruteForceParameter); diff --git a/src/inner_string_params.h b/src/inner_string_params.h index e7d480bd8b..62f633c8a4 100644 --- a/src/inner_string_params.h +++ b/src/inner_string_params.h @@ -200,6 +200,7 @@ const std::unordered_map DEFAULT_MAP = { {"INDEX_TYPE_IVF", INDEX_TYPE_IVF}, {"INDEX_TYPE_GNO_IMI", INDEX_TYPE_GNO_IMI}, {"INDEX_TYPE_PYRAMID", INDEX_TYPE_PYRAMID}, + {"INDEX_BRUTE_FORCE", INDEX_BRUTE_FORCE}, {"TYPE_KEY", TYPE_KEY}, {"HGRAPH_USE_ELP_OPTIMIZER_KEY", HGRAPH_USE_ELP_OPTIMIZER_KEY}, {"HGRAPH_IGNORE_REORDER_KEY", HGRAPH_IGNORE_REORDER_KEY}, diff --git a/src/param/CMakeLists.txt b/src/param/CMakeLists.txt new file mode 100644 index 0000000000..009f10b1a4 --- /dev/null +++ b/src/param/CMakeLists.txt @@ -0,0 +1,28 @@ + +# Copyright 2024-present the vsag project +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +file (GLOB PARAM_SRCS "*.cpp") +list (FILTER PARAM_SRCS EXCLUDE REGEX "_test.cpp") + +add_library (param OBJECT ${PARAM_SRCS}) +target_link_libraries (param PRIVATE coverage_config vsag_src_common) + +if (ENABLE_TESTS) + file (GLOB_RECURSE PARAM_TESTS "*_test.cpp") + add_library (param_test STATIC ${PARAM_TESTS}) + target_link_libraries (param_test PRIVATE Catch2::Catch2 vsag_static unittest_deps) + add_dependencies (param_test Catch2) +endif () diff --git a/src/param/schema.cpp b/src/param/schema.cpp new file mode 100644 index 0000000000..501f7bb6bd --- /dev/null +++ b/src/param/schema.cpp @@ -0,0 +1,169 @@ + +// Copyright 2024-present the vsag project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "param/schema.h" + +#include + +#include + +#include "common.h" + +namespace vsag::param { + +// --------------------------------------------------------------------------- +// Schema +// --------------------------------------------------------------------------- + +void +Schema::AddField(FieldPtr field) { + this->known_keys_.insert(field->Key()); + this->fields_.push_back(std::move(field)); +} + +void +Schema::Parse(void* instance, const JsonType& parent) const { + for (const auto& field : this->fields_) { + field->Parse(instance, parent); + } +} + +void +Schema::Serialize(const void* instance, JsonType& parent) const { + for (const auto& field : this->fields_) { + field->Serialize(instance, parent); + } +} + +bool +Schema::Equal(const void* lhs, const void* rhs) const { + for (const auto& field : this->fields_) { + if (not field->Equal(lhs, rhs)) { + return false; + } + } + return true; +} + +void +Schema::CheckUnknownKeys(const JsonType& parent, + const std::unordered_set& extra_known_keys) const { + auto* inner = parent.GetInnerJson(); + if (inner == nullptr || not inner->is_object()) { + return; + } + for (const auto& item : inner->items()) { + const auto& key = item.key(); + if (this->known_keys_.count(key) != 0U) { + continue; + } + if (extra_known_keys.count(key) != 0U) { + continue; + } + throw VsagException( + ErrorType::INVALID_ARGUMENT, + fmt::format("unknown parameter key '{}' is not declared by the schema", key)); + } +} + +// --------------------------------------------------------------------------- +// ConstantStringField +// --------------------------------------------------------------------------- + +ConstantStringField::ConstantStringField(std::string key, std::string value) + : key_(std::move(key)), value_(std::move(value)) { +} + +void +ConstantStringField::Parse(void* /*instance*/, const JsonType& parent) const { + if (not parent.Contains(this->key_)) { + return; + } + auto actual = parent[this->key_].GetString(); + CHECK_ARGUMENT( + actual == this->value_, + fmt::format("parameter '{}' must be '{}', got '{}'", this->key_, this->value_, actual)); +} + +void +ConstantStringField::Serialize(const void* /*instance*/, JsonType& parent) const { + parent[this->key_].SetString(this->value_); +} + +// --------------------------------------------------------------------------- +// Scalar JSON read/write specializations +// --------------------------------------------------------------------------- + +template <> +uint64_t +ReadJsonValue(const JsonType& json) { + return json.GetUint64(); +} + +template <> +int64_t +ReadJsonValue(const JsonType& json) { + return json.GetInt(); +} + +template <> +std::string +ReadJsonValue(const JsonType& json) { + return json.GetString(); +} + +template <> +bool +ReadJsonValue(const JsonType& json) { + return json.GetBool(); +} + +template <> +float +ReadJsonValue(const JsonType& json) { + return json.GetFloat(); +} + +template <> +void +WriteJsonValueAt(JsonType& parent, const std::string& key, const uint64_t& value) { + parent[key].SetUint64(value); +} + +template <> +void +WriteJsonValueAt(JsonType& parent, const std::string& key, const int64_t& value) { + parent[key].SetInt(value); +} + +template <> +void +WriteJsonValueAt(JsonType& parent, const std::string& key, const std::string& value) { + parent[key].SetString(value); +} + +template <> +void +WriteJsonValueAt(JsonType& parent, const std::string& key, const bool& value) { + parent[key].SetBool(value); +} + +template <> +void +WriteJsonValueAt(JsonType& parent, const std::string& key, const float& value) { + parent[key].SetFloat(value); +} + +} // namespace vsag::param diff --git a/src/param/schema.h b/src/param/schema.h new file mode 100644 index 0000000000..ff7d88bccb --- /dev/null +++ b/src/param/schema.h @@ -0,0 +1,331 @@ + +// Copyright 2024-present the vsag project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "json_types.h" +#include "param/validators.h" + +namespace vsag::param { + +// Type-erased descriptor for a single parameter field. A schema is a list of +// these descriptors; the descriptors carry the knowledge of how to parse a +// JSON value into a member, how to serialize a member back to JSON, and how +// to compare the member between two instances. +class FieldBase { +public: + virtual ~FieldBase() = default; + + virtual const std::string& + Key() const = 0; + + virtual void + Parse(void* instance, const JsonType& parent) const = 0; + + virtual void + Serialize(const void* instance, JsonType& parent) const = 0; + + virtual bool + Equal(const void* lhs, const void* rhs) const = 0; +}; + +using FieldPtr = std::shared_ptr; + +// A schema groups every field declared on a parameter class. It is the +// engine driving the auto-generated FromJson / ToJson / CheckCompatibility +// helpers exposed via the ``VSAG_PARAM_SCHEMA`` macro family. +class Schema { +public: + void + AddField(FieldPtr field); + + void + Parse(void* instance, const JsonType& parent) const; + + void + Serialize(const void* instance, JsonType& parent) const; + + bool + Equal(const void* lhs, const void* rhs) const; + + // Reject any JSON key that is neither registered as a schema field nor + // listed in ``extra_known_keys``. Used when a parameter class is fully + // schema-driven and we want to flag typos from callers. + void + CheckUnknownKeys(const JsonType& parent, + const std::unordered_set& extra_known_keys = {}) const; + + const std::unordered_set& + KnownKeys() const { + return this->known_keys_; + } + +private: + std::vector fields_; + std::unordered_set known_keys_; +}; + +// Read a scalar JSON value as ``T``. Specialized for the scalar types the +// schema supports (uint64_t, int64_t, std::string, bool, float). +template +T +ReadJsonValue(const JsonType& json); + +// Write a scalar ``T`` into ``parent[key]``. Specialized for the same set +// of scalar types as ``ReadJsonValue``. +template +void +WriteJsonValueAt(JsonType& parent, const std::string& key, const T& value); + +// Field descriptor for a scalar member (``T Owner::*``). Supports an optional +// ``ValidatorFn`` invoked after the JSON value is decoded. +template +class ScalarField : public FieldBase { +public: + ScalarField(std::string key, T Owner::*member, T default_value, ValidatorFn validator) + : key_(std::move(key)), + member_(member), + default_value_(std::move(default_value)), + validator_(std::move(validator)) { + } + + const std::string& + Key() const override { + return this->key_; + } + + void + Parse(void* instance, const JsonType& parent) const override { + auto* obj = static_cast(instance); + if (parent.Contains(this->key_)) { + T value = ReadJsonValue(parent[this->key_]); + if (this->validator_) { + this->validator_(this->key_, value); + } + obj->*member_ = std::move(value); + } else { + obj->*member_ = this->default_value_; + } + } + + void + Serialize(const void* instance, JsonType& parent) const override { + const auto* obj = static_cast(instance); + WriteJsonValueAt(parent, this->key_, obj->*member_); + } + + bool + Equal(const void* lhs, const void* rhs) const override { + const auto* l = static_cast(lhs); + const auto* r = static_cast(rhs); + return (l->*member_) == (r->*member_); + } + +private: + std::string key_; + T Owner::*member_; + T default_value_; + ValidatorFn validator_; +}; + +// Field descriptor for a member holding a ``std::shared_ptr`` where +// ``Sub`` derives from ``Parameter``. The user supplies a factory that +// inspects the JSON sub-object and returns the appropriate concrete +// instance (e.g. ``CreateFlattenParam``). +template +class SubParamField : public FieldBase { +public: + using SubFactory = std::function(const JsonType&)>; + using MemberPtr = std::shared_ptr Owner::*; + + SubParamField(std::string key, MemberPtr member, SubFactory factory, bool required) + : key_(std::move(key)), member_(member), factory_(std::move(factory)), required_(required) { + } + + const std::string& + Key() const override { + return this->key_; + } + + void + Parse(void* instance, const JsonType& parent) const override { + auto* obj = static_cast(instance); + if (parent.Contains(this->key_)) { + obj->*member_ = this->factory_(parent[this->key_]); + } else { + CHECK_ARGUMENT(not this->required_, + fmt::format("missing required sub-parameter '{}'", this->key_)); + obj->*member_ = nullptr; + } + } + + void + Serialize(const void* instance, JsonType& parent) const override { + const auto* obj = static_cast(instance); + if (obj->*member_) { + parent[this->key_].SetJson((obj->*member_)->ToJson()); + } + } + + bool + Equal(const void* lhs, const void* rhs) const override { + const auto* l = static_cast(lhs); + const auto* r = static_cast(rhs); + const bool l_null = ((l->*member_) == nullptr); + const bool r_null = ((r->*member_) == nullptr); + if (l_null != r_null) { + return false; + } + if (l_null) { + return true; + } + return (l->*member_)->CheckCompatibility(r->*member_); + } + +private: + std::string key_; + MemberPtr member_; + SubFactory factory_; + bool required_; +}; + +// Field descriptor for a constant string tag (typically the ``type`` key +// used to identify a concrete parameter subclass). The constant is always +// emitted by Serialize, and on Parse it is verified to match if present. +class ConstantStringField : public FieldBase { +public: + ConstantStringField(std::string key, std::string value); + + const std::string& + Key() const override { + return this->key_; + } + + void + Parse(void* instance, const JsonType& parent) const override; + + void + Serialize(const void* instance, JsonType& parent) const override; + + bool + Equal(const void* lhs, const void* rhs) const override { + return true; + } + +private: + std::string key_; + std::string value_; +}; + +// Fluent builder used by the ``VSAG_PARAM_SCHEMA`` macros to assemble a +// schema declaratively. Owns a ``Schema`` and returns ``*this`` so that +// every ``Scalar`` / ``SubParam`` / ``TypeTag`` call can be chained. +template +class SchemaBuilder { +public: + template + SchemaBuilder& + Scalar(const std::string& key, + T Owner::*member, + T default_value, + ValidatorFn validator = {}) { + this->schema_.AddField(std::make_shared>( + key, member, std::move(default_value), std::move(validator))); + return *this; + } + + template + SchemaBuilder& + SubParam(const std::string& key, + std::shared_ptr Owner::*member, + std::function(const JsonType&)> factory, + bool required = false) { + this->schema_.AddField( + std::make_shared>(key, member, std::move(factory), required)); + return *this; + } + + SchemaBuilder& + TypeTag(const std::string& key, const std::string& value) { + this->schema_.AddField(std::make_shared(key, value)); + return *this; + } + + Schema + Build() { + return std::move(this->schema_); + } + +private: + Schema schema_; +}; + +} // namespace vsag::param + +// --------------------------------------------------------------------------- +// Macro DSL +// +// The declaration goes inside the class body in the header: +// +// class BruteForceParameter : public InnerIndexParameter { +// public: +// VSAG_PARAM_SCHEMA_DECL(); +// // ... other members ... +// }; +// +// The definition lives in the corresponding .cpp file so that the schema +// can reference helper types (factories, sub-parameter types) that should +// not be transitively imported by every consumer of the header: +// +// VSAG_PARAM_SCHEMA(BruteForceParameter) +// VSAG_PARAM_TYPE_TAG(TYPE_KEY, INDEX_TYPE_BRUTE_FORCE) +// VSAG_PARAM_SUBPARAM_REQUIRED(FlattenInterfaceParameter, base_codes_param, +// BASE_CODES_KEY, CreateFlattenParam) +// VSAG_PARAM_SCHEMA_END() +// +// The expansion produces a ``const Schema& ClassName::schema()`` definition +// whose function-local static is constructed once on first call. +// --------------------------------------------------------------------------- + +#define VSAG_PARAM_SCHEMA_DECL() static const ::vsag::param::Schema& schema() + +#define VSAG_PARAM_SCHEMA(ClassName) \ + const ::vsag::param::Schema& ClassName::schema() { \ + using SelfType = ClassName; \ + static const ::vsag::param::Schema instance = ::vsag::param::SchemaBuilder() + +#define VSAG_PARAM(Type, Member, Key, Default, ...) \ + .template Scalar(Key, &SelfType::Member, static_cast(Default), ##__VA_ARGS__) + +#define VSAG_PARAM_SUBPARAM(SubType, Member, Key, Factory) \ + .template SubParam(Key, &SelfType::Member, Factory, false) + +#define VSAG_PARAM_SUBPARAM_REQUIRED(SubType, Member, Key, Factory) \ + .template SubParam(Key, &SelfType::Member, Factory, true) + +#define VSAG_PARAM_TYPE_TAG(Key, Value) .TypeTag(Key, Value) + +#define VSAG_PARAM_SCHEMA_END() \ + .Build(); \ + return instance; \ + } diff --git a/src/param/schema_test.cpp b/src/param/schema_test.cpp new file mode 100644 index 0000000000..814738ce2e --- /dev/null +++ b/src/param/schema_test.cpp @@ -0,0 +1,162 @@ + +// Copyright 2024-present the vsag project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "param/schema.h" + +#include "parameter.h" +#include "unittest.h" + +namespace vsag::param { + +namespace { + +// Tiny sub-parameter used to exercise SubParamField. Stores a single int64 +// value and implements the minimal Parameter contract. +class TestSubParam : public vsag::Parameter { +public: + int64_t value{0}; + + void + FromJson(const vsag::JsonType& json) override { + if (json.Contains("value")) { + this->value = json["value"].GetInt(); + } + } + + vsag::JsonType + ToJson() const override { + vsag::JsonType j; + j["value"].SetInt(this->value); + return j; + } + + bool + CheckCompatibility(const vsag::ParamPtr& other) const override { + auto p = std::dynamic_pointer_cast(other); + return p != nullptr && p->value == this->value; + } +}; + +std::shared_ptr +CreateTestSub(const vsag::JsonType& json) { + auto sub = std::make_shared(); + sub->FromJson(json); + return sub; +} + +// Synthetic schema-driven parameter exercising every kind of field. +class TestParam : public vsag::Parameter { +public: + uint64_t dim{0}; + std::string name; + std::shared_ptr sub{nullptr}; + + VSAG_PARAM_SCHEMA_DECL(); + + void + FromJson(const vsag::JsonType& json) override { + schema().Parse(this, json); + schema().CheckUnknownKeys(json); + } + + vsag::JsonType + ToJson() const override { + vsag::JsonType j; + schema().Serialize(this, j); + return j; + } + + bool + CheckCompatibility(const vsag::ParamPtr& other) const override { + auto p = std::dynamic_pointer_cast(other); + if (p == nullptr) { + return false; + } + return schema().Equal(this, p.get()); + } +}; + +VSAG_PARAM_SCHEMA(TestParam) +VSAG_PARAM(uint64_t, dim, "dim", 128, ::vsag::param::Range(1, 1000000)) +VSAG_PARAM(std::string, name, "name", "default") +VSAG_PARAM_SUBPARAM(TestSubParam, sub, "sub", CreateTestSub) +VSAG_PARAM_TYPE_TAG("type", "test_param") +VSAG_PARAM_SCHEMA_END() + +} // namespace + +TEST_CASE("Schema parses scalar and sub-parameter fields", "[ut][param][schema]") { + auto param_str = R"({ + "dim": 256, + "name": "hello", + "sub": {"value": 42}, + "type": "test_param" + })"; + + TestParam param; + param.FromString(param_str); + + REQUIRE(param.dim == 256); + REQUIRE(param.name == "hello"); + REQUIRE(param.sub != nullptr); + REQUIRE(param.sub->value == 42); +} + +TEST_CASE("Schema produces stable JSON round-trip", "[ut][param][schema]") { + auto param_str = R"({ + "dim": 256, + "name": "hello", + "sub": {"value": 42}, + "type": "test_param" + })"; + + auto param = std::make_shared(); + param->FromString(param_str); + auto str1 = param->ToString(); + + auto param2 = std::make_shared(); + param2->FromString(str1); + auto str2 = param2->ToString(); + + REQUIRE(str1 == str2); + REQUIRE(param->CheckCompatibility(param2)); +} + +TEST_CASE("Schema rejects out-of-range scalar via Range validator", "[ut][param][schema]") { + auto param_str = R"({ + "dim": 0, + "name": "x", + "sub": {"value": 1}, + "type": "test_param" + })"; + + TestParam param; + REQUIRE_THROWS_AS(param.FromString(param_str), vsag::VsagException); +} + +TEST_CASE("Schema rejects unknown JSON keys", "[ut][param][schema]") { + auto param_str = R"({ + "dim": 256, + "name": "x", + "sub": {"value": 1}, + "type": "test_param", + "unknown_field": true + })"; + + TestParam param; + REQUIRE_THROWS_AS(param.FromString(param_str), vsag::VsagException); +} + +} // namespace vsag::param diff --git a/src/param/validators.h b/src/param/validators.h new file mode 100644 index 0000000000..5e56c6ae96 --- /dev/null +++ b/src/param/validators.h @@ -0,0 +1,66 @@ + +// Copyright 2024-present the vsag project +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include +#include + +#include "common.h" + +namespace vsag::param { + +// Validator signature: inspects the parsed scalar value and throws a +// VsagException(INVALID_ARGUMENT) on failure. Returning normally signals +// "accepted". Validators are stored inside schema field descriptors and +// invoked from FromJson() after the JSON value is converted to the field +// type. +template +using ValidatorFn = std::function; + +// Inclusive numeric range validator. The template parameter T must match the +// field type exactly to avoid silent overflow/underflow/truncation from +// cross-type comparisons. Use explicit type at construction site: +// +// VSAG_PARAM(uint64_t, dim, DIM_KEY, 128, Range(1, 1000000)) +// +// The constructor accepts arbitrary arithmetic literals and static_casts them +// to T once during construction, after which all comparisons are same-type. +template +struct Range { + T min_value; + T max_value; + + Range(T min_v, T max_v) : min_value(min_v), max_value(max_v) { + } + + template && std::is_arithmetic_v, int> = 0> + Range(A min_v, B max_v) : min_value(static_cast(min_v)), max_value(static_cast(max_v)) { + } + + void + operator()(const std::string& key, const T& value) const { + CHECK_ARGUMENT( + value >= min_value && value <= max_value, + fmt::format( + "parameter '{}' value {} out of range [{}, {}]", key, value, min_value, max_value)); + } +}; + +} // namespace vsag::param diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index f9205c18b1..e2f828e814 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -29,7 +29,7 @@ target_link_libraries (unittests PRIVATE Catch2::Catch2 simd_test vsag_test algorithm_test factory_test attr_test - datacell_test quantizer_test storage_test io_test utils_test impl_test + datacell_test param_test quantizer_test storage_test io_test utils_test impl_test vsag_static ) @@ -39,9 +39,10 @@ if (APPLE) "-Wl,-force_load,$" "-Wl,-force_load,$" "-Wl,-force_load,$" + "-Wl,-force_load,$" "-Wl,-force_load,$" "-Wl,-force_load,$" - "-Wl,-force_load,$" + "-Wl,-force_load,$ $" "-Wl,-force_load,$" "-Wl,-force_load,$" "-Wl,-force_load,$" @@ -53,7 +54,7 @@ else() PRIVATE "-Wl,--whole-archive" simd_test vsag_test algorithm_test factory_test attr_test - datacell_test quantizer_test storage_test io_test utils_test impl_test + datacell_test param_test quantizer_test storage_test io_test utils_test impl_test "-Wl,--no-whole-archive" ) endif()