Skip to content

Commit 6a1a2a3

Browse files
S1mpotyagaroot
andauthored
FEAT: implement yaml schemas (#546)
done #536 #538 --------- Co-authored-by: root <root@zfk.localdomain>
1 parent 1fe257f commit 6a1a2a3

21 files changed

Lines changed: 420 additions & 1 deletion
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#include "schema_server.hpp"
2+
3+
#include <spdlog/fmt/fmt.h>
4+
5+
#include <regex>
6+
#include <unordered_set>
7+
8+
namespace sim {
9+
10+
SchemaServer::SchemaServer(const std::filesystem::path& a_schemas_dir)
11+
: m_schemas_dir(a_schemas_dir) {}
12+
13+
bool SchemaServer::is_meta_field(const std::string& field) {
14+
return field.starts_with('_');
15+
}
16+
17+
void SchemaServer::validate_untyped(const ConfigSchema& schema_node,
18+
const ConfigNodeWithPreset& config_node) {
19+
// create set of requried fields
20+
std::unordered_set<std::string> schema_fields;
21+
for (const auto& it : schema_node) {
22+
const std::string& field = it.get_name_or_throw();
23+
if (!is_meta_field(field)) {
24+
schema_fields.insert(field);
25+
}
26+
}
27+
28+
// check if config node has unknown fields
29+
for (const auto& subnode : config_node) {
30+
const std::string& field = subnode.get_name_or_throw();
31+
if (!schema_fields.contains(field)) {
32+
std::stringstream ss;
33+
ss << "Node has field `'" << field
34+
<< "' that does not described in schema:\n";
35+
ss << schema_node;
36+
throw subnode.create_parsing_error(ss.str());
37+
}
38+
}
39+
40+
// check if config node has all required fields
41+
for (const auto& required_field : schema_fields) {
42+
ConfigNodeWithPresetExpected exp_field_config =
43+
config_node[required_field];
44+
if (!exp_field_config.has_value()) {
45+
std::stringstream ss;
46+
ss << "Missing required field '" << required_field
47+
<< "' described in schema: \n";
48+
ss << schema_node << "\n";
49+
throw config_node.create_parsing_error(ss.str());
50+
}
51+
}
52+
53+
for (const auto& it : schema_node) {
54+
std::string field = it.get_name().value();
55+
if (!is_meta_field(field)) {
56+
validate(schema_node[field].value(), config_node[field].value());
57+
}
58+
}
59+
}
60+
61+
[[nodiscard]] bool SchemaServer::try_validate_basic_types(
62+
const ConfigSchema& schema_node, const ConfigNodeWithPreset& config_node) {
63+
const ConfigSchema type_node = schema_node["_type"].value();
64+
std::string type = type_node.as_or_throw<std::string>();
65+
auto unsafe_cast_config_node_to = [&]<typename T>() -> T {
66+
auto as_result = config_node.as<T>();
67+
if (!as_result.has_value()) {
68+
std::stringstream ss;
69+
ss << "Node should contain basic type `" << type
70+
<< "' due to schema:\n";
71+
ss << type_node << "\n";
72+
ss << "But its not:\n";
73+
ss << as_result.error() << '\n';
74+
throw config_node.create_parsing_error(ss.str());
75+
}
76+
return as_result.value();
77+
};
78+
if (type == "size_t") {
79+
unsafe_cast_config_node_to.operator()<size_t>();
80+
} else if (type == "int") {
81+
unsafe_cast_config_node_to.operator()<int>();
82+
} else if (type == "double") {
83+
unsafe_cast_config_node_to.operator()<double>();
84+
} else if (type == "bool") {
85+
unsafe_cast_config_node_to.operator()<bool>();
86+
} else if (type == "string") {
87+
unsafe_cast_config_node_to.operator()<std::string>();
88+
} else if (type == "regex") {
89+
std::string pattern =
90+
unsafe_cast_config_node_to.operator()<std::string>();
91+
try {
92+
std::regex r(pattern);
93+
} catch (const std::regex_error&) {
94+
std::stringstream ss;
95+
ss << "Field must contain valid regular expression.\n";
96+
ss << "Regex pattern: " << pattern << '\n';
97+
throw config_node.create_parsing_error(ss.str());
98+
}
99+
} else {
100+
return false;
101+
}
102+
return true;
103+
}
104+
105+
[[nodiscard]] bool SchemaServer::try_validate_custom_types(
106+
const ConfigSchema& schema_node, const ConfigNodeWithPreset& config_node) {
107+
const ConfigSchema type_node = schema_node["_type"].value();
108+
std::string type = type_node.as_or_throw<std::string>();
109+
if (type.ends_with(".schema")) {
110+
std::filesystem::path nested_schema_path = std::filesystem::path(type);
111+
std::filesystem::path sub_schema_path =
112+
nested_schema_path.is_absolute()
113+
? m_schemas_dir / nested_schema_path.relative_path()
114+
: std::filesystem::path(schema_node.get_config_path().value())
115+
.parent_path() /
116+
nested_schema_path;
117+
validate(sub_schema_path, config_node);
118+
return true;
119+
} else {
120+
return false;
121+
}
122+
}
123+
124+
void SchemaServer::validate(const ConfigSchema& schema_node,
125+
const ConfigNodeWithPreset& config_node) {
126+
ConfigNodeExpected exp_type_node = schema_node["_type"];
127+
if (exp_type_node) {
128+
std::string type = exp_type_node.value().as_or_throw<std::string>();
129+
if (try_validate_basic_types(schema_node, config_node)) {
130+
return;
131+
}
132+
if (try_validate_custom_types(schema_node, config_node)) {
133+
return;
134+
}
135+
std::stringstream ss;
136+
throw schema_node.create_parsing_error(
137+
fmt::format("Unknown specified type '{}'", type));
138+
} else {
139+
if (!config_node.IsMap()) {
140+
std::stringstream ss;
141+
ss << "Should be map due to schema\n";
142+
ss << schema_node << '\n';
143+
throw config_node.create_parsing_error(ss.str());
144+
}
145+
validate_untyped(schema_node, config_node);
146+
}
147+
}
148+
149+
void SchemaServer::validate(const std::filesystem::path& schema_path,
150+
const ConfigNodeWithPreset& config_node) {
151+
std::filesystem::path full_path =
152+
schema_path.is_absolute() ? schema_path : m_schemas_dir / schema_path;
153+
auto exp_sub_schema = safe_load_file(full_path);
154+
if (!exp_sub_schema) {
155+
throw config_node.create_parsing_error(
156+
fmt::format("Failed to parse corresponding schema file: {}",
157+
exp_sub_schema.error()));
158+
}
159+
validate(exp_sub_schema.value(), config_node);
160+
}
161+
162+
} // namespace sim
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#pragma once
2+
3+
#include "parser/config_reader/config_node_with_preset.hpp"
4+
5+
namespace sim {
6+
7+
using ConfigSchema = ConfigNode;
8+
9+
class SchemaServer {
10+
public:
11+
explicit SchemaServer(const std::filesystem::path& a_schemas_dir);
12+
13+
void validate(const ConfigSchema& schema_node,
14+
const ConfigNodeWithPreset& config_node);
15+
16+
void validate(const std::filesystem::path& schema_path,
17+
const ConfigNodeWithPreset& config_node);
18+
19+
private:
20+
// if schema_node correspond to basic type, validate config_node and returns
21+
// true otherwise, returns false
22+
[[nodiscard]] bool try_validate_basic_types(
23+
const ConfigSchema& schema_node,
24+
const ConfigNodeWithPreset& config_node);
25+
26+
// if schema_node correspond to custom type, validate config_node and
27+
// returns true otherwise, returns false
28+
[[nodiscard]] bool try_validate_custom_types(
29+
const ConfigSchema& schema_node,
30+
const ConfigNodeWithPreset& config_node);
31+
32+
void validate_untyped(const ConfigSchema& schema_node,
33+
const ConfigNodeWithPreset& config_node);
34+
35+
static bool is_meta_field(const std::string& field);
36+
37+
private:
38+
const std::filesystem::path m_schemas_dir;
39+
};
40+
41+
} // namespace sim

source/parser/config_reader/config_node_expected.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#include <spdlog/fmt/fmt.h>
2+
13
#include "config_node.hpp"
24

35
namespace sim {
@@ -29,4 +31,14 @@ ConfigNodeExpected::ConfigNodeExpected(utils::StrExpected<ConfigNode> a_node)
2931
return this->value()[key];
3032
}
3133

34+
ConfigNodeExpected safe_load_file(std::filesystem::path path) noexcept {
35+
try {
36+
return ConfigNode(YAML::LoadFile(path.string()), std::nullopt, path);
37+
} catch (const std::exception& ex) {
38+
return std::unexpected(
39+
fmt::format("Failed to parse file at path: {}, due to error: {}",
40+
path.string(), ex.what()));
41+
}
42+
}
43+
3244
} // namespace sim

