libpromeki 1.0.0-alpha
PROfessional MEdia toolKIt
 
Loading...
Searching...
No Matches
variantdatabase.h
Go to the documentation of this file.
1
8#pragma once
9
10
11#include <promeki/config.h>
12#if PROMEKI_ENABLE_CORE
13#include <optional>
14#include <type_traits>
15#include <promeki/optional.h>
16#include <promeki/namespace.h>
17#include <promeki/sharedptr.h>
19#include <promeki/variant.h>
20#include <promeki/variantspec.h>
21#include <promeki/error.h>
22#include <promeki/map.h>
23#include <promeki/list.h>
24#include <promeki/json.h>
25#include <promeki/stringlist.h>
26#include <promeki/textstream.h>
27#include <promeki/datastream.h>
29#include <promeki/logger.h>
30#include <promeki/util.h>
31
32PROMEKI_NAMESPACE_BEGIN
33
41enum class SpecValidation {
42 None,
43 Warn,
44 Strict
45};
46
122template <CompiledString Name> class VariantDatabase {
123 public:
125 using ID = typename StringRegistry<Name>::Item;
126
128 using SpecMap = ::promeki::Map<ID, VariantSpec>;
129
130 // ============================================================
131 // Static spec registry
132 // ============================================================
133
156 static ID declareID(const String &name, const VariantSpec &spec) {
157 // Use the strict registration path so a hash collision
158 // between two well-known names aborts at static-init
159 // time instead of silently diverging from
160 // `ID::literal(name)`.
161 ID id = ID::fromId(StringRegistry<Name>::instance().findOrCreateStrict(name));
162 specRegistry().insert(id.id(), spec);
163 return id;
164 }
165
171 static const VariantSpec *spec(ID id) { return specRegistry().find(id.id()); }
172
189 static const VariantSpec *specFor(const String &name) {
190 ID id = ID::find(name);
191 if (!id.isValid()) return nullptr;
192 return spec(id);
193 }
194
199 static Map<uint64_t, VariantSpec> registeredSpecs() { return specRegistry().all(); }
200
211 static VariantDatabase fromSpecs(const SpecMap &specs) {
212 VariantDatabase db;
213 Data *d = db._d.modify();
214 for (auto it = specs.cbegin(); it != specs.cend(); ++it) {
215 const Variant &def = it->second.defaultValue();
216 if (def.isValid()) d->data.insert(it->first.id(), def);
217 }
218 return db;
219 }
220
243 static int writeSpecMapHelp(TextStream &stream, const SpecMap &specs,
244 const StringList &skipKeys = StringList()) {
245 // Collect the visible set first so the column
246 // width pass doesn't count skipped keys.
247 StringList names;
248 for (auto it = specs.cbegin(); it != specs.cend(); ++it) {
249 const String &n = it->first.name();
250 if (skipKeys.contains(n)) continue;
251 names.pushToBack(n);
252 }
253 names = names.sort();
254 if (names.isEmpty()) return 0;
255
256 // Three-column layout: name | details | description.
257 // We cache the per-row details string so the width
258 // pass and the emit pass don't rebuild it twice.
259 List<String> details;
260 details.resize(names.size());
261 int nameWidth = 0;
262 int detailsWidth = 0;
263 for (size_t i = 0; i < names.size(); ++i) {
264 ID id(names[i]);
265 auto it = specs.find(id);
266 if (it == specs.end()) continue;
267 int nw = static_cast<int>(names[i].size());
268 if (nw > nameWidth) nameWidth = nw;
269 details[i] = it->second.detailsString();
270 int dw = static_cast<int>(details[i].size());
271 if (dw > detailsWidth) detailsWidth = dw;
272 }
273
274 // Fixed structure: " <name> <details> <desc>".
275 // Everything up to the description is a known
276 // size; the description's real width varies per
277 // row, so we track the widest actual line as we
278 // go and return that so the caller can draw a
279 // border matching the block.
280 const int prefixWidth = 2 + nameWidth + 2 + detailsWidth;
281 int maxLineWidth = 0;
282 for (size_t i = 0; i < names.size(); ++i) {
283 ID id(names[i]);
284 auto it = specs.find(id);
285 if (it == specs.end()) continue;
286
287 String nameCol = names[i];
288 while (static_cast<int>(nameCol.size()) < nameWidth) {
289 nameCol += ' ';
290 }
291 String detailsCol = details[i];
292 while (static_cast<int>(detailsCol.size()) < detailsWidth) {
293 detailsCol += ' ';
294 }
295 stream << " " << nameCol << " " << detailsCol;
296 const String &desc = it->second.description();
297 int lineWidth = prefixWidth;
298 if (!desc.isEmpty()) {
299 stream << " " << desc;
300 lineWidth += 2 + static_cast<int>(desc.size());
301 }
302 stream << endl;
303 if (lineWidth > maxLineWidth) maxLineWidth = lineWidth;
304 }
305 return maxLineWidth;
306 }
307
308 // ============================================================
309 // Construction
310 // ============================================================
311
313 VariantDatabase() = default;
314
353 Error setFromJson(ID id, const Variant &value) {
354 const VariantSpec *sp = spec(id);
355 if (sp != nullptr) {
356 // Recursive coercion: top-level Strings get
357 // parsed via VariantSpec::parseString, and
358 // nested VariantList / VariantMap entries get
359 // walked with their element / value sub-spec
360 // so a {"color": "red"} JSON object whose
361 // declared spec is TypeVariantMap with a
362 // TypeColor valueSpec lands as
363 // VariantMap{"color": Color::Red} rather than
364 // VariantMap{"color": String("red")}.
365 Error ce;
366 Variant coerced = sp->coerce(value, &ce);
367 if (ce.isError()) {
368 // The leaf parse / recursion failed
369 // — surface that error rather than
370 // storing a value that would fail
371 // spec validation in Strict mode
372 // anyway.
373 return ce;
374 }
375 Error err;
376 set(id, std::move(coerced), &err);
377 return err;
378 }
379 Error err;
380 set(id, value, &err);
381 return err;
382 }
383
400 static VariantDatabase fromJson(const JsonObject &json) {
401 VariantDatabase db;
402 json.forEach([&db](const String &key, const Variant &val) { db.setFromJson(ID(key), val); });
403 return db;
404 }
405
406 // ============================================================
407 // Validation mode
408 // ============================================================
409
419 void setValidation(SpecValidation mode) { _d.modify()->validation = mode; }
420
425 SpecValidation validation() const { return _d->validation; }
426
427 // ============================================================
428 // Value management
429 // ============================================================
430
451 bool set(ID id, const Variant &value, Error *err = nullptr) {
452 if (!validateOnSet(id, value, err)) return false;
453 _d.modify()->data.insert(id.id(), value);
454 return true;
455 }
456
464 bool set(ID id, Variant &&value, Error *err = nullptr) {
465 if (!validateOnSet(id, value, err)) return false;
466 _d.modify()->data.insert(id.id(), std::move(value));
467 return true;
468 }
469
483 bool setIfMissing(ID id, const Variant &value) {
484 if (_d->data.contains(id.id())) return false;
485 _d.modify()->data.insert(id.id(), value);
486 return true;
487 }
488
496 bool setIfMissing(ID id, Variant &&value) {
497 if (_d->data.contains(id.id())) return false;
498 _d.modify()->data.insert(id.id(), std::move(value));
499 return true;
500 }
501
508 Variant get(ID id, const Variant &defaultValue = Variant()) const {
509 auto it = _d->data.find(id.id());
510 if (it == _d->data.end()) return defaultValue;
511 return it->second;
512 }
513
527 template <typename T> T getAs(ID id, const T &defaultValue = T{}, Error *err = nullptr) const {
528 auto it = _d->data.find(id.id());
529 if (it == _d->data.end()) {
530 if (err) *err = Error::IdNotFound;
531 return defaultValue;
532 }
533 Error e;
534 T result = it->second.template get<T>(&e);
535 if (e.isError()) {
536 if (err) *err = Error::ConversionFailed;
537 return defaultValue;
538 }
539 if (err) *err = Error::Ok;
540 return result;
541 }
542
608 template <typename Resolver>
609 String format(const String &tmpl, Resolver &&resolver, Error *err = nullptr) const {
610 if (err != nullptr) *err = Error::Ok;
611 String out;
612 out.reserve(tmpl.byteCount());
613 const char *src = tmpl.cstr();
614 const size_t len = tmpl.byteCount();
615 bool sawUnresolved = false;
616 size_t i = 0;
617 while (i < len) {
618 char c = src[i];
619 if (c == '{') {
620 if (i + 1 < len && src[i + 1] == '{') {
621 out.pushBack('{');
622 i += 2;
623 continue;
624 }
625 size_t end = i + 1;
626 while (end < len && src[end] != '}') ++end;
627 if (end >= len) {
628 out += String(src + i, len - i);
629 break;
630 }
631 const char *bodyData = src + i + 1;
632 const size_t bodyLen = end - (i + 1);
633 size_t colon = bodyLen;
634 for (size_t p = 0; p < bodyLen; ++p) {
635 if (bodyData[p] == ':') {
636 colon = p;
637 break;
638 }
639 }
640 const size_t keyLen = colon;
641 const size_t specOff = (colon == bodyLen) ? bodyLen : colon + 1;
642 String keyName(bodyData, keyLen);
643 String specStr(bodyData + specOff, bodyLen - specOff);
644 // Split on the first '.' (or '[') to allow
645 // nested keys like "Foo.bar" or "Foo[0]"
646 // when the value at "Foo" is a VariantMap
647 // / VariantList. The leading ID
648 // component still must be a registered key.
649 size_t sep = keyName.byteCount();
650 const char *kn = keyName.cstr();
651 for (size_t p = 0; p < keyName.byteCount(); ++p) {
652 if (kn[p] == '.' || kn[p] == '[') {
653 sep = p;
654 break;
655 }
656 }
657 String headKey = (sep == keyName.byteCount()) ? keyName
658 : String(kn, sep);
659 String tailPath;
660 if (sep < keyName.byteCount()) {
661 // Trim a leading '.' but keep '['.
662 size_t start = (kn[sep] == '.') ? sep + 1 : sep;
663 tailPath = String(kn + start, keyName.byteCount() - start);
664 }
665 ID id = ID::find(headKey);
666 auto it = id.isValid() ? _d->data.find(id.id()) : _d->data.end();
667 if (it != _d->data.end()) {
668 Variant target = it->second;
669 if (!tailPath.isEmpty()) {
670 Error pe;
671 target = promekiResolveVariantPath(target, tailPath, &pe);
672 if (pe.isError() || !target.isValid()) {
673 sawUnresolved = true;
674 out += '?';
675 out += String(bodyData, keyLen);
676 out += '?';
677 i = end + 1;
678 continue;
679 }
680 }
681 out += target.format(specStr);
682 } else {
683 Optional<String> resolved;
684 if constexpr (!std::is_same_v<std::decay_t<Resolver>, std::nullptr_t>) {
685 resolved = resolver(keyName, specStr);
686 }
687 if (resolved.hasValue()) {
688 out += *resolved;
689 } else {
690 sawUnresolved = true;
691 out += '?';
692 out += String(bodyData, keyLen);
693 out += '?';
694 }
695 }
696 i = end + 1;
697 } else if (c == '}') {
698 if (i + 1 < len && src[i + 1] == '}') {
699 out.pushBack('}');
700 i += 2;
701 } else {
702 out.pushBack('}');
703 ++i;
704 }
705 } else {
706 out.pushBack(c);
707 ++i;
708 }
709 }
710 if (sawUnresolved && err != nullptr) *err = Error::IdNotFound;
711 return out;
712 }
713
725 String format(const String &tmpl, Error *err = nullptr) const { return format(tmpl, nullptr, err); }
726
732 bool contains(ID id) const { return _d->data.contains(id.id()); }
733
739 bool remove(ID id) {
740 if (!_d->data.contains(id.id())) return false;
741 return _d.modify()->data.remove(id.id());
742 }
743
748 size_t size() const { return _d->data.size(); }
749
754 bool isEmpty() const { return _d->data.isEmpty(); }
755
759 void clear() {
760 if (_d->data.isEmpty()) return;
761 _d.modify()->data.clear();
762 }
763
768 List<ID> ids() const {
769 List<ID> ret;
770 for (auto it = _d->data.cbegin(); it != _d->data.cend(); ++it) {
771 ret.pushToBack(ID::fromId(it->first));
772 }
773 return ret;
774 }
775
797 StringList unknownKeys(const SpecMap &extraSpecs = SpecMap()) const {
798 StringList out;
799 for (auto it = _d->data.cbegin(); it != _d->data.cend(); ++it) {
800 ID id = ID::fromId(it->first);
801 if (extraSpecs.find(id) != extraSpecs.end()) continue;
802 if (spec(id) != nullptr) continue;
803 out.pushToBack(id.name());
804 }
805 // List<String>::sort() returns a List<String>, not a
806 // StringList — assign-through-base lets us keep
807 // StringList's type identity on the return value.
808 out = out.sort();
809 return out;
810 }
811
818 template <typename Func> void forEach(Func &&func) const {
819 for (auto it = _d->data.cbegin(); it != _d->data.cend(); ++it) {
820 func(ID::fromId(it->first), it->second);
821 }
822 }
823
832 void merge(const VariantDatabase &other) {
833 other.forEach([this](ID id, const Variant &val) { set(id, val); });
834 }
835
844 VariantDatabase extract(const List<ID> &idList) const {
845 VariantDatabase result;
846 Data *r = result._d.modify();
847 for (size_t i = 0; i < idList.size(); ++i) {
848 auto it = _d->data.find(idList[i].id());
849 if (it != _d->data.end()) {
850 r->data.insert(it->first, it->second);
851 }
852 }
853 return result;
854 }
855
856 // ============================================================
857 // Comparison
858 // ============================================================
859
875 bool operator==(const VariantDatabase &other) const {
876 if (_d == other._d) return true;
877 return _d->data == other._d->data;
878 }
879
881 bool operator!=(const VariantDatabase &other) const { return !(*this == other); }
882
883 // ============================================================
884 // JSON serialization
885 // ============================================================
886
896 JsonObject toJson() const {
897 JsonObject json;
898 for (auto it = _d->data.cbegin(); it != _d->data.cend(); ++it) {
899 String name = StringRegistry<Name>::instance().name(it->first);
900 json.setFromVariant(name, it->second);
901 }
902 return json;
903 }
904
905 // ============================================================
906 // DataStream serialization
907 // ============================================================
908
917 void writeTo(DataStream &stream) const {
918 stream << static_cast<uint32_t>(_d->data.size());
919 for (auto it = _d->data.cbegin(); it != _d->data.cend(); ++it) {
920 String name = StringRegistry<Name>::instance().name(it->first);
921 stream << name << it->second;
922 }
923 }
924
934 void readFrom(DataStream &stream) {
935 _d.modify()->data.clear();
936 uint32_t count = 0;
937 stream >> count;
938 for (uint32_t i = 0; i < count && stream.status() == DataStream::Ok; ++i) {
939 String name;
940 Variant value;
941 stream >> name >> value;
942 if (stream.status() == DataStream::Ok) {
943 set(ID(name), std::move(value));
944 }
945 }
946 }
947
948 // ============================================================
949 // TextStream serialization
950 // ============================================================
951
963 void writeTo(TextStream &stream) const {
964 for (auto it = _d->data.cbegin(); it != _d->data.cend(); ++it) {
965 String name = StringRegistry<Name>::instance().name(it->first);
966 stream << name << " = " << it->second << endl;
967 }
968 }
969
970 private:
981 struct Data {
982 PROMEKI_SHARED_FINAL(Data)
983 Map<uint64_t, Variant> data;
984 SpecValidation validation = SpecValidation::Strict;
985 };
986 SharedPtr<Data> _d = SharedPtr<Data>::create();
987
995 struct SpecRegistry {
996 static SpecRegistry &instance() {
997 static SpecRegistry reg;
998 return reg;
999 }
1000
1001 void insert(uint64_t id, const VariantSpec &spec) {
1002 ReadWriteLock::WriteLocker lock(_lock);
1003 _specs.insert(id, spec);
1004 }
1005
1006 const VariantSpec *find(uint64_t id) const {
1007 ReadWriteLock::ReadLocker lock(_lock);
1008 auto it = _specs.find(id);
1009 if (it == _specs.end()) return nullptr;
1010 return &it->second;
1011 }
1012
1013 Map<uint64_t, VariantSpec> all() const {
1014 ReadWriteLock::ReadLocker lock(_lock);
1015 return _specs;
1016 }
1017
1018 private:
1019 SpecRegistry() = default;
1020 mutable ReadWriteLock _lock;
1021 Map<uint64_t, VariantSpec> _specs;
1022 };
1023
1024 static SpecRegistry &specRegistry() { return SpecRegistry::instance(); }
1025
1042 bool validateOnSet(ID id, const Variant &value, Error *err = nullptr) {
1043 if (err) *err = Error::Ok;
1044 const SpecValidation mode = _d->validation;
1045 if (mode == SpecValidation::None) return true;
1046 const VariantSpec *s = specRegistry().find(id.id());
1047 if (!s) return true;
1048 Error verr;
1049 if (!s->validate(value, &verr)) {
1050 if (mode == SpecValidation::Warn) {
1051 promekiWarn("VariantDatabase: value for '%s' fails spec (%s)", id.name().cstr(),
1052 verr.name().cstr());
1053 return true;
1054 }
1055 if (err) *err = verr;
1056 return false; // Strict: reject
1057 }
1058 return true;
1059 }
1060};
1061
1068template <CompiledString Name> DataStream &operator<<(DataStream &stream, const VariantDatabase<Name> &db) {
1069 db.writeTo(stream);
1070 return stream;
1071}
1072
1079template <CompiledString Name> DataStream &operator>>(DataStream &stream, VariantDatabase<Name> &db) {
1080 db.readFrom(stream);
1081 return stream;
1082}
1083
1090template <CompiledString Name> TextStream &operator<<(TextStream &stream, const VariantDatabase<Name> &db) {
1091 db.writeTo(stream);
1092 return stream;
1093}
1094
1095PROMEKI_NAMESPACE_END
1096
1132#define PROMEKI_DECLARE_ID(Name, ...) \
1133 static constexpr ID Name = ID::literal(#Name); \
1134 [[maybe_unused]] static inline const ID PROMEKI_CONCAT(_promeki_spec_reg_, Name) = declareID(#Name, __VA_ARGS__)
1135
1136#endif // PROMEKI_ENABLE_CORE