Generalize FieldManager, automatic value validation

This commit is contained in:
GriffinR
2026-02-21 22:44:43 -05:00
parent 3571b4ea4b
commit 29fe37f79e
14 changed files with 389 additions and 213 deletions

View File

@@ -47,6 +47,8 @@
Appropriately implementing 'toString'/'fromString' has the added benefit that your type
can automatically be used as a JSON key if it for example appears as the key in a QMap.
// TODO: Document clamp
*/
@@ -69,6 +71,19 @@ struct DefaultConverter {
// Default to identity
static QString toString(const T& value) {return value;}
static T fromString(const QString& string, QStringList* = nullptr) {return string;}
static T clamp(const T& value, const T& min, const T& max, QStringList* errors = nullptr) {
Q_ASSERT(min <= max);
if (value < min) {
if (errors) errors->append("Value too low");
return min;
}
if (value > max) {
if (errors) errors->append("Value too high");
return max;
}
return value;
}
};
template <typename T, typename Enable = void>
@@ -189,6 +204,17 @@ struct Converter<BaseGame::Version> : DefaultStringConverter<BaseGame::Version>
}
};
template <typename T>
struct Converter<std::optional<T>> : DefaultConverter<std::optional<T>> {
static QJsonValue toJson(const std::optional<T>& optional) {
return optional.has_value() ? Converter<T>::toJson(optional.value()) : QJsonValue();
}
static std::optional<T> fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
if (json.isNull()) return {};
return Converter<T>::fromJson(json, errors);
}
};
template <typename T>
struct Converter<QList<T>> : DefaultConverter<QList<T>> {
static QJsonValue toJson(const QList<T>& list) {
@@ -277,6 +303,14 @@ struct Converter<QSize> : DefaultConverter<QSize> {
size.setHeight(obj.value("height").toInt());
return size;
}
static QSize clamp(const QSize& value, const QSize& min, const QSize& max, const QStringList* = nullptr) {
Q_ASSERT(min.width() <= max.width());
Q_ASSERT(min.height() <= max.height());
QSize size = value;
if (value.width() < min.width() || value.height() < min.height()) size = value.expandedTo(min);
if (value.width() > max.width() || value.height() > max.height()) size = value.boundedTo(max);
return size;
}
};
template <>
@@ -298,6 +332,18 @@ struct Converter<QMargins> : DefaultConverter<QMargins> {
margins.setRight(obj.value("right").toInt());
return margins;
}
static QMargins clamp(const QMargins& value, const QMargins& min, const QMargins& max, const QStringList* = nullptr) {
Q_ASSERT(min.left() <= max.left());
Q_ASSERT(min.right() <= max.right());
Q_ASSERT(min.top() <= max.top());
Q_ASSERT(min.bottom() <= max.bottom());
QMargins margins = value;
margins.setLeft( std::clamp(value.left(), min.left(), max.left()));
margins.setRight( std::clamp(value.right(), min.right(), max.right()));
margins.setTop( std::clamp(value.top(), min.top(), max.top()));
margins.setBottom(std::clamp(value.bottom(), min.bottom(), max.bottom()));
return margins;
}
};
template <>

171
include/core/fieldmanager.h Normal file
View File