source/parser/config_reader/config_node_expected.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,5 @@ class ConfigNodeExpected : public utils::StrExpected<ConfigNode> {
2525
[[nodiscard]] ConfigNodeExpected operator[](std::string_view key) const;
2626
};
2727

28+
ConfigNodeExpected safe_load_file(std::filesystem::path path) noexcept;
2829
} // namespace sim

source/parser/config_reader/config_node_with_preset_.cpp

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ ConfigNodeWithPresetExpected ConfigNodeWithPreset::operator[](
5959
return std::unexpected(ss.str());
6060
}
6161
// preset was found successfull. put the found value in m_preset
62+
6263
m_preset.emplace(preset_node.value());
6364
}
6465
// tries to find key in preset
@@ -80,6 +81,8 @@ ConfigNodeWithPresetExpected ConfigNodeWithPreset::operator[](
8081
std::nullopt);
8182
}
8283

84+
bool ConfigNodeWithPreset::IsMap() const noexcept { return m_node.IsMap(); }
85+
8386
std::ostream& operator<<(std::ostream& out, const ConfigNodeWithPreset& node) {
8487
return out << node.get_node();
8588
}
@@ -114,7 +117,45 @@ const std::optional<ConfigNode> ConfigNodeWithPreset::get_presets_node()
114117

