Skip to content
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions source/config_schema/schema_server.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#include "schema_server.hpp"

#include <spdlog/fmt/fmt.h>

#include <regex>
#include <unordered_set>

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<std::string> 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 << "Unknown field '" << field;
ss << "' found in configuration:\n";
ss << subnode;
ss << "This field is not described in schema:\n";
ss << schema_node;
throw config_node.create_parsing_error(ss.str());
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
}
}

// 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 << "Required field '" << required_field;
ss << "' is missing in configuration: \n";
ss << config_node << '\n';
ss << "Field is required by schema node: \n";
ss << schema_node << "\n";
throw config_node.create_parsing_error(ss.str());
Comment thread
S1mpotyaga marked this conversation as resolved.
}
}

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) {
std::string type = schema_node["_type"].value().as<std::string>().value();
if (type == "size_t") {
config_node.as_or_throw<size_t>();
} else if (type == "int") {
config_node.as_or_throw<int>();
} else if (type == "double") {
config_node.as_or_throw<double>();
} else if (type == "bool") {
config_node.as_or_throw<bool>();
} else if (type == "string") {
config_node.as_or_throw<std::string>();
} else if (type == "regex") {
std::string pattern = config_node.as_or_throw<std::string>();
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
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) {
ConfigSchema type_node = schema_node["_type"].value();
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
std::string type = type_node.as_or_throw<std::string>();
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(__FILE__).parent_path() /
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
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<std::string>();
if (try_validate_basic_types(schema_node, config_node)) {
return;
}
if (try_validate_custom_types(schema_node, config_node)) {
return;
}
std::stringstream ss;
ss << "Unknown specified type '" << type << "' in schema: ";
ss << schema_node;
throw schema_node.create_parsing_error(ss.str());
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
} else {
if (!config_node.IsMap()) {
std::stringstream ss;
ss << "Expected object/map for config node:\n";
ss << config_node << ".\n";
ss << "Because schema\n";
ss << schema_node << '\n';
ss << " has nested fields";
throw config_node.create_parsing_error(ss.str());
Comment thread
S1mpotyaga marked this conversation as resolved.
}
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()));
}
Comment thread
S1mpotyaga marked this conversation as resolved.
validate(exp_sub_schema.value(), config_node);
}

} // namespace sim
41 changes: 41 additions & 0 deletions source/config_schema/schema_server.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#pragma once

#include "parser/config_reader/config_node_with_preset.hpp"

namespace sim {

using ConfigSchema = ConfigNode;

class SchemaServer {
public:
SchemaServer(const std::filesystem::path& a_schema_path);
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated

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);
Comment thread
S1mpotyaga marked this conversation as resolved.

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
10 changes: 10 additions & 0 deletions source/parser/config_reader/config_node_expected.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,14 @@ ConfigNodeExpected::ConfigNodeExpected(utils::StrExpected<ConfigNode> 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
2 changes: 2 additions & 0 deletions source/parser/config_reader/config_node_expected.hpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include "spdlog/fmt/fmt.h"
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
#include "utils/str_expected.hpp"

namespace sim {
Expand All @@ -25,4 +26,5 @@ class ConfigNodeExpected : public utils::StrExpected<ConfigNode> {
[[nodiscard]] ConfigNodeExpected operator[](std::string_view key) const;
};

ConfigNodeExpected safe_load_file(std::filesystem::path path) noexcept;
} // namespace sim
38 changes: 38 additions & 0 deletions source/parser/config_reader/config_node_with_preset_.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
}
Expand Down Expand Up @@ -117,4 +120,39 @@ ConfigNodeWithPreset load_file_with_presets(std::filesystem::path path) {
return ConfigNodeWithPreset(node, node["presets"].to_optional());
}

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
25 changes: 25 additions & 0 deletions source/parser/config_reader/config_node_with_preset_.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions test/config_schema/_schemas/basic_types.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node:
field1:
_type: size_t
field2:
_type: int
field3:
_type: double
field4:
_type: bool
field5:
_type: string
field6:
_type: regex
2 changes: 2 additions & 0 deletions test/config_schema/_schemas/custom_type.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
_type: int
_doc: Custom type
5 changes: 5 additions & 0 deletions test/config_schema/_schemas/nested_custom_type.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
node:
Comment thread
S1mpotyaga marked this conversation as resolved.
field1:
_type: /custom_type.schema
field2:
_type: int
2 changes: 2 additions & 0 deletions test/config_schema/_schemas/root_is_type.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node:
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
_type: bool
4 changes: 4 additions & 0 deletions test/config_schema/_schemas/test_paths.schema
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
absolute_path:
Comment thread
S1mpotyaga marked this conversation as resolved.
Outdated
_type: /nested_absolute_path.schema
relative_path:
_type: test_relative_paths/relative_path.schema
7 changes: 7 additions & 0 deletions test/config_schema/basic_types.yml
Original file line number Diff line number Diff line change
@@ -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,}$
7 changes: 7 additions & 0 deletions test/config_schema/basic_types_wrong.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node:
field1: hello
field2: hello
field3: hello
field4: hello
field5: hello
field6: 1.25
Loading
Loading