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