LCOV - code coverage report
Current view: top level - src/app - main.cpp (source / functions) Coverage Total Hit
Project: Smithy Qt Lines: 16.9 % 160 27
Version: 0.1.0-pre Functions: 16.7 % 12 2

            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              : #include "generator.h"
       5              : #include "renderer.h"
       6              : 
       7              : #include <qtsmithy/model.h>
       8              : 
       9              : #if defined USE_CUTELEE
      10              : #include <cutelee/cutelee_version.h>
      11              : #elif defined USE_GRANTLEE
      12              : #include <grantlee/grantlee_version.h>
      13              : #elif defined USE_KTEXTTEMPLATE
      14              : #include <ktexttemplate_version.h>
      15              : #endif
      16              : 
      17              : #include <QCommandLineParser>
      18              : #include <QCoreApplication>
      19              : #include <QDebug>
      20              : #include <QDir>
      21              : #include <QDirIterator>
      22              : #include <QFileInfo>
      23              : #include <QJsonDocument>
      24              : #include <QLoggingCategory>
      25              : 
      26              : #if defined(Q_OS_UNIX)
      27              : #include <unistd.h>
      28              : #elif defined(Q_OS_WIN)
      29              : #include <Windows.h>
      30              : #endif
      31              : 
      32            0 : Q_LOGGING_CATEGORY(lc, "smithy.app", QtInfoMsg); ///< Logging category for main application.
      33              : 
      34              : inline bool haveConsole()
      35              : {
      36              :     #if defined(Q_OS_UNIX)
      37            0 :     return isatty(STDERR_FILENO);
      38              :     #elif defined(Q_OS_WIN)
      39              :     return GetConsoleWindow();
      40              :     #else
      41              :     return false;
      42              :     #endif
      43              : }
      44              : 
      45            0 : void configureLogging(const QCommandLineParser &parser)
      46              : {
      47              :     // Start with the Qt default message pattern (see qtbase:::qlogging.cpp:defaultPattern)
      48            0 :     QString messagePattern = QStringLiteral("%{if-category}%{category}: %{endif}%{message}");
      49              : 
      50            0 :     if (parser.isSet(QStringLiteral("debug"))) {
      51              :         #ifdef QT_MESSAGELOGCONTEXT
      52              :         // %{file}, %{line} and %{function} are only available when QT_MESSAGELOGCONTEXT is set.
      53              :         messagePattern.prepend(QStringLiteral("%{function} "));
      54              :         #endif
      55            0 :         messagePattern.prepend(QStringLiteral("%{time process} %{type} "));
      56            0 :         QLoggingCategory::setFilterRules(QStringLiteral("smithy.*.debug=true"));
      57              :     }
      58              : 
      59            0 :     const QString color = parser.value(QStringLiteral("color"));
      60            0 :     if ((color == QStringLiteral("yes")) || (color == QStringLiteral("auto") && haveConsole())) {
      61            0 :         messagePattern.prepend(QStringLiteral(
      62              :         "%{if-debug}\x1b[37m%{endif}"      // White
      63              :         "%{if-info}\x1b[32m%{endif}"       // Green
      64              :         "%{if-warning}\x1b[35m%{endif}"    // Magenta
      65              :         "%{if-critical}\x1b[31m%{endif}"   // Red
      66              :         "%{if-fatal}\x1b[31;1m%{endif}")); // Red and bold
      67            0 :         messagePattern.append(QStringLiteral("\x1b[0m")); // Reset.
      68              :     }
      69              : 
      70            0 :     qSetMessagePattern(messagePattern);
      71            0 : }
      72              : 
      73            2 : void parseCommandLineOptions(QCoreApplication &app, QCommandLineParser &parser)
      74              : {
      75           39 :     parser.addOptions({
      76            1 :         {{QStringLiteral("m"), QStringLiteral("models")},
      77            4 :           QCoreApplication::translate("main", "Read Smithy models from dir"),
      78            2 :           QStringLiteral("dir")},
      79            1 :         {{QStringLiteral("t"), QStringLiteral("templates")},
      80            4 :           QCoreApplication::translate("main", "Read text templates from dir"),
      81            2 :           QStringLiteral("dir")},
      82            1 :         {{QStringLiteral("o"), QStringLiteral("output")},
      83            4 :           QCoreApplication::translate("main", "Write output files to dir"), QStringLiteral("dir")},
      84            1 :         {{QStringLiteral("f"), QStringLiteral("force")},
      85            4 :           QCoreApplication::translate("main", "Overwrite existing files")},
      86            1 :         { {QStringLiteral("c"), QStringLiteral("color")},
      87            4 :           QCoreApplication::translate("main", "Color the console output (default auto)"),
      88            2 :           QStringLiteral("yes|no|auto"), QStringLiteral("auto")},
      89            1 :         {{QStringLiteral("d"), QStringLiteral("debug")},
      90            4 :           QCoreApplication::translate("main", "Enable debug output")},
      91              :     });
      92            2 :     parser.addHelpOption();
      93            2 :     parser.addVersionOption();
      94            2 :     parser.process(app);
      95           57 : }
      96              : 
      97            0 : bool requireOptions(const QStringList &requiredOtions, const QCommandLineParser &parser)
      98              : {
      99            0 :     QStringList missingOptions;
     100            0 :     for (const QString &requiredOption: requiredOtions) {
     101            0 :         if (!parser.isSet(requiredOption)) {
     102            0 :             missingOptions.append(requiredOption);
     103              :         }
     104              :     }
     105            0 :     if (!missingOptions.empty()) {
     106            0 :         qCCritical(lc).noquote() << QCoreApplication::translate("requireOptions",
     107            0 :             "Missing required option(s): %1").arg(missingOptions.join(QLatin1Char(' ')));
     108            0 :         return false;
     109              :     }
     110              :     return true;
     111              : }
     112              : 
     113            0 : inline bool checkRequiredOptions(const QCommandLineParser &parser)
     114              : {
     115              :     const QStringList requiredOptions{
     116            0 :         QStringLiteral("models"),
     117            0 :         QStringLiteral("templates"),
     118            0 :         QStringLiteral("output"),
     119            0 :     };
     120            0 :     return requireOptions(requiredOptions, parser);
     121              : }
     122              : 
     123            0 : bool requireDirs(const QCommandLineParser &parser, const QString &option, const QDir::Filters &rw)
     124              : {
     125              :     bool success = true;
     126            0 :     const QStringList dirs = parser.values(option);
     127            0 :     for (const QString &dir: dirs) {
     128            0 :         const QFileInfo info(dir);
     129            0 :         const QString label = option.at(0).toUpper() + option.mid(1);
     130            0 :         if (!info.exists()) {
     131            0 :             qCCritical(lc).noquote() << QCoreApplication::translate("requireDirs",
     132            0 :                 "%1 directory does not exist: %2").arg(label, dir);
     133              :             success = false;
     134            0 :         } else if (!info.isDir()) {
     135            0 :             qCCritical(lc).noquote() << QCoreApplication::translate("requireDirs",
     136            0 :                 "%1 directory is not a directory: %2").arg(label, dir);
     137              :             success = false;
     138              :         } else {
     139            0 :             if ((rw.testFlag(QDir::Readable)) && (!info.isReadable())) {
     140            0 :                 qCCritical(lc).noquote() << QCoreApplication::translate("requireDirs",
     141            0 :                     "%1 directory is not readable: %2").arg(label, dir);
     142              :                 success = false;
     143              :             }
     144            0 :             if ((rw.testFlag(QDir::Writable)) && (!info.isWritable())) {
     145            0 :                 qCCritical(lc).noquote() << QCoreApplication::translate("requireDirs",
     146            0 :                     "%1 directory is not writable: %2").arg(label, dir);
     147              :                 success = false;
     148              :             }
     149              :         }
     150            0 :     }
     151            0 :     return success;
     152              : }
     153              : 
     154            0 : inline bool checkRequiredDirs(const QCommandLineParser &parser)
     155              : {
     156              :     bool success = true;
     157              :     const QMap<QString,QDir::Filters> options{
     158            0 :         { QLatin1String("models"),    QDir::Readable },
     159            0 :         { QLatin1String("templates"), QDir::Readable },
     160            0 :         { QLatin1String("output"),    QDir::Writable },
     161            0 :     };
     162            0 :     for (auto iter = options.constBegin(); iter != options.constEnd(); ++iter) {
     163            0 :         if (!requireDirs(parser, iter.key(), iter.value())) {
     164              :             success = false;
     165              :         }
     166              :     }
     167            0 :     return success;
     168            0 : }
     169              : 
     170            0 : int loadModels(const QString &dir, smithy::Model &model)
     171              : {
     172              :     int count = 0;
     173            0 :     qCInfo(lc).noquote()
     174            0 :         << QCoreApplication::translate("loadModels", "Loading Smithy models from %1").arg(dir);
     175            0 :     QDirIterator iter{dir, {QStringLiteral("*.json")}, QDir::Files, QDirIterator::Subdirectories};
     176            0 :     while (iter.hasNext()) {
     177            0 :         QFile file{iter.next()};
     178            0 :         qCDebug(lc).noquote() << QCoreApplication::translate("loadModels", "Loading Smithy JSON: %1")
     179            0 :                                  .arg(file.fileName());
     180            0 :         if (!file.open(QFile::ReadOnly)) {
     181            0 :             qCCritical(lc).noquote() << QCoreApplication::translate("loadModels",
     182            0 :                 "Failed to open JSON file: %1").arg(file.fileName());
     183            0 :             return -count;
     184              :         }
     185            0 :         QJsonParseError error{};
     186            0 :         QJsonDocument json = QJsonDocument::fromJson(file.readAll(), &error);
     187            0 :         if (error.error != QJsonParseError::NoError) {
     188            0 :             qCCritical(lc).noquote() << QCoreApplication::translate("loadModels",
     189            0 :                 "Failed to parse JSON file: %1").arg(file.fileName());
     190            0 :             return -count;
     191              :         }
     192            0 :         if (!json.isObject()) {
     193            0 :             qCCritical(lc).noquote() << QCoreApplication::translate("loadModels",
     194            0 :                 "File is not a JSON object: %1").arg(file.fileName());
     195            0 :             return -count;
     196              :         }
     197            0 :         if (!model.insert(json.object())) {
     198            0 :             qCCritical(lc).noquote() << QCoreApplication::translate("loadModels",
     199            0 :                 "Failed to parse Smithy JSON AST: %1").arg(file.fileName());
     200            0 :             return -count;
     201              :         }
     202            0 :         ++count;
     203            0 :     }
     204            0 :     qCDebug(lc).noquote() << QCoreApplication::translate("loadModels", "Loaded %n model(s) from %1",
     205            0 :                                                          nullptr, count).arg(dir);
     206            0 :     if (count == 0) {
     207            0 :         qCCritical(lc).noquote() << QCoreApplication::translate("loadModels",
     208            0 :             "Failed to find any JSON AST models in %1").arg(dir);
     209              :     }
     210              :     return count;
     211            0 : }
     212              : 
     213            0 : int loadModels(const QStringList &dirs, smithy::Model &model)
     214              : {
     215              :     int count = 0;
     216            0 :     for (const QString &dir: dirs) {
     217            0 :         const int thisCount = loadModels(dir, model);
     218            0 :         if (thisCount <= 0) {
     219            0 :             return thisCount - count;
     220              :         } else {
     221            0 :             count += thisCount;
     222              :         }
     223              :     }
     224            0 :     qCDebug(lc).noquote() << QCoreApplication::translate("loadModels", "Loaded %n model(s) in total",
     225            0 :                                                          nullptr, count);
     226            0 :     return count;
     227              : }
     228              : 
     229            2 : int main(int argc, char *argv[])
     230              : {
     231              :     // Setup the core application.
     232            2 :     QCoreApplication app(argc, argv);
     233            3 :     app.setApplicationName(QStringLiteral(PROJECT_NAME));
     234              :     #ifdef PROJECT_PRE_RELEASE
     235            3 :     app.setApplicationVersion(QStringLiteral(PROJECT_VERSION "-" PROJECT_PRE_RELEASE));
     236              :     #else
     237              :     app.setApplicationVersion(QStringLiteral(PROJECT_VERSION));
     238              :     #endif
     239              : 
     240              :     // Parse the command line options.
     241            2 :     QCommandLineParser parser;
     242            2 :     parseCommandLineOptions(app, parser);
     243            0 :     configureLogging(parser);
     244            0 :     qCDebug(lc).noquote() << QCoreApplication::applicationName() << QCoreApplication::applicationVersion();
     245            0 :     qCDebug(lc).noquote() << "Qt " QT_VERSION_STR " compile-time";
     246            0 :     qCDebug(lc).noquote() << "Qt" << qVersion() << "runtime";
     247              :     #if defined USE_CUTELEE
     248            0 :     qCDebug(lc).noquote() << "Cutelee " CUTELEE_VERSION_STRING " compile-time";
     249              :     #elif defined USE_GRANTLEE
     250            0 :     qCDebug(lc).noquote() << "Grantlee " GRANTLEE_VERSION_STRING " compile-time";
     251              :     #elif defined USE_KTEXTTEMPLATE
     252              :     qCDebug(lc).noquote() << "KTextTemplate " KTEXTTEMPLATE_VERSION_STRING " compile-time";
     253              :     #endif
     254              : 
     255            0 :     if (!checkRequiredOptions(parser)) return 1;
     256            0 :     if (!checkRequiredDirs(parser))    return 2;
     257              : 
     258              :     // Load the Smithy model files.
     259            0 :     smithy::Model model;
     260            0 :     const int modelsCount = loadModels(parser.values(QStringLiteral("models")), model);
     261            0 :     if (modelsCount <= 0) return 3; // loadModels() will have already logged the (criticial) error.
     262            0 :     if ((!model.finish()) || (!model.isValid())) {
     263            0 :         qCCritical(lc).noquote() << QCoreApplication::translate("main",
     264            0 :             "Failed to merge Smithy model files into a valid Smithy model");
     265            0 :         return 4;
     266              :     }
     267              : 
     268              :     // Setup the renderer.
     269            0 :     Renderer renderer;
     270            0 :     if (!renderer.loadTemplates(parser.value(QStringLiteral("templates")))) {
     271              :         return 5;
     272              :     }
     273              : 
     274              :     // Setup the generator.
     275            0 :     const QString outputDir = QDir(parser.value(QStringLiteral("output"))).absolutePath();
     276            0 :     Generator generator(&model, &renderer);
     277            0 :     if (!parser.isSet(QStringLiteral("force"))) {
     278            0 :         qCWarning(lc).noquote() << QCoreApplication::translate("main", "About to generate approximately"
     279            0 :             " %n file/s in %2", nullptr, generator.expectedFileCount()).arg(outputDir);
     280            0 :         qCInfo(lc).noquote() << QCoreApplication::translate("main", "Press Enter to contine");
     281            0 :         QTextStream stream(stdin);
     282            0 :         stream.readLine();
     283            0 :     }
     284            0 :     qCInfo(lc).noquote() << QCoreApplication::translate("main", "Rendering approximately"
     285            0 :         " %n file/s in %2", nullptr, generator.expectedFileCount()).arg(outputDir);
     286            0 :     if (!generator.generate(outputDir, parser.isSet(QStringLiteral("force"))
     287              :             ? Generator::ClobberMode::Overwrite : Generator::ClobberMode::Prompt)) {
     288              :         return 6;
     289              :     }
     290            0 :     qCInfo(lc).noquote() << QCoreApplication::translate("main",
     291            0 :         "Rendered %n file/s (and skipped %1) in %2", nullptr, generator.renderedFiles().count())
     292            0 :         .arg(generator.skippedFiles().size()).arg(outputDir);
     293            0 :     return 0;
     294            0 : }
        

Generated by: LCOV version 2.2-1