@@ -0,0 +1,171 @@
#include "converter.h"
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// A FieldInterface provides a simple interface for converting a field to/from JSON.
// It's constructed with a pointer to some data, and has two functions:
// - 'get' returns the pointed-to data, converted to JSON
// - 'set' assigns the pointed-to data to a given QJsonValue, with appropriate conversion.
// Returns any errors that occur during conversion/assignment.
//
// A FieldInterface is normally constructed using the 'makeFieldInterface' function:
// Example:
// int someField = 0;
// FieldInterface* fi = makeFieldInterface(&someField);
// fi->set(QJsonValue("5")); // someField is now 5
//
// There are additional implementations of 'makeFieldInterface' that let you specify what valid values are.
// Example:
// int someField = 0;
// int min = 1, max = 4;
// FieldInterface* fi = makeFieldInterface(&someField, min, max);
// fi->set(QJsonValue("5")); // someField is now 4 (defaults to closest bound), error messages returned
// or
// QString someField = "";
// QList<QString> options = {"hello","hi there"};
// FieldInterface* fi = makeFieldInterface(&someField, options);
// fi->set(QJsonValue("5")); // someField is now "hello" (defaults to first element), error messages returned
// Base class lets us use the interface without any type information.
class FieldInterface {
public:
FieldInterface(){};
virtual ~FieldInterface() {};
virtual QJsonValue get() const = 0;
virtual QStringList set(const QJsonValue& json) const = 0;
};
template <typename T>
class BasicFieldInterface : public FieldInterface {
public:
BasicFieldInterface(T* field) : m_field(field) {
Q_ASSERT(m_field);
};
virtual ~BasicFieldInterface() {};
virtual QJsonValue get() const override {return Converter<T>::toJson(*m_field);}
virtual QStringList set(const QJsonValue& json) const override {
QStringList errors;
auto value = Converter<T>::fromJson(json, &errors);
if (errors.isEmpty()) *m_field = value; // Don't bother changing the value if conversion failed
return errors;
}
protected:
T* m_field;
};
template <typename T>
static FieldInterface* makeFieldInterface(T* field) {
return new BasicFieldInterface<T>(field);
}
// Create a regular FieldInterface, but override 'set' to use the given min/max.
template <typename T>
static FieldInterface* makeFieldInterface(T* field, const T& min, const T& max) {
class BoundedFieldInterface : public BasicFieldInterface<T> {
public:
BoundedFieldInterface(T* field, const T& min, const T& max)
: BasicFieldInterface<T>(field), m_min(min), m_max(max) {};
virtual ~BoundedFieldInterface() {};
virtual QStringList set(const QJsonValue& json) const override {
QStringList errors;
auto value = Converter<T>::fromJson(json, &errors);
if (errors.isEmpty()) { // Don't bother changing the value if conversion failed
value = Converter<T>::clamp(value, m_min, m_max, &errors);
*this->m_field = value;
}
return errors;
}
private:
const T m_min;
const T m_max;
};
return new BoundedFieldInterface(field, min, max);
}
// Create a regular FieldInterface, but override 'set' to use the given 'acceptableValues'.
template <typename T>
static FieldInterface* makeFieldInterface(T* field, const QList<T>& acceptableValues) {
Q_ASSERT(!acceptableValues.isEmpty());
class BoundedFieldInterface : public BasicFieldInterface<T> {
public:
BoundedFieldInterface(T* field, const QList<T>& acceptableValues)
: BasicFieldInterface<T>(field),
m_acceptableValues(acceptableValues.begin(), acceptableValues.end()),
m_defaultValue(acceptableValues.first()) {};
virtual ~BoundedFieldInterface() {};
virtual QStringList set(const QJsonValue& json) const override {
QStringList errors;
auto value = Converter<T>::fromJson(json, &errors);
if (errors.isEmpty()) {
if (!m_acceptableValues.contains(value)) {
value = m_defaultValue;
errors.append("Invalid value.");
}
*this->m_field = value;
}
return errors;
}
private:
// The order of the list only matters for determining the default value,
// so save that separately and convert the list to a set for better lookup speed.
const QSet<T> m_acceptableValues;
const T m_defaultValue;
};
return new BoundedFieldInterface(field, acceptableValues);
}
/////////////////////////////////////////////////////////////////////////////////////////////////////////
// FieldManager manages a QHash mapping string keys to FieldInterfaces.
// This makes it easy to map many fields to/from JSON without explicitly serializing anything.
class FieldManager {
public:
~FieldManager(){ clear(); }
QStringList setField(const QString& key, const QJsonValue& value) const {
auto it = m_fields.find(key);
return (it != m_fields.end()) ? it.value()->set(value) : QStringList();
}
QJsonValue getField(const QString& key) const {
auto it = m_fields.find(key);
return (it != m_fields.end()) ? it.value()->get() : QJsonValue();
}
QJsonObject getFields() const {
QJsonObject obj;
for (auto it = m_fields.constBegin(); it != m_fields.constEnd(); it++) {
obj[it.key()] = it.value()->get();
}
return obj;
}
void clear() {
qDeleteAll(m_fields);
m_fields.clear();
}
bool hasField(const QString& key) const {return m_fields.contains(key);}
template <typename T>
void addField(T* field, const QString& key) {
Q_ASSERT(!m_fields.contains(key));
m_fields.insert(key, makeFieldInterface<T>(field));
}
template <typename T>
void addField(T* field, const QString& key, const T& min, const T& max) {
Q_ASSERT(!m_fields.contains(key));
m_fields.insert(key, makeFieldInterface<T>(field, min, max));
}
template <typename T>
void addField(T* field, const QString& key, const QList<T>& acceptableValues) {
Q_ASSERT(!m_fields.contains(key));
m_fields.insert(key, makeFieldInterface<T>(field, acceptableValues));
}
private:
QHash<QString, FieldInterface*> m_fields;
};

View File

@@ -3,6 +3,7 @@
#define ORDERED_SET_H
#include <QJsonArray>
#include <set>
template <typename T>
class OrderedSet : public std::set<T>

View File

@@ -18,7 +18,6 @@ namespace Util {
void setErrorStylesheet(QLineEdit *lineEdit, bool isError);
QString toStylesheetString(const QFont &font);
void show(QWidget *w);
QColorSpace toColorSpace(int colorSpaceInt);
QString mkpath(const QString& dirPath);
QString getFileHash(const QString &filepath);