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 : }
|