Smithy Qt 0.1.0-pre
Internal development documentation
Loading...
Searching...
No Matches
model.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2013-2025 Paul Colby <git@colby.id.au>
2// SPDX-License-Identifier: LGPL-3.0-or-later
3
4/*!
5 * \file
6 * Defines the Model and ModelPrivate classes.
7 */
8
9#include <qtsmithy/model.h>
10#include "model_p.h"
11
12#include <QJsonArray>
13
15
16/*!
17 * \class Model
18 *
19 * The Model class provides a Qt representation of a Smithy semantic model.
20 *
21 * \see https://awslabs.github.io/smithy/2.0/spec/model.html#semantic-model
22 */
23
24/*!
25 * Constructs a new, empty Smithy model.
26 */
28{
29 Q_D(Model);
30 d->error = Error::NoData;
31 /// \todo Load the Smithy prelude here?
32}
33
34Model::Model(Model &&other) : d_ptr(new ModelPrivate(this))
35{
36 Q_D(Model);
37 d->error = std::move(other.d_ptr->error);
38 d->mergedMetadata = std::move(other.d_ptr->mergedMetadata);
39 d->mergedShapes = std::move(other.d_ptr->mergedShapes);
40 d->allMetadata = std::move(other.d_ptr->allMetadata);
41 d->allShapes = std::move(other.d_ptr->allShapes);
42}
43
44Model::Model(const Model &other) : d_ptr(new ModelPrivate(this))
45{
46 Q_D(Model);
47 d->error = other.d_ptr->error;
48 d->mergedMetadata = other.d_ptr->mergedMetadata;
49 d->mergedShapes = other.d_ptr->mergedShapes;
50 d->allMetadata = other.d_ptr->allMetadata;
51 d->allShapes = other.d_ptr->allShapes;
52}
53
54Model& Model::operator=(const Model &model)
55{
56 Q_D(Model);
57 d->error = model.d_ptr->error;
58 d->mergedMetadata = model.d_ptr->mergedMetadata;
59 d->mergedShapes = model.d_ptr->mergedShapes;
60 d->allMetadata = model.d_ptr->allMetadata;
61 d->allShapes = model.d_ptr->allShapes;
62 return *this;
63}
64
65Model& Model::operator=(const Model &&model)
66{
67 Q_D(Model);
68 d->error = std::move(model.d_ptr->error);
69 d->mergedMetadata = std::move(model.d_ptr->mergedMetadata);
70 d->mergedShapes = std::move(model.d_ptr->mergedShapes);
71 d->allMetadata = std::move(model.d_ptr->allMetadata);
72 d->allShapes = std::move(model.d_ptr->allShapes);
73 return *this;
74}
75
76/*!
77 * Destroys this Model object.
78 */
80{
81 delete d_ptr;
82}
83
84void Model::clear()
85{
86 Q_D(Model);
87 d->error = Error::NoData;
88 d->mergedMetadata = QJsonObject{};
89 d->mergedShapes.clear();
90 d->allMetadata.clear();
91 d->allShapes.clear();
92}
93
94/*!
95 * Add the logical content of the JSON AST model file given by \a ast into this semantic model.
96 *
97 * A Smithy semantic model is split into one or more model files. Use this method to add all model
98 * files that comprise this semantic model.
99 *
100 * \see https://awslabs.github.io/smithy/2.0/spec/model.html
101 */
102bool Model::insert(const QJsonObject &ast)
103{
104 Q_D(Model);
105 if (d->error == Error::NoData) {
106 d->error = Error::NoError;
107 }
108
109 // Clear any previously-merged data; we'll need to re-merge later.
110 d->mergedMetadata = QJsonObject{};
111 d->mergedShapes.clear();
112
113 // Fetch the Smithy version.
114 const QVersionNumber version = d->smithyVersion(ast);
115 if (version.majorVersion() > 2) {
116 qCWarning(d->lc).noquote() << tr("Unknown Smithy version %1").arg(version.toString());
117 }
118
119 // Warn on any unrecognised top-level Smithy AST properties.
120 // https://awslabs.github.io/smithy/2.0/spec/json-ast.html#top-level-properties
121 const QStringList keys = ast.keys();
122 for (const QString &key: keys) {
123 const QStringList knownKeys{
124 QStringLiteral("smithy"),QStringLiteral("metadata"), QStringLiteral("shapes") };
125 if (!knownKeys.contains(key)) {
126 qCWarning(d->lc).noquote() << tr("Ignoring unknown Smithy AST property %1").arg(key);
127 }
128 }
129
130 // Process the (optional) metadata.
131 const QJsonValue metadata = ast.value(QStringLiteral("metadata"));
132 if (metadata != QJsonValue::Undefined) {
133 if (!metadata.isObject()) {
134 qCCritical(d->lc).noquote() << tr("Smithy AST metadata is not an object");
135 qDebug().noquote() << metadata;
136 if (d->error == Error::NoError) d->error = Error::InvalidMetadata;
137 return false;
138 }
139 const QJsonObject object = metadata.toObject();
140 qCDebug(d->lc).noquote() << tr("Processing %n metadata entry(s)", nullptr, object.length());
141 for (auto iter = object.constBegin(); iter != object.constEnd(); ++iter) {
142 d->allMetadata.insert(iter.key(), iter.value());
143 }
144 }
145
146 // Process the (optional) shapes.
147 const QJsonValue shapes = ast.value(QStringLiteral("shapes"));
148 if (shapes != QJsonValue::Undefined) {
149 if (!shapes.isObject()) {
150 qCCritical(d->lc).noquote() << tr("Smithy AST shapes is not an object");
151 if (d->error == Error::NoError) d->error = Error::InvalidShapes;
152 return false;
153 }
154 const QJsonObject object = shapes.toObject();
155 qCDebug(d->lc).noquote() << tr("Processing %n shape(s)", nullptr, object.length());
156 for (auto iter = object.constBegin(); iter != object.constEnd(); ++iter) {
157 const ShapeId shapeId(iter.key());
158 if (!shapeId.isValid()) {
159 qCCritical(d->lc).noquote() << tr("Failed to parse shape ID %1").arg(iter.key());
160 if (d->error == Error::NoError) d->error = Error::InvalidShapeId;
161 return false;
162 }
163 if (!shapeId.hasNameSpace()) {
164 qCCritical(d->lc).noquote() << tr("Shape ID %1 has no namespace").arg(iter.key());
165 if (d->error == Error::NoError) d->error = Error::InvalidShapeId;
166 return false;
167 }
168 if (!iter.value().isObject()) {
169 qCCritical(d->lc).noquote() << tr("Shape %1 is not a JSON object").arg(iter.key());
170 if (d->error == Error::NoError) d->error = Error::InvalidShape;
171 return false;
172 }
173 qCDebug(d->lc).noquote() << tr("Processing shape %1").arg(shapeId.toString());
174 const Shape shape{iter.value().toObject(), shapeId};
175 if (!shape.isValid()) {
176 qCCritical(d->lc).noquote() << tr("Failed to process shape %1").arg(iter.key());
177 if (d->error == Error::NoError) d->error = Error::InvalidShape;
178 return false;
179 }
180 d->allShapes.insert(iter.key(), shape);
181 }
182 }
183 return true;
184}
185
187{
188 Q_D(Model);
189 if (d->error != Error::NoError) {
190 qCWarning(d->lc).noquote() << tr("Model::finish() called with Model errors present");
191 return false;
192 }
193
194 d->mergedMetadata = ModelPrivate::mergeMetadata(d->allMetadata);
195 if (d->allMetadata.isEmpty() != d->mergedMetadata.isEmpty()) {
196 if (d->error == Error::NoError) d->error = Error::ConflictingMetadata;
197 }
198
199 /// \todo resolve shape conflicts.
200 /// \todo resolve trait conflicts; include 'apply' statements.
201 Q_UNIMPLEMENTED();
202 return isValid();
203}
204
205Model::Error Model::error() const
206{
207 Q_D(const Model);
208 return d->error;
209}
210
211bool Model::isValid() const
212{
213 Q_D(const Model);
214 return (d->error == Error::NoError);
215}
216
217QJsonObject Model::metadata() const
218{
219 Q_D(const Model);
220 return d->mergedMetadata;
221}
222
223Shape Model::shape(const ShapeId &shapeId) const
224{
225 Q_D(const Model);
226 /// \todo this should be d->mergedShapes, but using allShapes while finish() is incomplete.
227 return d->allShapes.value(shapeId);
228}
229
230QHash<ShapeId, Shape> Model::shapes(const Shape::Type &type) const
231{
232 Q_D(const Model);
233 if (type == Shape::Type::Undefined) {
234 return d->mergedShapes;
235 }
236 QHash<ShapeId, Shape> shapes;
237 /// \todo this should be d->mergedShapes, but using allShapes while finish() is incomplete.
238 for (auto iter = d->allShapes.constBegin(); iter != d->allShapes.constEnd(); ++iter) {
239 if (iter.value().type() == type) {
240 shapes.insert(iter.key(), iter.value());
241 }
242 }
243 return shapes;
244}
245
246/*!
247 * \cond internal
248 * \class ModelPrivate
249 *
250 * The ModelPrivate class provides private implementation for Model.
251 */
252
253/*!
254 * \internal
255 * Constructs a new ModelPrivate object with public implementation \a q.
256 */
258{
259
260}
261
262// https://awslabs.github.io/smithy/2.0/spec/model.html#merging-metadata
263QJsonObject ModelPrivate::mergeMetadata(const QMultiHash<QString, QJsonValue> &metadata)
264{
265 qCDebug(lc).noquote() << tr("Merging %n metedata entry(s)", nullptr, metadata.size());
266 QJsonObject merged;
267 const QStringList keys = metadata.keys();
268 for (const QString &key: keys) {
269 // If values are arrays, concatenate them.
270 const auto values = metadata.values(key);
271 if (values.first().isArray()) {
272 QJsonArray concatenatedArray;
273 for (const QJsonValue &value: values) {
274 if (!value.isArray()) {
275 qCCritical(lc).noquote() << tr("Metadata %1 has conflicting types").arg(key);
276 return QJsonObject{};
277 }
278 const QJsonArray thisArray = value.toArray();
279 for (const QJsonValue &item: thisArray) {
280 concatenatedArray.append(item);
281 }
282 }
283 merged.insert(key, concatenatedArray);
284 continue;
285 }
286
287 // Otherwise all values must be identical.
288 for (const QJsonValue &value: values) {
289 qDebug() << values.first() << value;
290 if (value != values.first()) {
291 qCDebug(lc).noquote() << tr("Metatadata %1 has conflicting values").arg(key);
292 return QJsonObject{};
293 }
294 }
295 merged.insert(key, values.first());
296 }
297 qCDebug(lc).noquote() << tr("Merged %n metedata entry(s) to %1", nullptr, metadata.size())
298 .arg(merged.size());
299 return merged;
300}
301
302QVersionNumber ModelPrivate::smithyVersion(const QJsonObject &ast)
303{
304 const QString versionString = ast.value(QLatin1String("smithy")).toString();
305 qCDebug(lc).noquote() << tr("Smithy version string:") << versionString;
306 #if (QT_VERSION < QT_VERSION_CHECK(6, 4, 0))
307 int suffixIndex = -1; // Initial value is ignored.
308 #else
309 qsizetype suffixIndex = -1; // Initial value is ignored.
310 #endif
311 const QVersionNumber versionNumber = QVersionNumber::fromString(versionString, &suffixIndex);
312 qCDebug(lc).noquote() << tr("Smithy version number:") << versionNumber;
313 if (versionNumber.isNull()) {
314 qCWarning(lc).noquote() << tr("Failed to parse Smithy version \"%1\"").arg(versionString);
315 } else if (suffixIndex < versionString.length()) {
316 qCWarning(lc).noquote() << tr("Ignoring Smithy version suffix \"%1\"")
317 .arg(versionString.mid(suffixIndex));
318 }
319 return versionNumber;
320}
321
322/// \endcond
323
The ModelPrivate class provides private implementation for Model.
Definition model_p.h:22
ModelPrivate(Model *const q)
Definition model.cpp:257
Model * q_ptr
Internal q-pointer.
Definition model_p.h:32
The Model class provides a Qt representation of a Smithy semantic model.
Definition model.h:26
QHash< ShapeId, Shape > shapes(const Shape::Type &type=Shape::Type::Undefined) const
Definition model.cpp:230
Shape shape(const ShapeId &shapeId) const
Definition model.cpp:223
bool insert(const QJsonObject &ast)
Add the logical content of the JSON AST model file given by ast into this semantic model.
Definition model.cpp:102
bool finish()
Definition model.cpp:186
~Model()
Destroys this Model object.
Definition model.cpp:79
Model()
Constructs a new, empty Smithy model.
Definition model.cpp:27
ModelPrivate * d_ptr
Definition model.h:60
The ShapeId class provides a Qt representation of a Smithy Shape ID.
Definition shapeid.h:29
bool isValid() const
Returns true if this object represents a valid, non-empty Smithy Shape ID.
Definition shapeid.cpp:281
QString toString() const
Returns this object as an absolute Smithy Shape ID if this object has a namespace,...
Definition shapeid.cpp:214
bool hasNameSpace() const
Returns true if this Shape ID has a non-empty namespace, otherwise false otherwise.
Definition shapeid.cpp:225
The Shape class provides a Qt representation of a Smithy semantic shape.
Definition shape.h:25
Type
Definition shape.h:37
@ Undefined
The shape is undefined, usually the result of an error condition.
Definition shape.h:38
Declares the Model class.
Declares the ModelPrivate class.
#define QTSMITHY_BEGIN_NAMESPACE
Macro for starting the QtSmithy library's top-most namespace (if one is defined).
#define QTSMITHY_END_NAMESPACE
Macro for ending the QtSmithy library's top-most namespace (if one is defined).