LCOV - code coverage report
Current view: top level - src/lib - model.cpp (source / functions) Coverage Total Hit
Project: Smithy Qt Lines: 0.0 % 232 0
Version: 0.1.0-pre Functions: 0.0 % 39 0

            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
        

Generated by: LCOV version 2.2-1