MQTTSuite
Loading...
Searching...
No Matches
StoragePlan.cpp
Go to the documentation of this file.
1/*
2 * MQTTSuite - A lightweight MQTT Integration System
3 * Copyright (C) Volker Christian <me@vchrist.at>
4 * 2022, 2023, 2024, 2025, 2026
5 *
6 * This program is free software: you can redistribute it and/or modify it
7 * under the terms of the GNU General Public License as published by the Free
8 * Software Foundation, either version 3 of the License, or (at your option)
9 * any later version.
10 */
11
12#include "StoragePlan.h"
13
14#ifndef DOXYGEN_SHOULD_SKIP_THIS
15
16#include "nlohmann/json-schema.hpp"
17
18#include <exception>
19#include <fstream>
20#include <iterator>
21#include <map>
22#include <nlohmann/detail/iterators/iter_impl.hpp>
23#include <nlohmann/detail/json_pointer.hpp>
24#include <nlohmann/json.hpp>
25#include <sstream>
26#include <stdexcept>
27#include <vector>
28
29#endif
30
31namespace mqtt::mqttstore::lib {
32
33#include "projection-schema.json.h" // IWYU pragma: keep
34
35 namespace {
36
37 [[nodiscard]] std::string jsonSummary(const nlohmann::json& json) {
38 std::string summary = json.dump();
39 constexpr std::size_t maxSummaryLength = 160;
40 if (summary.size() > maxSummaryLength) {
41 summary.resize(maxSummaryLength);
42 summary += "...";
43 }
44
45 return summary;
46 }
47
49 public:
50 void error(const nlohmann::json::json_pointer& pointer, const nlohmann::json& instance, const std::string& message) override {
51 nlohmann::json_schema::basic_error_handler::error(pointer, instance, message);
52
53 std::ostringstream error;
54 error << " - " << pointer << ": " << message << " (value: " << jsonSummary(instance) << ")";
55 errors.push_back(error.str());
56 }
57
58 [[nodiscard]] std::string message() const {
59 std::string result;
60 for (const std::string& error : errors) {
61 result += (result.empty() ? "" : "\n") + error;
62 }
63
64 return result;
65 }
66
67 private:
69 };
70
71 void validateProjectionConfiguration(const nlohmann::json& json) {
72 try {
73 const nlohmann::json projectionJsonSchema = nlohmann::json::parse(projectionJsonSchemaString);
74 const nlohmann::json_schema::json_validator validator(
75 projectionJsonSchema, nullptr, nlohmann::json_schema::default_string_format_check);
77
78 static_cast<void>(validator.validate(json, errorHandler));
79 if (errorHandler) {
80 throw std::runtime_error("Validating mqttstore projection file failed:\n" + errorHandler.message());
81 }
82 } catch (const std::runtime_error&) {
83 throw;
84 } catch (const std::exception& exception) {
85 throw std::runtime_error("Validating mqttstore projection file failed: " + std::string(exception.what()));
86 }
87 }
88
89 [[nodiscard]] std::vector<std::string> splitTopic(const std::string& topic) {
90 std::vector<std::string> result;
91 std::stringstream topicStream(topic);
92
93 for (std::string level; std::getline(topicStream, level, '/');) {
94 result.push_back(level);
95 }
96
97 if (!topic.empty() && topic.back() == '/') {
98 result.emplace_back();
99 }
100
101 return result;
102 }
103
104 } // namespace
105
106 StoragePlan StoragePlan::fromFile(const std::string& fileName) {
107 if (fileName.empty()) {
108 return {};
109 }
110
111 std::ifstream planFile(fileName);
112 if (!planFile.is_open()) {
113 throw std::runtime_error("Cannot open mqttstore projection file '" + fileName + "'");
114 }
115
116 nlohmann::json planJson;
117 try {
118 planJson = nlohmann::json::parse(std::string(std::istreambuf_iterator<char>(planFile), std::istreambuf_iterator<char>()));
119 } catch (const nlohmann::json::parse_error& exception) {
120 throw std::runtime_error("Cannot parse mqttstore projection file '" + fileName + "': " + exception.what());
121 }
122
123 try {
124 return fromJson(planJson);
125 } catch (const std::exception& exception) {
126 throw std::runtime_error("Cannot load mqttstore projection file '" + fileName + "': " + exception.what());
127 }
128 }
129
132
133 StoragePlan plan;
134
135 const nlohmann::json& projectionsJson = json.contains("projections") ? json.at("projections") : json;
136 if (!projectionsJson.is_array()) {
137 throw std::runtime_error("mqttstore projection configuration must be an array or contain a projections array");
138 }
139
140 for (const nlohmann::json& projectionJson : projectionsJson) {
141 Projection projection;
142 projection.name = projectionJson.value("name", "");
143 projection.topic = projectionJson.at("topic").get<std::string>();
144 projection.table = projectionJson.at("table").get<std::string>();
145
146 const nlohmann::json& columnsJson = projectionJson.at("columns");
147 if (!columnsJson.is_object()) {
148 throw std::runtime_error("mqttstore projection columns must be an object");
149 }
150
151 for (auto columnIterator = columnsJson.begin(); columnIterator != columnsJson.end(); ++columnIterator) {
152 ColumnMapping mapping;
153 mapping.column = columnIterator.key();
154
155 if (columnIterator.value().is_string()) {
156 mapping.jsonPointer = columnIterator.value().get<std::string>();
157 } else {
158 const nlohmann::json& mappingJson = columnIterator.value();
159 mapping.jsonPointer = mappingJson.value("json_pointer", "");
160 mapping.required = mappingJson.value("required", false);
161
162 if (mappingJson.contains("topic_level")) {
163 mapping.topicLevel = mappingJson.at("topic_level").get<std::size_t>();
164 }
165 if (mappingJson.contains("literal")) {
166 mapping.literal = mappingJson.at("literal").get<std::string>();
167 }
168 }
169
170 projection.columns.push_back(mapping);
171 }
172
173 plan.projections.push_back(projection);
174 }
175
176 return plan;
177 }
178
180 return projections;
181 }
182
183 std::vector<const StoragePlan::Projection*> StoragePlan::match(const std::string& topic) const {
184 std::vector<const Projection*> matches;
185
186 for (const Projection& projection : projections) {
187 if (topicMatches(projection.topic, topic)) {
188 matches.push_back(&projection);
189 }
190 }
191
192 return matches;
193 }
194
195 bool StoragePlan::topicMatches(const std::string& filter, const std::string& topic) {
196 const std::vector<std::string> filterLevels = splitTopic(filter);
197 const std::vector<std::string> topicLevels = splitTopic(topic);
198
199 for (std::size_t level = 0; level < filterLevels.size(); ++level) {
200 if (filterLevels[level] == "#") {
201 return level + 1 == filterLevels.size();
202 }
203
204 if (level >= topicLevels.size()) {
205 return false;
206 }
207
208 if (filterLevels[level] != "+" && filterLevels[level] != topicLevels[level]) {
209 return false;
210 }
211 }
212
213 return filterLevels.size() == topicLevels.size();
214 }
215
216} // namespace mqtt::mqttstore::lib
const std::vector< Projection > & getProjections() const
std::vector< const Projection * > match(const std::string &topic) const
static StoragePlan fromJson(const nlohmann::json &json)
static StoragePlan fromFile(const std::string &fileName)
static bool topicMatches(const std::string &filter, const std::string &topic)
std::vector< Projection > projections
Definition StoragePlan.h:53
void error(const nlohmann::json::json_pointer &pointer, const nlohmann::json &instance, const std::string &message) override
void error(const json::json_pointer &, const json &, const std::string &) override
void validateProjectionConfiguration(const nlohmann::json &json)
std::string jsonSummary(const nlohmann::json &json)
std::vector< std::string > splitTopic(const std::string &topic)
void default_string_format_check(const std::string &format, const std::string &value)