diff --git a/source/config_schema/schema_server.cpp b/source/config_schema/schema_server.cpp new file mode 100644 index 0000000000..f902f5c219 --- /dev/null +++ b/source/config_schema/schema_server.cpp @@ -0,0 +1,162 @@ +#include "schema_server.hpp" + +#include + +#include +#include + +namespace sim { + +SchemaServer::SchemaServer(const std::filesystem::path& a_schemas_dir) + : m_schemas_dir(a_schemas_dir) {} + +bool SchemaServer::is_meta_field(const std::string& field) { + return field.starts_with('_'); +} + +void SchemaServer::validate_untyped(const ConfigSchema& schema_node, + const ConfigNodeWithPreset& config_node) { + // create set of requried fields + std::unordered_set schema_fields; + for (const auto& it : schema_node) { + const std::string& field = it.get_name_or_throw(); + if (!is_meta_field(field)) { + schema_fields.insert(field); + } + } + + // check if config node has unknown fields + for (const auto& subnode : config_node) { + const std::string& field = subnode.get_name_or_throw(); + if (!schema_fields.contains(field)) { + std::stringstream ss; + ss << "Node has field `'" << field + << "' that does not described in schema:\n"; + ss << schema_node; + throw subnode.create_parsing_error(ss.str()); + } + } + + // check if config node has all required fields + for (const auto& required_field : schema_fields) { + ConfigNodeWithPresetExpected exp_field_config = + config_node[required_field]; + if (!exp_field_config.has_value()) { + std::stringstream ss; + ss << "Missing required field '" << required_field + << "' described in schema: \n"; + ss << schema_node << "\n"; + throw config_node.create_parsing_error(ss.str()); + } + } + + for (const auto& it : schema_node) { + std::string field = it.get_name().value(); + if (!is_meta_field(field)) { + validate(schema_node[field].value(), config_node[field].value()); + } + } +} + +[[nodiscard]] bool SchemaServer::try_validate_basic_types( + const ConfigSchema& schema_node, const ConfigNodeWithPreset& config_node) { + const ConfigSchema type_node = schema_node["_type"].value(); + std::string type = type_node.as_or_throw(); + auto unsafe_cast_config_node_to = [&]() -> T { + auto as_result = config_node.as(); + if (!as_result.has_value()) { + std::stringstream ss; + ss << "Node should contain basic type `" << type + << "' due to schema:\n"; + ss << type_node << "\n"; + ss << "But its not:\n"; + ss << as_result.error() << '\n'; + throw config_node.create_parsing_error(ss.str()); + } + return as_result.value(); + }; + if (type == "size_t") { + unsafe_cast_config_node_to.operator()(); + } else if (type == "int") { + unsafe_cast_config_node_to.operator()(); + } else if (type == "double") { + unsafe_cast_config_node_to.operator()(); + } else if (type == "bool") { + unsafe_cast_config_node_to.operator()(); + } else if (type == "string") { + unsafe_cast_config_node_to.operator()(); + } else if (type == "regex") { + std::string pattern = + unsafe_cast_config_node_to.operator()(); + try { + std::regex r(pattern); + } catch (const std::regex_error&) { + std::stringstream ss; + ss << "Field must contain valid regular expression.\n"; + ss << "Regex pattern: " << pattern << '\n'; + throw config_node.create_parsing_error(ss.str()); + } + } else { + return false; + } + return true; +} + +[[nodiscard]] bool SchemaServer::try_validate_custom_types( + const ConfigSchema& schema_node, const ConfigNodeWithPreset& config_node) { + const ConfigSchema type_node = schema_node["_type"].value(); + std::string type = type_node.as_or_throw(); + if (type.ends_with(".schema")) { + std::filesystem::path nested_schema_path = std::filesystem::path(type); + std::filesystem::path sub_schema_path = + nested_schema_path.is_absolute() + ? m_schemas_dir / nested_schema_path.relative_path() + : std::filesystem::path(schema_node.get_config_path().value()) + .parent_path() / + nested_schema_path; + validate(sub_schema_path, config_node); + return true; + } else { + return false; + } +} + +void SchemaServer::validate(const ConfigSchema& schema_node, + const ConfigNodeWithPreset& config_node) { + ConfigNodeExpected exp_type_node = schema_node["_type"]; + if (exp_type_node) { + std::string type = exp_type_node.value().as_or_throw(); + if (try_validate_basic_types(schema_node, config_node)) { + return; + } + if (try_validate_custom_types(schema_node, config_node)) { + return; + } + std::stringstream ss; + throw schema_node.create_parsing_error( + fmt::format("Unknown specified type '{}'", type)); + } else { + if (!config_node.IsMap()) { + std::stringstream ss; + ss << "Should be map due to schema\n"; + ss << schema_node << '\n'; + throw config_node.create_parsing_error(ss.str()); + } + validate_untyped(schema_node, config_node); + } +} + +void SchemaServer::validate(const std::filesystem::path& schema_path, + const ConfigNodeWithPreset& config_node) { + std::filesystem::path full_path = + schema_path.is_absolute() ? schema_path : m_schemas_dir / schema_path; + auto exp_sub_schema = safe_load_file(full_path); + if (!exp_sub_schema) { + throw config_node.create_parsing_error( + fmt::format("Failed to parse corresponding schema file: {}", + exp_sub_schema.error())); + } + validate(exp_sub_schema.value(), config_node); +} + +} // namespace sim \ No newline at end of file diff --git a/source/config_schema/schema_server.hpp b/source/config_schema/schema_server.hpp new file mode 100644 index 0000000000..bc45c1a9c8 --- /dev/null +++ b/source/config_schema/schema_server.hpp @@ -0,0 +1,41 @@ +#pragma once + +#include "parser/config_reader/config_node_with_preset.hpp" + +namespace sim { + +using ConfigSchema = ConfigNode; + +class SchemaServer { +public: + explicit SchemaServer(const std::filesystem::path& a_schemas_dir); + + void validate(const ConfigSchema& schema_node, + const ConfigNodeWithPreset& config_node); + + void validate(const std::filesystem::path& schema_path, + const ConfigNodeWithPreset& config_node); + +private: + // if schema_node correspond to basic type, validate config_node and returns + // true otherwise, returns false + [[nodiscard]] bool try_validate_basic_types( + const ConfigSchema& schema_node, + const ConfigNodeWithPreset& config_node); + + // if schema_node correspond to custom type, validate config_node and + // returns true otherwise, returns false + [[nodiscard]] bool try_validate_custom_types( + const ConfigSchema& schema_node, + const ConfigNodeWithPreset& config_node); + + void validate_untyped(const ConfigSchema& schema_node, + const ConfigNodeWithPreset& config_node); + + static bool is_meta_field(const std::string& field); + +private: + const std::filesystem::path m_schemas_dir; +}; + +} // namespace sim \ No newline at end of file diff --git a/source/parser/config_reader/config_node_expected.cpp b/source/parser/config_reader/config_node_expected.cpp index 6e37c99336..8b5262c72a 100644 --- a/source/parser/config_reader/config_node_expected.cpp +++ b/source/parser/config_reader/config_node_expected.cpp @@ -1,3 +1,5 @@ +#include + #include "config_node.hpp" namespace sim { @@ -29,4 +31,14 @@ ConfigNodeExpected::ConfigNodeExpected(utils::StrExpected a_node) return this->value()[key]; } +ConfigNodeExpected safe_load_file(std::filesystem::path path) noexcept { + try { + return ConfigNode(YAML::LoadFile(path.string()), std::nullopt, path); + } catch (const std::exception& ex) { + return std::unexpected( + fmt::format("Failed to parse file at path: {}, due to error: {}", + path.string(), ex.what())); + } +} + } // namespace sim \ No newline at end of file diff --git a/source/parser/config_reader/config_node_expected.hpp b/source/parser/config_reader/config_node_expected.hpp index 6ba1ed4deb..387b255526 100644 --- a/source/parser/config_reader/config_node_expected.hpp +++ b/source/parser/config_reader/config_node_expected.hpp @@ -25,4 +25,5 @@ class ConfigNodeExpected : public utils::StrExpected { [[nodiscard]] ConfigNodeExpected operator[](std::string_view key) const; }; +ConfigNodeExpected safe_load_file(std::filesystem::path path) noexcept; } // namespace sim \ No newline at end of file diff --git a/source/parser/config_reader/config_node_with_preset_.cpp b/source/parser/config_reader/config_node_with_preset_.cpp index 68ed4d275c..45ec292123 100644 --- a/source/parser/config_reader/config_node_with_preset_.cpp +++ b/source/parser/config_reader/config_node_with_preset_.cpp @@ -59,6 +59,7 @@ ConfigNodeWithPresetExpected ConfigNodeWithPreset::operator[]( return std::unexpected(ss.str()); } // preset was found successfull. put the found value in m_preset + m_preset.emplace(preset_node.value()); } // tries to find key in preset @@ -80,6 +81,8 @@ ConfigNodeWithPresetExpected ConfigNodeWithPreset::operator[]( std::nullopt); } +bool ConfigNodeWithPreset::IsMap() const noexcept { return m_node.IsMap(); } + std::ostream& operator<<(std::ostream& out, const ConfigNodeWithPreset& node) { return out << node.get_node(); } @@ -114,7 +117,45 @@ const std::optional ConfigNodeWithPreset::get_presets_node() ConfigNodeWithPreset load_file_with_presets(std::filesystem::path path) { ConfigNode node = load_file(path); - return ConfigNodeWithPreset(node, node["presets"].to_optional()); + if (node.IsMap()) { + return ConfigNodeWithPreset(node, node["presets"].to_optional()); + } + return ConfigNodeWithPreset(node, std::nullopt); +} + +ConfigNodeWithPreset::Iterator::Iterator(ConfigNode::Iterator a_it, + const ConfigNodeWithPreset& a_parent) + : m_iterator(a_it), m_parent(a_parent) {} + +ConfigNodeWithPreset::Iterator& ConfigNodeWithPreset::Iterator::operator++() { + ++m_iterator; + return *this; +} + +ConfigNodeWithPreset::Iterator ConfigNodeWithPreset::Iterator::operator++(int) { + Iterator iterator_copy(*this); + ++(*this); + return iterator_copy; +} + +bool ConfigNodeWithPreset::Iterator::operator==(const Iterator& rhs) const { + return m_iterator == rhs.m_iterator; +} + +bool ConfigNodeWithPreset::Iterator::operator!=(const Iterator& rhs) const { + return m_iterator != rhs.m_iterator; +} + +ConfigNodeWithPreset ConfigNodeWithPreset::Iterator::operator*() const { + return ConfigNodeWithPreset(*m_iterator, m_parent.get_presets_node()); +} + +ConfigNodeWithPreset::Iterator ConfigNodeWithPreset::begin() const { + return Iterator(m_node.begin(), *this); +} + +ConfigNodeWithPreset::Iterator ConfigNodeWithPreset::end() const { + return Iterator(m_node.end(), *this); } } // namespace sim diff --git a/source/parser/config_reader/config_node_with_preset_.hpp b/source/parser/config_reader/config_node_with_preset_.hpp index 8d126c54bf..7792371a14 100644 --- a/source/parser/config_reader/config_node_with_preset_.hpp +++ b/source/parser/config_reader/config_node_with_preset_.hpp @@ -16,6 +16,8 @@ class ConfigNodeWithPreset { ConfigNodeWithPresetExpected operator[](std::string_view key) const; + [[nodiscard]] bool IsMap() const noexcept; + friend std::ostream& operator<<(std::ostream& out, const ConfigNodeWithPreset& node); @@ -44,6 +46,29 @@ class ConfigNodeWithPreset { std::runtime_error create_parsing_error(std::string_view error) const; + class Iterator { + public: + Iterator(ConfigNode::Iterator a_it, + const ConfigNodeWithPreset& a_parent); + + Iterator& operator++(); + + Iterator operator++(int); + + bool operator==(const Iterator& rhs) const; + + bool operator!=(const Iterator& rhs) const; + + ConfigNodeWithPreset operator*() const; + + private: + ConfigNode::Iterator m_iterator; + const ConfigNodeWithPreset& m_parent; + }; + + Iterator begin() const; + Iterator end() const; + private: // m_node contains information about config node and probably preset name // m_preset - preset node which that is used to supplement fields of m_node diff --git a/test/config_schema/_schemas/absolute_path.schema b/test/config_schema/_schemas/absolute_path.schema new file mode 100644 index 0000000000..d2e4798693 --- /dev/null +++ b/test/config_schema/_schemas/absolute_path.schema @@ -0,0 +1 @@ +_type: int \ No newline at end of file diff --git a/test/config_schema/_schemas/basic_types.schema b/test/config_schema/_schemas/basic_types.schema new file mode 100644 index 0000000000..7ee1aa33b7 --- /dev/null +++ b/test/config_schema/_schemas/basic_types.schema @@ -0,0 +1,13 @@ +node: + field1: + _type: size_t + field2: + _type: int + field3: + _type: double + field4: + _type: bool + field5: + _type: string + field6: + _type: regex \ No newline at end of file diff --git a/test/config_schema/_schemas/custom_type.schema b/test/config_schema/_schemas/custom_type.schema new file mode 100644 index 0000000000..8c1105cfb7 --- /dev/null +++ b/test/config_schema/_schemas/custom_type.schema @@ -0,0 +1,2 @@ +_type: int +_doc: Custom type \ No newline at end of file diff --git a/test/config_schema/_schemas/nested_custom_type.schema b/test/config_schema/_schemas/nested_custom_type.schema new file mode 100644 index 0000000000..5da1db2f8e --- /dev/null +++ b/test/config_schema/_schemas/nested_custom_type.schema @@ -0,0 +1,5 @@ +node: + field1: + _type: /custom_type.schema + field2: + _type: int \ No newline at end of file diff --git a/test/config_schema/_schemas/root_is_type.schema b/test/config_schema/_schemas/root_is_type.schema new file mode 100644 index 0000000000..e3c62f9cb9 --- /dev/null +++ b/test/config_schema/_schemas/root_is_type.schema @@ -0,0 +1 @@ +_type: bool \ No newline at end of file diff --git a/test/config_schema/_schemas/test_paths/test_paths.schema b/test/config_schema/_schemas/test_paths/test_paths.schema new file mode 100644 index 0000000000..08964b5931 --- /dev/null +++ b/test/config_schema/_schemas/test_paths/test_paths.schema @@ -0,0 +1,4 @@ +absolute_path: + _type: /absolute_path.schema +relative_path: + _type: test_relative_path/relative_path.schema \ No newline at end of file diff --git a/test/config_schema/_schemas/test_paths/test_relative_path/relative_path.schema b/test/config_schema/_schemas/test_paths/test_relative_path/relative_path.schema new file mode 100644 index 0000000000..e3c62f9cb9 --- /dev/null +++ b/test/config_schema/_schemas/test_paths/test_relative_path/relative_path.schema @@ -0,0 +1 @@ +_type: bool \ No newline at end of file diff --git a/test/config_schema/check_schemas.cpp b/test/config_schema/check_schemas.cpp new file mode 100644 index 0000000000..7074323571 --- /dev/null +++ b/test/config_schema/check_schemas.cpp @@ -0,0 +1,86 @@ +#include +#include + +#include "config_schema/schema_server.hpp" + +namespace sim { +namespace test2 { + +class SchemaTest : public ::testing::Test { +protected: + void SetUp() override { + current_dir = std::filesystem::path(__FILE__).parent_path(); + config_nodes_dir = current_dir / "config_nodes"; + schemas_dir = current_dir / "_schemas"; + schema_server = std::make_unique(schemas_dir); + } + + ConfigSchema load_schema(const std::string& name) { + return load_file(schemas_dir / name); + } + + ConfigNodeWithPreset load_config_node_with_preset(const std::string& name) { + return load_file_with_presets(config_nodes_dir / name); + } + + std::filesystem::path current_dir; + std::filesystem::path config_nodes_dir; + std::filesystem::path schemas_dir; + std::unique_ptr schema_server; +}; + +TEST_F(SchemaTest, BasicTypes) { + ConfigNodeWithPreset node = load_config_node_with_preset("basic_types.yml"); + ConfigSchema schema_node = load_schema("basic_types.schema"); + + ASSERT_NO_THROW(schema_server->validate(schema_node, node)); +} + +TEST_F(SchemaTest, WrongBasicTypes) { + ConfigNodeWithPreset node = + load_config_node_with_preset("basic_types_wrong.yml"); + ConfigSchema schema_node = load_schema("basic_types.schema"); + + ASSERT_ANY_THROW(schema_server->validate(schema_node, node)); +} + +TEST_F(SchemaTest, CustomType) { + ConfigSchema schema = load_schema("nested_custom_type.schema"); + ConfigNodeWithPreset node = load_config_node_with_preset("custom_type.yml"); + + ASSERT_NO_THROW(schema_server->validate(schema, node)); +} + +TEST_F(SchemaTest, WrongCustomType) { + ConfigSchema schema = load_schema("nested_custom_type.schema"); + ConfigNodeWithPreset node = + load_config_node_with_preset("custom_type_wrong.yml"); + + ASSERT_ANY_THROW(schema_server->validate(schema, node)); +} + +TEST_F(SchemaTest, RootIsType) { + ConfigSchema schema = load_schema("root_is_type.schema"); + ConfigNodeWithPreset node = + load_config_node_with_preset("root_is_type.yml"); + + ASSERT_NO_THROW(schema_server->validate(schema, node)); +} + +TEST_F(SchemaTest, WrongRootIsType) { + ConfigSchema schema = load_schema("root_is_type.schema"); + ConfigNodeWithPreset node = + load_config_node_with_preset("root_is_type_wrong.yml"); + + ASSERT_ANY_THROW(schema_server->validate(schema, node)); +} + +TEST_F(SchemaTest, CheckPaths) { + ConfigSchema schema = load_schema("test_paths/test_paths.schema"); + ConfigNodeWithPreset config_node = + load_config_node_with_preset("test_paths.yml"); + ASSERT_NO_THROW(schema_server->validate(schema, config_node)); +} + +} // namespace test2 +} // namespace sim \ No newline at end of file diff --git a/test/config_schema/config_nodes/basic_types.yml b/test/config_schema/config_nodes/basic_types.yml new file mode 100644 index 0000000000..1049c47e91 --- /dev/null +++ b/test/config_schema/config_nodes/basic_types.yml @@ -0,0 +1,7 @@ +node: + field1: 12389 + field2: 21039 + field3: 1.35 + field4: false + field5: hello + field6: ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ \ No newline at end of file diff --git a/test/config_schema/config_nodes/basic_types_wrong.yml b/test/config_schema/config_nodes/basic_types_wrong.yml new file mode 100644 index 0000000000..5639e631bd --- /dev/null +++ b/test/config_schema/config_nodes/basic_types_wrong.yml @@ -0,0 +1,7 @@ +node: + field1: hello + field2: hello + field3: hello + field4: hello + field5: hello + field6: 1.25 \ No newline at end of file diff --git a/test/config_schema/config_nodes/custom_type.yml b/test/config_schema/config_nodes/custom_type.yml new file mode 100644 index 0000000000..cae29acba1 --- /dev/null +++ b/test/config_schema/config_nodes/custom_type.yml @@ -0,0 +1,3 @@ +node: + field1: 123 + field2: 456 \ No newline at end of file diff --git a/test/config_schema/config_nodes/custom_type_wrong.yml b/test/config_schema/config_nodes/custom_type_wrong.yml new file mode 100644 index 0000000000..82d3dabf18 --- /dev/null +++ b/test/config_schema/config_nodes/custom_type_wrong.yml @@ -0,0 +1,3 @@ +node: + field1: hello + field2: 456 \ No newline at end of file diff --git a/test/config_schema/config_nodes/root_is_type.yml b/test/config_schema/config_nodes/root_is_type.yml new file mode 100644 index 0000000000..f32a5804e2 --- /dev/null +++ b/test/config_schema/config_nodes/root_is_type.yml @@ -0,0 +1 @@ +true \ No newline at end of file diff --git a/test/config_schema/config_nodes/root_is_type_wrong.yml b/test/config_schema/config_nodes/root_is_type_wrong.yml new file mode 100644 index 0000000000..d800886d9c --- /dev/null +++ b/test/config_schema/config_nodes/root_is_type_wrong.yml @@ -0,0 +1 @@ +123 \ No newline at end of file diff --git a/test/config_schema/config_nodes/test_paths.yml b/test/config_schema/config_nodes/test_paths.yml new file mode 100644 index 0000000000..66eaa48c4c --- /dev/null +++ b/test/config_schema/config_nodes/test_paths.yml @@ -0,0 +1,2 @@ +absolute_path: 123 +relative_path: true \ No newline at end of file