Line data Source code
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 :
14 : QTSMITHY_BEGIN_NAMESPACE
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 : */
27 0 : Model::Model() : d_ptr(new ModelPrivate(this))
28 0 : {
29 0 : Q_D(Model);
30 0 : d->error = Error::NoData;
31 : /// \todo Load the Smithy prelude here?
32 0 : }
33 :
34 0 : Model::Model(Model &&other) : d_ptr(new ModelPrivate(this))
35 0 : {
36 0 : Q_D(Model);
37 0 : d->error = std::move(other.d_ptr->error);
38 0 : d->mergedMetadata = std::move(other.d_ptr->mergedMetadata);
39 0 : d->mergedShapes = std::move(other.d_ptr->mergedShapes);
40 0 : d->allMetadata = std::move(other.d_ptr->allMetadata);
41 0 : d->allShapes = std::move(other.d_ptr->allShapes);
42 0 : }
43 :
44 0 : Model::Model(const Model &other) : d_ptr(new ModelPrivate(this))
45 0 : {
46 0 : Q_D(Model);
47 0 : d->error = other.d_ptr->error;
48 0 : d->mergedMetadata = other.d_ptr->mergedMetadata;
49 0 : d->mergedShapes = other.d_ptr->mergedShapes;
50 0 : d->allMetadata = other.d_ptr->allMetadata;
51 0 : d->allShapes = other.d_ptr->allShapes;
52 0 : }
53 :
54 0 : Model& Model::operator=(const Model &model)
55 0 : {
56 0 : Q_D(Model);
57 0 : d->error = model.d_ptr->error;
58 0 : d->mergedMetadata = model.d_ptr->mergedMetadata;
59 0 : d->mergedShapes = model.d_ptr->mergedShapes;
60 0 : d->allMetadata = model.d_ptr->allMetadata;
61 0 : d->allShapes = model.d_ptr->allShapes;
62 0 : return *this;
63 0 : }
64 :
65 0 : Model& Model::operator=(const Model &&model)
66 0 : {
67 0 : Q_D(Model);
68 0 : d->error = std::move(model.d_ptr->error);
69 0 : d->mergedMetadata = std::move(model.d_ptr->mergedMetadata);
70 0 : d->mergedShapes = std::move(model.d_ptr->mergedShapes);
71 0 : d->allMetadata = std::move(model.d_ptr->allMetadata);
72 0 : d->allShapes = std::move(model.d_ptr->allShapes);
73 0 : return *this;
74 0 : }
75 :
76 : /*!
77 : * Destroys this Model object.
78 : */
79 0 : Model::~Model()
80 0 : {
81 0 : delete d_ptr;
82 0 : }
83 :
84 0 : void Model::clear()
85 0 : {
86 0 : Q_D(Model);
87 0 : d->error = Error::NoData;
88 0 : d->mergedMetadata = QJsonObject{};
89 0 : d->mergedShapes.clear();
90 0 : d->allMetadata.clear();
91 0 : d->allShapes.clear();
92 0 : }
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 : */
102 0 : bool Model::insert(const QJsonObject &ast)
103 0 : {
104 0 : Q_D(Model);
105 0 : if (d->error == Error::NoData) {
106 0 : d->error = Error::NoError;
107 0 : }
108 :
109 : // Clear any previously-merged data; we'll need to re-merge later.
110 0 : d->mergedMetadata = QJsonObject{};
111 0 : d->mergedShapes.clear();
112 :
113 : // Fetch the Smithy version.
114 0 : const QVersionNumber version = d->smithyVersion(ast);
115 0 : if (version.majorVersion() > 2) {
116 0 : qCWarning(d->lc).noquote() << tr("Unknown Smithy version %1").arg(version.toString());
117 0 : }
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 0 : const QStringList keys = ast.keys();
122 0 : for (const QString &key: keys) {
123 0 : const QStringList knownKeys{
124 0 : QStringLiteral("smithy"),QStringLiteral("metadata"), QStringLiteral("shapes") };
125 0 : if (!knownKeys.contains(key)) {
126 0 : qCWarning(d->lc).noquote() << tr("Ignoring unknown Smithy AST property %1").arg(key);
127 0 : }
128 0 : }
129 :
130 : // Process the (optional) metadata.
131 0 : const QJsonValue metadata = ast.value(QStringLiteral("metadata"));
132 0 : if (metadata != QJsonValue::Undefined) {
133 0 : if (!metadata.isObject()) {
134 0 : qCCritical(d->lc).noquote() << tr("Smithy AST metadata is not an object");
135 0 : qDebug().noquote() << metadata;
136 0 : if (d->error == Error::NoError) d->error = Error::InvalidMetadata;
137 0 : return false;
138 0 : }
139 0 : const QJsonObject object = metadata.toObject();
140 0 : qCDebug(d->lc).noquote() << tr("Processing %n metadata entry(s)", nullptr, object.length());
141 0 : for (auto iter = object.constBegin(); iter != object.constEnd(); ++iter) {
142 0 : d->allMetadata.insert(iter.key(), iter.value());
143 0 : }
144 0 : }
145 :
146 : // Process the (optional) shapes.
147 0 : const QJsonValue shapes = ast.value(QStringLiteral("shapes"));
148 0 : if (shapes != QJsonValue::Undefined) {
149 0 : if (!shapes.isObject()) {
150 0 : qCCritical(d->lc).noquote() << tr("Smithy AST shapes is not an object");
151 0 : if (d->error == Error::NoError) d->error = Error::InvalidShapes;
152 0 : return false;
153 0 : }
154 0 : const QJsonObject object = shapes.toObject();
155 0 : qCDebug(d->lc).noquote() << tr("Processing %n shape(s)", nullptr, object.length());
156 0 : for (auto iter = object.constBegin(); iter != object.constEnd(); ++iter) {
157 0 : const ShapeId shapeId(iter.key());
158 0 : if (!shapeId.isValid()) {
159 0 : qCCritical(d->lc).noquote() << tr("Failed to parse shape ID %1").arg(iter.key());
160 0 : if (d->error == Error::NoError) d->error = Error::InvalidShapeId;
161 0 : return false;
162 0 : }
163 0 : if (!shapeId.hasNameSpace()) {
164 0 : qCCritical(d->lc).noquote() << tr("Shape ID %1 has no namespace").arg(iter.key());
165 0 : if (d->error == Error::NoError) d->error = Error::InvalidShapeId;
166 0 : return false;
167 0 : }
168 0 : if (!iter.value().isObject()) {
169 0 : qCCritical(d->lc).noquote() << tr("Shape %1 is not a JSON object").arg(iter.key());
170 0 : if (d->error == Error::NoError) d->error = Error::InvalidShape;
171 0 : return false;
172 0 : }
173 0 : qCDebug(d->lc).noquote() << tr("Processing shape %1").arg(shapeId.toString());
174 0 : const Shape shape{iter.value().toObject(), shapeId};
175 0 : if (!shape.isValid()) {
176 0 : qCCritical(d->lc).noquote() << tr("Failed to process shape %1").arg(iter.key());
177 0 : if (d->error == Error::NoError) d->error = Error::InvalidShape;
178 0 : return false;
179 0 : }
180 0 : d->allShapes.insert(iter.key(), shape);
181 0 : }
182 0 : }
183 0 : return true;
184 0 : }
185 :
186 0 : bool Model::finish()
187 0 : {
188 0 : Q_D(Model);
189 0 : if (d->error != Error::NoError) {
190 0 : qCWarning(d->lc).noquote() << tr("Model::finish() called with Model errors present");
191 0 : return false;
192 0 : }
193 :
194 0 : d->mergedMetadata = ModelPrivate::mergeMetadata(d->allMetadata);
195 0 : if (d->allMetadata.isEmpty() != d->mergedMetadata.isEmpty()) {
196 0 : if (d->error == Error::NoError) d->error = Error::ConflictingMetadata;
197 0 : }
198 :
199 : /// \todo resolve shape conflicts.
200 : /// \todo resolve trait conflicts; include 'apply' statements.
201 0 : Q_UNIMPLEMENTED();
202 0 : return isValid();
203 0 : }
204 :
205 0 : Model::Error Model::error() const
206 0 : {
207 0 : Q_D(const Model);
208 0 : return d->error;
209 0 : }
210 :
211 0 : bool Model::isValid() const
212 0 : {
213 0 : Q_D(const Model);
214 0 : return (d->error == Error::NoError);
215 0 : }
216 :
217 0 : QJsonObject Model::metadata() const
218 0 : {
219 0 : Q_D(const Model);
220 0 : return d->mergedMetadata;
221 0 : }
222 :
223 0 : Shape Model::shape(const ShapeId &shapeId) const
224 0 : {
225 0 : Q_D(const Model);
226 : /// \todo this should be d->mergedShapes, but using allShapes while finish() is incomplete.
227 0 : return d->allShapes.value(shapeId);
228 0 : }
229 :
230 0 : QHash<ShapeId, Shape> Model::shapes(const Shape::Type &type) const
231 0 : {
232 0 : Q_D(const Model);
233 0 : if (type == Shape::Type::Undefined) {
234 0 : return d->mergedShapes;
235 0 : }
236 0 : QHash<ShapeId, Shape> shapes;
237 : /// \todo this should be d->mergedShapes, but using allShapes while finish() is incomplete.
238 0 : for (auto iter = d->allShapes.constBegin(); iter != d->allShapes.constEnd(); ++iter) {
239 0 : if (iter.value().type() == type) {
240 0 : shapes.insert(iter.key(), iter.value());
241 0 : }
242 0 : }
243 0 : return shapes;
244 0 : }
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 : */
257 0 : ModelPrivate::ModelPrivate(Model * const q) : q_ptr(q)
258 0 : {
259 :
260 0 : }
261 :
262 : // https://awslabs.github.io/smithy/2.0/spec/model.html#merging-metadata
263 0 : QJsonObject ModelPrivate::mergeMetadata(const QMultiHash<QString, QJsonValue> &metadata)
264 0 : {
265 0 : qCDebug(lc).noquote() << tr("Merging %n metedata entry(s)", nullptr, metadata.size());
266 0 : QJsonObject merged;
267 0 : const QStringList keys = metadata.keys();
268 0 : for (const QString &key: keys) {
269 : // If values are arrays, concatenate them.
270 0 : const auto values = metadata.values(key);
271 0 : if (values.first().isArray()) {
272 0 : QJsonArray concatenatedArray;
273 0 : for (const QJsonValue &value: values) {
274 0 : if (!value.isArray()) {
275 0 : qCCritical(lc).noquote() << tr("Metadata %1 has conflicting types").arg(key);
276 0 : return QJsonObject{};
277 0 : }
278 0 : const QJsonArray thisArray = value.toArray();
279 0 : for (const QJsonValue &item: thisArray) {
280 0 : concatenatedArray.append(item);
281 0 : }
282 0 : }
283 0 : merged.insert(key, concatenatedArray);
284 0 : continue;
285 0 : }
286 :
287 : // Otherwise all values must be identical.
288 0 : for (const QJsonValue &value: values) {
289 0 : qDebug() << values.first() << value;
290 0 : if (value != values.first()) {
291 0 : qCDebug(lc).noquote() << tr("Metatadata %1 has conflicting values").arg(key);
292 0 : return QJsonObject{};
293 0 : }
294 0 : }
295 0 : merged.insert(key, values.first());
296 0 : }
297 0 : qCDebug(lc).noquote() << tr("Merged %n metedata entry(s) to %1", nullptr, metadata.size())
298 0 : .arg(merged.size());
299 0 : return merged;
300 0 : }
301 :
302 0 : QVersionNumber ModelPrivate::smithyVersion(const QJsonObject &ast)
303 0 : {
304 0 : const QString versionString = ast.value(QLatin1String("smithy")).toString();
305 0 : qCDebug(lc).noquote() << tr("Smithy version string:") << versionString;
306 0 : #if (QT_VERSION < QT_VERSION_CHECK(6, 4, 0))
307 0 : int suffixIndex = -1; // Initial value is ignored.
308 : #else
309 0 : qsizetype suffixIndex = -1; // Initial value is ignored.
310 0 : #endif
311 0 : const QVersionNumber versionNumber = QVersionNumber::fromString(versionString, &suffixIndex);
312 0 : qCDebug(lc).noquote() << tr("Smithy version number:") << versionNumber;
313 0 : if (versionNumber.isNull()) {
314 0 : qCWarning(lc).noquote() << tr("Failed to parse Smithy version \"%1\"").arg(versionString);
315 0 : } else if (suffixIndex < versionString.length()) {
316 0 : qCWarning(lc).noquote() << tr("Ignoring Smithy version suffix \"%1\"")
317 0 : .arg(versionString.mid(suffixIndex));
318 0 : }
319 0 : return versionNumber;
320 0 : }
321 :
322 : /// \endcond
323 :
324 : QTSMITHY_END_NAMESPACE
|