115118
ConfigNodeWithPreset load_file_with_presets(std::filesystem::path path) {
116119
ConfigNode node = load_file(path);
117-
return ConfigNodeWithPreset(node, node["presets"].to_optional());
120+
if (node.IsMap()) {
121+
return ConfigNodeWithPreset(node, node["presets"].to_optional());
122+
}
123+
return ConfigNodeWithPreset(node, std::nullopt);
124+
}
125+
126+
ConfigNodeWithPreset::Iterator::Iterator(ConfigNode::Iterator a_it,
127+
const ConfigNodeWithPreset& a_parent)
128+
: m_iterator(a_it), m_parent(a_parent) {}
129+
130+
ConfigNodeWithPreset::Iterator& ConfigNodeWithPreset::Iterator::operator++() {
131+
++m_iterator;
132+
return *this;
133+
}
134+
135+
ConfigNodeWithPreset::Iterator ConfigNodeWithPreset::Iterator::operator++(int) {
136+
Iterator iterator_copy(*this);
137+
++(*this);
138+
return iterator_copy;
139+
}
140+
141+
bool ConfigNodeWithPreset::Iterator::operator==(const Iterator& rhs) const {
142+
return m_iterator == rhs.m_iterator;
143+
}
144+
145+
bool ConfigNodeWithPreset::Iterator::operator!=(const Iterator& rhs) const {
146+
return m_iterator != rhs.m_iterator;
147+
}
148+
149+
ConfigNodeWithPreset ConfigNodeWithPreset::Iterator::operator*() const {
150+
return ConfigNodeWithPreset(*m_iterator, m_parent.get_presets_node());
151+
}
152+
153+
ConfigNodeWithPreset::Iterator ConfigNodeWithPreset::begin() const {
154+
return Iterator(m_node.begin(), *this);
155+
}
156+
157+
ConfigNodeWithPreset::Iterator ConfigNodeWithPreset::end() const {
158+
return Iterator(m_node.end(), *this);
118159
}
119160

120161
} // namespace sim

source/parser/config_reader/config_node_with_preset_.hpp

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ class ConfigNodeWithPreset {
1616

1717
ConfigNodeWithPresetExpected operator[](std::string_view key) const;
1818

19+
[[nodiscard]] bool IsMap() const noexcept;
20+
1921
friend std::ostream& operator<<(std::ostream& out,
2022
const ConfigNodeWithPreset& node);
2123

@@ -44,6 +46,29 @@ class ConfigNodeWithPreset {
4446

4547
std::runtime_error create_parsing_error(std::string_view error) const;
4648

49+
class Iterator {
50+
public:
51+
Iterator(ConfigNode::Iterator a_it,
52+
const ConfigNodeWithPreset& a_parent);
53+
54+
Iterator& operator++();
55+
56+
Iterator operator++(int);
57+
58+
bool operator==(const Iterator& rhs) const;
59+
60+
bool operator!=(const Iterator& rhs) const;
61+
62+
ConfigNodeWithPreset operator*() const;
63+
64+
private:
65+
ConfigNode::Iterator m_iterator;
66+
const ConfigNodeWithPreset& m_parent;
67+
};
68+
69+
Iterator begin() const;
70+
Iterator end() const;
71+
4772
private:
4873
// m_node contains information about config node and probably preset name
4974
// m_preset - preset node which that is used to supplement fields of m_node
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
_type: int
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
node:
2+
field1:
3+
_type: size_t
4+
field2:
5+
_type: int
6+
field3:
7+
_type: double
8+
field4:
9+
_type: bool
10+
field5:
11+
_type: string
12+
field6:
13+
_type: regex
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
_type: int
2+
_doc: Custom type
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node:
2+
field1:
3+
_type: /custom_type.schema
4+
field2:
5+
_type: int

0 commit comments

Comments
 (0)