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>
17#include <QCommandLineParser>
18#include <QCoreApplication>
21#include <QDirIterator>
23#include <QJsonDocument>
24#include <QLoggingCategory>
28#elif defined(Q_OS_WIN)
32Q_LOGGING_CATEGORY(lc,
"smithy.app", QtInfoMsg);
34inline bool haveConsole()
36 #if defined(Q_OS_UNIX)
37 return isatty(STDERR_FILENO);
38 #elif defined(Q_OS_WIN)
39 return GetConsoleWindow();
45void configureLogging(
const QCommandLineParser &parser)
48 QString messagePattern = QStringLiteral(
"%{if-category}%{category}: %{endif}%{message}");
50 if (parser.isSet(QStringLiteral(
"debug"))) {
51 #ifdef QT_MESSAGELOGCONTEXT
53 messagePattern.prepend(QStringLiteral(
"%{function} "));
55 messagePattern.prepend(QStringLiteral(
"%{time process} %{type} "));
56 QLoggingCategory::setFilterRules(QStringLiteral(
"smithy.*.debug=true"));
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}"
63 "%{if-info}\x1b[32m%{endif}"
64 "%{if-warning}\x1b[35m%{endif}"
65 "%{if-critical}\x1b[31m%{endif}"
66 "%{if-fatal}\x1b[31;1m%{endif}"));
67 messagePattern.append(QStringLiteral(
"\x1b[0m"));
70 qSetMessagePattern(messagePattern);
73void parseCommandLineOptions(QCoreApplication &app, QCommandLineParser &parser)
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")},
92 parser.addHelpOption();
93 parser.addVersionOption();
97bool requireOptions(
const QStringList &requiredOtions,
const QCommandLineParser &parser)
99 QStringList missingOptions;
100 for (
const QString &requiredOption: requiredOtions) {
101 if (!parser.isSet(requiredOption)) {
102 missingOptions.append(requiredOption);
105 if (!missingOptions.empty()) {
106 qCCritical(lc).noquote() << QCoreApplication::translate(
"requireOptions",
107 "Missing required option(s): %1").arg(missingOptions.join(QLatin1Char(
' ')));
113inline bool checkRequiredOptions(
const QCommandLineParser &parser)
115 const QStringList requiredOptions{
116 QStringLiteral(
"models"),
117 QStringLiteral(
"templates"),
118 QStringLiteral(
"output"),
120 return requireOptions(requiredOptions, parser);
123bool requireDirs(
const QCommandLineParser &parser,
const QString &option,
const QDir::Filters &rw)
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);
134 }
else if (!info.isDir()) {
135 qCCritical(lc).noquote() << QCoreApplication::translate(
"requireDirs",
136 "%1 directory is not a directory: %2").arg(label, dir);
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);
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);
154inline bool checkRequiredDirs(
const QCommandLineParser &parser)
157 const QMap<QString,QDir::Filters> options{
158 { QLatin1String(
"models"), QDir::Readable },
159 { QLatin1String(
"templates"), QDir::Readable },
160 { QLatin1String(
"output"), QDir::Writable },
162 for (
auto iter = options.constBegin(); iter != options.constEnd(); ++iter) {
163 if (!requireDirs(parser, iter.key(), iter.value())) {
170int loadModels(
const QString &dir, smithy::Model &model)
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());
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());
192 if (!json.isObject()) {
193 qCCritical(lc).noquote() << QCoreApplication::translate(
"loadModels",
194 "File is not a JSON object: %1").arg(file.fileName());
197 if (!model.insert(json.object())) {
198 qCCritical(lc).noquote() << QCoreApplication::translate(
"loadModels",
199 "Failed to parse Smithy JSON AST: %1").arg(file.fileName());
204 qCDebug(lc).noquote() << QCoreApplication::translate(
"loadModels",
"Loaded %n model(s) from %1",
205 nullptr, count).arg(dir);
207 qCCritical(lc).noquote() << QCoreApplication::translate(
"loadModels",
208 "Failed to find any JSON AST models in %1").arg(dir);
213int loadModels(
const QStringList &dirs, smithy::Model &model)
216 for (
const QString &dir: dirs) {
217 const int thisCount = loadModels(dir, model);
218 if (thisCount <= 0) {
219 return thisCount - count;
224 qCDebug(lc).noquote() << QCoreApplication::translate(
"loadModels",
"Loaded %n model(s) in total",
229int main(
int argc,
char *argv[])
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));
237 app.setApplicationVersion(QStringLiteral(PROJECT_VERSION));
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";
255 if (!checkRequiredOptions(parser))
return 1;
256 if (!checkRequiredDirs(parser))
return 2;
260 const int modelsCount = loadModels(parser.values(QStringLiteral(
"models")), model);
261 if (modelsCount <= 0)
return 3;
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");
270 if (!renderer.loadTemplates(parser.value(QStringLiteral(
"templates")))) {
275 const QString outputDir = QDir(parser.value(QStringLiteral(
"output"))).absolutePath();
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);
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)) {
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);
Declares the Model class.