Repository navigation
Expand file tree
/
Copy pathParameterList.cpp
More file actions
369 lines (349 loc) · 14.3 KB
/
Copy pathParameterList.cpp
File metadata and controls
369 lines (349 loc) · 14.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
#include "ParameterList.h"
#include "Exceptions.h"
#include "Helper.h"
#include <QThread>
#include <cmath>
#include <limits>
#include <utility>
QByteArray Parameter::toString(ParameterType type)
{
switch (type)
{
case ParameterType::INT: return "INT";
case ParameterType::DOUBLE: return "DOUBLE";
case ParameterType::BOOL: return "BOOL";
case ParameterType::STRING: return "STRING";
case ParameterType::STRINGLIST: return "STRINGLIST";
}
THROW(ProgrammingException, "Unhandled parameter type: " + QString::number(static_cast<int>(type)));
}
QByteArray Parameter::toString(ConstraintType type)
{
switch (type)
{
case ConstraintType::MIN: return "MIN";
case ConstraintType::MAX: return "MAX";
case ConstraintType::ALLOWED_VALUES: return "ALLOWED_VALUES";
case ConstraintType::NON_EMPTY: return "NON_EMPTY";
case ConstraintType::DISPLAY_DIGITS: return "DISPLAY_DIGITS";
}
THROW(ProgrammingException, "Unhandled constraint type: " + QString::number(static_cast<int>(type)));
}
Parameter::Parameter(QByteArray name, QString description, ParameterType type, QHash<ConstraintType, QVariant> constraints)
: name_(name)
, description_(description)
, type_(type)
, constraints_(constraints)
{
QString parameter_name = QString::fromUtf8(name_);
if (type_ == ParameterType::INT || type_ == ParameterType::DOUBLE)
{
for(auto it=constraints.cbegin(); it!=constraints.cend(); ++it)
{
if (it.key() == ConstraintType::DISPLAY_DIGITS)
{
bool ok = false;
double digits = it.value().toDouble(&ok);
if (type_ != ParameterType::DOUBLE || !ok || it.value().isNull() || it.value().userType() == QMetaType::Bool
|| !std::isfinite(digits) || std::floor(digits) != digits || digits < 0 || digits > std::numeric_limits<double>::max_digits10)
{
THROW(ArgumentException, "DISPLAY_DIGITS requires a DOUBLE parameter and an integer between 0 and " + QString::number(std::numeric_limits<double>::max_digits10) + " for parameter '" + name_ + "'.");
}
continue;
}
if (it.key()==ConstraintType::MIN)
{
QString value_as_str = it.value().toString();
if (!Helper::isNumeric(value_as_str)) THROW(ArgumentException, "Int/double parameter '"+name_+"' has non-numeric 'MIN' constraint '" + value_as_str + "'!");
}
else if (it.key()==ConstraintType::MAX)
{
QString value_as_str = it.value().toString();
if (!Helper::isNumeric(value_as_str)) THROW(ArgumentException, "Int/double parameter '"+name_+"' has non-numeric 'MAX' constraint '" + value_as_str + "'!");
}
else THROW(ArgumentException, "Int/double parameter '"+name_+"' cannot have constraint '" + Parameter::toString(it.key()) + "'!");
bool ok = false;
double limit = it.value().toDouble(&ok);
if (!ok || !std::isfinite(limit))
{
THROW(ArgumentException, "Constraint '" + Parameter::toString(it.key()) + "' for parameter '" + name_ + "' is not a representable finite number: '" + it.value().toString() + "'.");
}
}
if (constraints_.contains(ConstraintType::MIN) && constraints_.contains(ConstraintType::MAX)
&& constraints_.value(ConstraintType::MIN).toDouble() > constraints_.value(ConstraintType::MAX).toDouble())
{
THROW(ArgumentException, "MIN constraint exceeds MAX constraint for parameter '" + name_ + "'.");
}
}
else if (type_ == ParameterType::STRING || type_ == ParameterType::STRINGLIST)
{
for(auto it=constraints.cbegin(); it!=constraints.cend(); ++it)
{
if (it.key()==ConstraintType::ALLOWED_VALUES)
{
if (it.value().userType() != QMetaType::QString && it.value().userType() != QMetaType::QByteArray)
{
THROW(ArgumentException, "String/string-list parameter '"+name_+"' has non-string ALLOWED_VALUES constraint!");
}
}
else if (it.key() != ConstraintType::NON_EMPTY) THROW(ArgumentException, "String/string-list parameter '"+name_+"' cannot have constraint '" + Parameter::toString(it.key()) + "'!");
}
}
else
{
for(auto it=constraints.cbegin(); it!=constraints.cend(); ++it)
{
THROW(ArgumentException, "String parameter '"+name_+"' cannot have constraint '" + Parameter::toString(it.key()) + "'!");
}
}
}
ParameterList::ParameterList(QByteArray name, const QList<Parameter>& parameters, const QHash<QByteArray, QVariant>& defaults)
: name_(name)
, entries_(parameters)
{
indices_.reserve(entries_.size());
for (int i = 0; i < entries_.size(); ++i)
{
const QByteArray& name = std::as_const(entries_).at(i).name();
if (indices_.contains(name)) THROW(ArgumentException, "Parameter '" + name + "' added twice to parameter list '" + name_ + "'.");
indices_.insert(name, i);
}
//Reject undefined parameter names in defaults
for (auto it = defaults.cbegin(); it != defaults.cend(); ++it)
{
parameterIndex(it.key());
}
//add default for each parameter
for (int i = 0; i < entries_.size(); ++i)
{
const Parameter& parameter = std::as_const(entries_).at(i);
auto it = defaults.constFind(parameter.name());
if (it == defaults.cend()) THROW(ArgumentException, "Missing default for parameter '" + parameter.name() + "' in parameter list '" + name_ + "'.");
setValue(parameter.name(), parameter.type(), it.value(), false);
}
}
void ParameterList::reset(const QList<Parameter>& parameters, const QHash<QByteArray, QVariant>& defaults)
{
ParameterList validated(name_, parameters, defaults);
QList<QMetaProperty> properties;
if (linked_object_) properties = validated.linkProperties(linked_object_);
entries_.swap(validated.entries_);
indices_.swap(validated.indices_);
linked_properties_.swap(properties);
syncLinkedValues();
emit constraintsChanged();
emit parameterChanged();
}
void ParameterList::checkLinkThread(QObject* target) const
{
if (QThread::currentThread() != thread() || target->thread() != thread())
{
THROW(ArgumentException, "Parameter list '" + name_ + "' and its linked object must belong to the current thread.");
}
}
QList<QMetaProperty> ParameterList::linkProperties(QObject* target) const
{
checkLinkThread(target);
QList<QMetaProperty> properties;
QHash<QByteArray, int> names;
const QMetaObject* meta = target->metaObject();
for (const Parameter& parameter : entries_)
{
const QByteArray property_name = parameter.name().toLower().replace(' ', '_').replace('-', '_');
const int property_index = meta->indexOfProperty(property_name.constData());
QMetaProperty property;
if (property_index >= QObject::staticMetaObject.propertyCount())
{
if (names.contains(property_name))
{
THROW(ArgumentException, "Multiple parameters map to linked property '" + property_name + "' in parameter list '" + name_ + "'.");
}
property = meta->property(property_index);
if (!property.isWritable() || property.userType() != parameter.value().userType())
{
THROW(ArgumentException, "Linked property '" + property_name + "' must be writable and have the exact type of parameter '" + parameter.name() + "'.");
}
names.insert(property_name, property_index);
}
properties.append(property);
}
//Catch misspelled fields and schema changes that would otherwise leave stale values.
for (int i = QObject::staticMetaObject.propertyCount(); i < meta->propertyCount(); ++i)
{
const QByteArray property_name = meta->property(i).name();
if (!names.contains(property_name))
{
THROW(ArgumentException, "Linked property '" + property_name + "' has no matching parameter in list '" + name_ + "'.");
}
}
if (names.isEmpty() && !entries_.isEmpty())
{
THROW(ArgumentException, "Linked object has no parameter properties for list '" + name_ + "'.");
}
return properties;
}
void ParameterList::link(QObject* fast_access_struct)
{
QList<QMetaProperty> properties;
if (fast_access_struct) properties = linkProperties(fast_access_struct);
linked_object_ = fast_access_struct;
linked_properties_.swap(properties);
syncLinkedValues();
}
void ParameterList::syncLinkedValues()
{
if (!linked_object_) return;
checkLinkThread(linked_object_);
for (int i = 0; i < entries_.size(); ++i)
{
const QMetaProperty& property = linked_properties_.at(i);
if (property.isValid() && !property.write(linked_object_, entries_.at(i).value()))
{
THROW(ProgrammingException, "Could not write linked property '" + QByteArray(property.name()) + "' for parameter list '" + name_ + "'.");
}
}
}
int ParameterList::parameterIndex(const QByteArray& name) const
{
auto it = indices_.constFind(name);
if (it == indices_.cend()) THROW(ArgumentException, "Unknown parameter '" + name + "' in parameter list '" + name_ + "'.");
return it.value();
}
int ParameterList::checkedIndex(const QByteArray& name, ParameterType type) const
{
int index = parameterIndex(name);
const Parameter& parameter = entries_.at(index);
if (parameter.type() != type) THROW(ArgumentException, "Requested type '"+Parameter::toString(type)+"' for parameter '" + name + "' with type '"+Parameter::toString(parameter.type())+"' in parameter list '"+name_ +"'.");
return index;
}
void ParameterList::setValue(const QByteArray& name, ParameterType type, const QVariant& value, bool emit_changed_signal)
{
const int index = checkedIndex(name, type);
const QVariant normalized_value = validatedValue(std::as_const(entries_).at(index), value);
if (entries_.at(index).value() == normalized_value) return;
if (linked_object_) checkLinkThread(linked_object_);
entries_[index].value_ = normalized_value;
if (linked_object_)
{
const QMetaProperty& property = linked_properties_.at(index);
if (property.isValid() && !property.write(linked_object_, normalized_value))
{
THROW(ProgrammingException, "Could not write linked property '" + QByteArray(property.name()) + "' for parameter list '" + name_ + "'.");
}
}
if (emit_changed_signal) emit parameterChanged();
}
void ParameterList::overrideConstraint(const QByteArray& name, ConstraintType type, const QVariant& value)
{
const int index = parameterIndex(name);
const Parameter& current = std::as_const(entries_).at(index);
auto constraints = current.constraints();
constraints.insert(type, value);
Parameter updated(current.name(), current.description(), current.type(), constraints);
updated.value_ = validatedValue(updated, current.value());
if (current.constraints() == updated.constraints()) return;
entries_[index] = updated;
emit constraintsChanged();
}
QVariant ParameterList::validatedValue(const Parameter& parameter, const QVariant& value) const
{
const QByteArray& name = parameter.name();
const ParameterType type = parameter.type();
const auto& constraints = parameter.constraints();
if (!value.isValid())
{
THROW(ArgumentException, "Invalid value for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
QVariant normalized_value;
if (type == ParameterType::INT || type == ParameterType::DOUBLE)
{
bool ok = false;
double numeric_value = value.toDouble(&ok);
if (!ok || value.isNull())
{
THROW(ArgumentException, "Non-numeric value for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
if (!std::isfinite(numeric_value))
{
THROW(ArgumentException, "Non-finite value for parameter '" + name + "' in parameter list '"+name_+"'.");
}
if (type == ParameterType::INT)
{
if (std::floor(numeric_value) != numeric_value || numeric_value < std::numeric_limits<int>::min() || numeric_value > std::numeric_limits<int>::max())
{
THROW(ArgumentException, "Value is not a representable integer for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
normalized_value = static_cast<int>(numeric_value);
}
else normalized_value = numeric_value;
for (auto it = constraints.cbegin(); it != constraints.cend(); ++it)
{
double limit = it.value().toDouble();
if (it.key() == ConstraintType::MIN && numeric_value < limit)
{
THROW(ArgumentException, "Value '"+value.toString()+"' of parameter '"+name+"' smaller than MIN constraint ("+QString::number(limit)+") for parameter '" + name + "' in parameter list '"+name_+"'.");
}
if (it.key() == ConstraintType::MAX && numeric_value > limit)
{
THROW(ArgumentException, "Value '"+value.toString()+"' of parameter '"+name+"' larger than MAX constraint ("+QString::number(limit)+") for parameter '" + name + "' in parameter list '"+name_+"'.");
}
}
}
else if (type == ParameterType::BOOL)
{
if (value.userType() != QMetaType::Bool || value.isNull())
{
THROW(ArgumentException, "Non-boolean value for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
normalized_value = value.toBool();
}
else if (type == ParameterType::STRING)
{
if (value.userType() != QMetaType::QString && value.userType() != QMetaType::QByteArray)
{
THROW(ArgumentException, "Non-string value for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
normalized_value = value.toByteArray();
if (constraints.contains(ConstraintType::NON_EMPTY) && normalized_value.toByteArray().isEmpty())
{
THROW(ArgumentException, "Empty string is not allowed for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
if (constraints.contains(ConstraintType::ALLOWED_VALUES))
{
QByteArrayList allowed_values = constraints.value(ConstraintType::ALLOWED_VALUES).toByteArray().split('\t');
if (!allowed_values.contains(normalized_value.toByteArray()))
{
THROW(ArgumentException, "Value '"+value.toByteArray()+"' is not allowed for parameter '" + name + "' in parameter list '"+name_+"'.");
}
}
}
else if (type == ParameterType::STRINGLIST)
{
if (value.userType() != qMetaTypeId<QByteArrayList>())
{
THROW(ArgumentException, "Non-string-list value for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
const QByteArrayList entries = value.value<QByteArrayList>();
if (constraints.contains(ConstraintType::NON_EMPTY) && entries.contains(QByteArray()))
{
THROW(ArgumentException, "Empty string is not allowed in parameter '" + name + "' in parameter list '" + name_ + "'.");
}
if (constraints.contains(ConstraintType::ALLOWED_VALUES))
{
const QByteArrayList allowed_values = constraints.value(ConstraintType::ALLOWED_VALUES).toByteArray().split('\t');
for (const QByteArray& entry : entries)
{
if (!allowed_values.contains(entry))
{
THROW(ArgumentException, "Value '" + entry + "' is not allowed for parameter '" + name + "' in parameter list '" + name_ + "'.");
}
}
}
normalized_value = QVariant::fromValue(entries);
}
else
{
THROW(ProgrammingException, "Unhandled parameter type: " + QString::number(static_cast<int>(type)));
}
return normalized_value;
}