|
2 | 2 | // Copyright 2026 Davide Faconti |
3 | 3 | // SPDX-License-Identifier: Apache-2.0 |
4 | 4 |
|
| 5 | +#include <charconv> |
| 6 | +#include <cstdint> |
5 | 7 | #include <cstring> |
6 | 8 | #include <functional> |
7 | 9 | #include <initializer_list> |
@@ -1284,4 +1286,217 @@ class ColorMapRegistryView { |
1284 | 1286 | PJ_colormap_registry_t registry_{}; |
1285 | 1287 | }; |
1286 | 1288 |
|
| 1289 | +// --------------------------------------------------------------------------- |
| 1290 | +// SettingsView — typed C++ view over PJ_settings_store_t |
| 1291 | +// --------------------------------------------------------------------------- |
| 1292 | + |
| 1293 | +/// A read result from SettingsView, modeled on Qt's QVariant: it holds the |
| 1294 | +/// raw stored string (or nothing, if the key was absent) and converts on |
| 1295 | +/// demand with a caller-supplied default. Scalars are stored as strings by |
| 1296 | +/// SettingsView::setValue and parsed back here. |
| 1297 | +class SettingsValue { |
| 1298 | + public: |
| 1299 | + SettingsValue() = default; |
| 1300 | + explicit SettingsValue(std::optional<std::string> raw) : raw_(std::move(raw)) {} |
| 1301 | + |
| 1302 | + /// True when the key was absent (no value stored). |
| 1303 | + [[nodiscard]] bool isNull() const noexcept { |
| 1304 | + return !raw_.has_value(); |
| 1305 | + } |
| 1306 | + |
| 1307 | + [[nodiscard]] std::string toString(std::string_view def = {}) const { |
| 1308 | + return raw_.has_value() ? *raw_ : std::string(def); |
| 1309 | + } |
| 1310 | + |
| 1311 | + [[nodiscard]] std::int64_t toInt(std::int64_t def = 0) const { |
| 1312 | + return parse<std::int64_t>(def); |
| 1313 | + } |
| 1314 | + |
| 1315 | + [[nodiscard]] double toDouble(double def = 0.0) const { |
| 1316 | + return parse<double>(def); |
| 1317 | + } |
| 1318 | + |
| 1319 | + /// "true"/"1"/"on" → true; "false"/"0"/"off" → false; otherwise @p def. |
| 1320 | + [[nodiscard]] bool toBool(bool def = false) const { |
| 1321 | + if (!raw_.has_value()) { |
| 1322 | + return def; |
| 1323 | + } |
| 1324 | + const std::string& s = *raw_; |
| 1325 | + if (s == "true" || s == "1" || s == "on") { |
| 1326 | + return true; |
| 1327 | + } |
| 1328 | + if (s == "false" || s == "0" || s == "off") { |
| 1329 | + return false; |
| 1330 | + } |
| 1331 | + return def; |
| 1332 | + } |
| 1333 | + |
| 1334 | + private: |
| 1335 | + template <typename T> |
| 1336 | + [[nodiscard]] T parse(T def) const { |
| 1337 | + if (!raw_.has_value()) { |
| 1338 | + return def; |
| 1339 | + } |
| 1340 | + T out{}; |
| 1341 | + const char* begin = raw_->data(); |
| 1342 | + const char* end = begin + raw_->size(); |
| 1343 | + auto [ptr, ec] = std::from_chars(begin, end, out); |
| 1344 | + return (ec == std::errc{} && ptr == end) ? out : def; |
| 1345 | + } |
| 1346 | + |
| 1347 | + std::optional<std::string> raw_; |
| 1348 | +}; |
| 1349 | + |
| 1350 | +/// C++ wrapper around PJ_settings_store_t — an optional, QSettings-like |
| 1351 | +/// key/value store the host may expose to plugins (service "pj.settings.v1"). |
| 1352 | +/// Empty-constructible; `valid()` tells whether the host bound a store. |
| 1353 | +/// Scalars are stored as strings; reads return an Expected so a backend fault |
| 1354 | +/// is visible rather than silently masked as a missing key: |
| 1355 | +/// if (auto v = settings.value("count")) { int n = v->toInt(42); } |
| 1356 | +/// An unbound store or an absent key is a successful Expected holding a null |
| 1357 | +/// value/empty list/false — only a host backend fault yields `!has_value()`. |
| 1358 | +/// All calls are main-thread, mirroring QSettings usage. |
| 1359 | +class SettingsView { |
| 1360 | + public: |
| 1361 | + SettingsView() = default; |
| 1362 | + explicit SettingsView(PJ_settings_store_t store) : store_(store) {} |
| 1363 | + |
| 1364 | + [[nodiscard]] bool valid() const noexcept { |
| 1365 | + return store_.vtable != nullptr && store_.ctx != nullptr; |
| 1366 | + } |
| 1367 | + |
| 1368 | + // --- writes (QSettings setValue style; scalars serialized to string) --- |
| 1369 | + |
| 1370 | + [[nodiscard]] Status setValue(std::string_view key, std::string_view value) const { |
| 1371 | + if (!valid() || store_.vtable->set_string == nullptr) { |
| 1372 | + return unexpected("settings store is not bound"); |
| 1373 | + } |
| 1374 | + PJ_error_t err{}; |
| 1375 | + if (!store_.vtable->set_string(store_.ctx, toAbiString(key), toAbiString(value), &err)) { |
| 1376 | + return unexpected(errorToString(err)); |
| 1377 | + } |
| 1378 | + return okStatus(); |
| 1379 | + } |
| 1380 | + |
| 1381 | + /// const char* overload so a string literal binds to the string setter |
| 1382 | + /// rather than the bool one. |
| 1383 | + [[nodiscard]] Status setValue(std::string_view key, const char* value) const { |
| 1384 | + return setValue(key, std::string_view(value == nullptr ? "" : value)); |
| 1385 | + } |
| 1386 | + |
| 1387 | + [[nodiscard]] Status setValue(std::string_view key, std::int64_t value) const { |
| 1388 | + const std::string s = std::to_string(value); |
| 1389 | + return setValue(key, std::string_view(s)); |
| 1390 | + } |
| 1391 | + |
| 1392 | + [[nodiscard]] Status setValue(std::string_view key, int value) const { |
| 1393 | + return setValue(key, static_cast<std::int64_t>(value)); |
| 1394 | + } |
| 1395 | + |
| 1396 | + [[nodiscard]] Status setValue(std::string_view key, double value) const { |
| 1397 | + char buf[40]; |
| 1398 | + auto [ptr, ec] = std::to_chars(buf, buf + sizeof(buf), value); |
| 1399 | + if (ec != std::errc{}) { |
| 1400 | + return unexpected("settings: failed to format double value"); |
| 1401 | + } |
| 1402 | + return setValue(key, std::string_view(buf, static_cast<std::size_t>(ptr - buf))); |
| 1403 | + } |
| 1404 | + |
| 1405 | + [[nodiscard]] Status setValue(std::string_view key, bool value) const { |
| 1406 | + return setValue(key, std::string_view(value ? "true" : "false")); |
| 1407 | + } |
| 1408 | + |
| 1409 | + [[nodiscard]] Status setValue(std::string_view key, const std::vector<std::string>& values) const { |
| 1410 | + if (!valid() || store_.vtable->set_string_list == nullptr) { |
| 1411 | + return unexpected("settings store is not bound"); |
| 1412 | + } |
| 1413 | + std::vector<PJ_string_view_t> raw; |
| 1414 | + raw.reserve(values.size()); |
| 1415 | + for (const auto& v : values) { |
| 1416 | + raw.push_back(toAbiString(v)); |
| 1417 | + } |
| 1418 | + PJ_error_t err{}; |
| 1419 | + if (!store_.vtable->set_string_list(store_.ctx, toAbiString(key), raw.data(), raw.size(), &err)) { |
| 1420 | + return unexpected(errorToString(err)); |
| 1421 | + } |
| 1422 | + return okStatus(); |
| 1423 | + } |
| 1424 | + |
| 1425 | + // --- reads --- |
| 1426 | + |
| 1427 | + /// Read a scalar as a QVariant-like SettingsValue. A failed Expected means |
| 1428 | + /// the host backend faulted; success holds a null value (isNull()) when the |
| 1429 | + /// store is unbound (optional service) or the key is absent. So `!has_value()` |
| 1430 | + /// is exactly a real host error, not a missing key. |
| 1431 | + [[nodiscard]] Expected<SettingsValue> value(std::string_view key) const { |
| 1432 | + if (!valid() || store_.vtable->get_string == nullptr) { |
| 1433 | + return SettingsValue{}; |
| 1434 | + } |
| 1435 | + PJ_string_view_t out{}; |
| 1436 | + bool found = false; |
| 1437 | + PJ_error_t err{}; |
| 1438 | + if (!store_.vtable->get_string(store_.ctx, toAbiString(key), &out, &found, &err)) { |
| 1439 | + return unexpected(errorToString(err)); |
| 1440 | + } |
| 1441 | + if (!found) { |
| 1442 | + return SettingsValue{}; |
| 1443 | + } |
| 1444 | + // Copy out of the host's scratch buffer before it can be reused. |
| 1445 | + return SettingsValue{std::string(toStringView(out))}; |
| 1446 | + } |
| 1447 | + |
| 1448 | + /// Read a string list. A failed Expected means the host backend faulted; |
| 1449 | + /// success holds an empty vector when the store is unbound or the key is |
| 1450 | + /// absent. |
| 1451 | + [[nodiscard]] Expected<std::vector<std::string>> valueStringList(std::string_view key) const { |
| 1452 | + std::vector<std::string> result; |
| 1453 | + if (!valid() || store_.vtable->get_string_list == nullptr) { |
| 1454 | + return result; |
| 1455 | + } |
| 1456 | + const PJ_string_view_t* items = nullptr; |
| 1457 | + std::size_t count = 0; |
| 1458 | + bool found = false; |
| 1459 | + PJ_error_t err{}; |
| 1460 | + if (!store_.vtable->get_string_list(store_.ctx, toAbiString(key), &items, &count, &found, &err)) { |
| 1461 | + return unexpected(errorToString(err)); |
| 1462 | + } |
| 1463 | + if (!found) { |
| 1464 | + return result; |
| 1465 | + } |
| 1466 | + result.reserve(count); |
| 1467 | + for (std::size_t i = 0; i < count; ++i) { |
| 1468 | + result.emplace_back(toStringView(items[i])); |
| 1469 | + } |
| 1470 | + return result; |
| 1471 | + } |
| 1472 | + |
| 1473 | + /// Report whether a key exists. A failed Expected means the host backend |
| 1474 | + /// faulted; success holds false when the store is unbound. |
| 1475 | + [[nodiscard]] Expected<bool> contains(std::string_view key) const { |
| 1476 | + if (!valid() || store_.vtable->contains == nullptr) { |
| 1477 | + return false; |
| 1478 | + } |
| 1479 | + bool present = false; |
| 1480 | + PJ_error_t err{}; |
| 1481 | + if (!store_.vtable->contains(store_.ctx, toAbiString(key), &present, &err)) { |
| 1482 | + return unexpected(errorToString(err)); |
| 1483 | + } |
| 1484 | + return present; |
| 1485 | + } |
| 1486 | + |
| 1487 | + [[nodiscard]] Status remove(std::string_view key) const { |
| 1488 | + if (!valid() || store_.vtable->remove_key == nullptr) { |
| 1489 | + return unexpected("settings store is not bound"); |
| 1490 | + } |
| 1491 | + PJ_error_t err{}; |
| 1492 | + if (!store_.vtable->remove_key(store_.ctx, toAbiString(key), &err)) { |
| 1493 | + return unexpected(errorToString(err)); |
| 1494 | + } |
| 1495 | + return okStatus(); |
| 1496 | + } |
| 1497 | + |
| 1498 | + private: |
| 1499 | + PJ_settings_store_t store_{}; |
| 1500 | +}; |
| 1501 | + |
1287 | 1502 | } // namespace PJ::sdk |
0 commit comments