10#include <QDirIterator>
11#include <QJsonParseError>
12#include <QRegularExpression>
14#define QSL(str) QStringLiteral(str)
19 ScopedContext(
Renderer * renderer,
const QVariantHash &hash) : renderer(renderer)
33const QRegularExpression Generator::servicePattern{
34 QSL(
"(?<type>Service)(?<seperator>[^a-zA-Z0-9]*)(?<id>Title|Id|sdkId)"),
35 QRegularExpression::CaseInsensitiveOption
38const QRegularExpression Generator::operationPattern{
39 QSL(
"(?<type>Operation)(?<seperator>[^a-zA-Z0-9]*)(?<id>Name)"),
40 QRegularExpression::CaseInsensitiveOption
43Generator::Generator(
const smithy::Model *
const model,
Renderer *
const renderer)
44 : model(model), renderer(renderer)
46 Q_ASSERT(model->isValid());
51int Generator::expectedFileCount()
const
53 const QHash<smithy::ShapeId, smithy::Shape> services = model->shapes(smithy::Shape::Type::Service);
54 const int operations = std::accumulate(services.constBegin(), services.constEnd(), 0,
55 [](
const int a,
const smithy::Shape &shape) { return a + shape.operations().size();
57 const QStringList templates = renderer->templatesNames();
58 return std::accumulate(templates.constBegin(), templates.constEnd(), 0,
59 [&](
const int a,
const QString &name) {
60 return a + (servicePattern.match(name).hasMatch() ?
61 (operationPattern.match(name).hasMatch() ? operations : services.size()) : 1);
65bool Generator::generate(
const QString &outputDir, ClobberMode clobberMode)
68 QVariantMap servicesMap;
69 const QHash<smithy::ShapeId, smithy::Shape> services = model->shapes(smithy::Shape::Type::Service);
70 for (
const smithy::Shape &service: services) {
71 servicesMap.insert(service.id().toString(), toContext(service));
73 const ScopedContext context(renderer, { { QSL(
"services"), servicesMap } });
76 const QStringList templatesNames = renderer->templatesNames();
77 QStringList serviceTemplateNames, operationTemplateNames, plainTemplateNames;
78 for (
const QString &name: templatesNames) {
79 ((servicePattern.match(name).hasMatch()) ? (operationPattern.match(name).hasMatch() ?
80 operationTemplateNames : serviceTemplateNames) : plainTemplateNames).append(name);
82 Q_ASSERT(serviceTemplateNames.size() + operationTemplateNames.size()
83 + plainTemplateNames.size() == templatesNames.size());
86 if (!std::all_of(services.constBegin(), services.constEnd(), [&](
const smithy::Shape &service) {
87 return renderService(service, serviceTemplateNames, operationTemplateNames, outputDir, clobberMode);
93 qCDebug(lc).noquote() << tr(
"Rendering %n unary template/s",
nullptr, plainTemplateNames.size());
94 return std::all_of(plainTemplateNames.constBegin(), plainTemplateNames.constEnd(),
95 [&](
const QString &templateName){
96 const QString outputPathName = outputDir + QLatin1Char(
'/') + templateName;
97 return render(templateName, outputPathName, clobberMode);
101QStringList Generator::renderedFiles()
const
103 return renderedPathNames;
106QStringList Generator::skippedFiles()
const
108 return skippedPathNames;
111bool Generator::renderService(
const smithy::Shape &service,
112 const QStringList &serviceTemplateNames,
113 const QStringList &operationTemplateNames,
114 const QString &outputDir, ClobberMode &clobberMode)
116 qCDebug(lc).noquote() << tr(
"Rendering templates for service %1").arg(service.id().toString());
119 const QVariantHash serviceContext = toContext(service);
121 const ScopedContext context(renderer, { { QSL(
"service"), serviceContext } });
124 for (
const QString &templateName: serviceTemplateNames) {
125 const QString outputPathName = makeOutputPathName(templateName, servicePattern, {
126 { QSL(
"name"), service.id().shapeName() },
127 { QSL(
"id"), serviceContext.value(QSL(
"canonicalId")).toString() },
128 { QSL(
"sdkid"), serviceContext.value(QSL(
"sdkId")).toString() },
130 if (!render(templateName, outputPathName, clobberMode)) {
136 const QVariantMap operations = serviceContext.value(QSL(
"operations")).toMap();
137 for (
auto iter = operations.constBegin(); iter != operations.constEnd(); ++iter) {
138 const smithy::Shape operation = model->shape(iter->toHash().value(QSL(
"shapeId")).toString());
139 if (!operation.isValid()) {
140 qCCritical(lc).noquote() << tr(
"Failed to find shape for %1 operation in %2 service")
141 .arg(iter.key(), service.id().toString());
144 if (!renderOperation(operation, serviceContext, operationTemplateNames, outputDir, clobberMode)) {
151bool Generator::renderOperation(
const smithy::Shape &operation,
const QVariantHash serviceContext,
152 const QStringList &templateNames,
const QString &outputDir,
153 ClobberMode &clobberMode)
155 qCDebug(lc).noquote() << tr(
"Rendering templates for operation %1").arg(operation.id().toString());
159 const ScopedContext context(renderer, { { QSL(
"operation"), toContext(operation) } });
160 for (
const QString &templateName: templateNames) {
161 QString outputPathName = makeOutputPathName(templateName, servicePattern, {
162 { QSL(
"name"), serviceContext.value(QSL(
"shapeName")).toString() },
163 { QSL(
"id"), serviceContext.value(QSL(
"canonicalId")).toString() },
164 { QSL(
"sdkid"), serviceContext.value(QSL(
"sdkId")).toString() },
166 outputPathName = makeOutputPathName(outputPathName, operationPattern, {
167 { QSL(
"name"), operation.id().shapeName() },
169 if (!render(templateName, outputPathName, clobberMode)) {
176bool Generator::render(
const QString &templateName,
const QString &outputPathName,
177 ClobberMode &clobberMode)
179 if (QFile::exists(outputPathName)) {
180 switch (clobberMode) {
181 case ClobberMode::Overwrite:
182 qCDebug(lc) << tr(
"Overwriting existing file: %1").arg(outputPathName);
184 case ClobberMode::Prompt:
185 if (promptToOverwrite(outputPathName, clobberMode)) {
188 __attribute__((fallthrough));
189 case ClobberMode::Skip:
190 qCDebug(lc) << tr(
"Skipping existing output file: %1").arg(outputPathName);
191 skippedPathNames.append(outputPathName);
195 if (!renderer->render(templateName, outputPathName)) {
198 renderedPathNames.append(outputPathName);
203QVariantHash Generator::toContext(
const smithy::Shape &shape)
const
205 if (shape.type() == smithy::Shape::Type::Service) {
206 QVariantHash hash = shape.rawAst().toVariantHash();
207 hash.insert(QSL(
"shapeName"), shape.id().shapeName());
208 hash.insert(QSL(
"canonicalId"), canonicalServiceId(shape.traits()
209 .value(QSL(
"aws.api#service")).toObject().value(QSL(
"sdkId")).toString()));
210 hash.insert(QSL(
"sdkId"), shape.traits().value(QSL(
"aws.api#service")).toObject()
211 .value(QSL(
"sdkId")).toString());
213 shape.traits().value(QSL(
"smithy.api#documentation")).toString()));
214 QVariantMap operations;
215 const smithy::Shape::ShapeReferences operationRefs = shape.operations();
216 for (
const smithy::Shape::ShapeReference &operationRef: operationRefs) {
217 operations.insert(operationRef.target.shapeName(),
218 toContext(model->shape(operationRef.target)));
220 Q_ASSERT(operationRefs.size() == operations.size());
221 const smithy::Shape::ShapeReferences resourceRefs = shape.resources();
222 for (
const smithy::Shape::ShapeReference &resourceRef: resourceRefs) {
223 const QVariantHash resourceContext = toContext(model->shape(resourceRef.target));
227 const QVariantMap resourceOperations = resourceContext.value(QSL(
"operations")).toMap();
228 for (
auto iter = resourceOperations.constBegin(); iter != resourceOperations.constEnd(); ++iter) {
229 operations.insert(iter.key(), iter.value());
232 hash.insert(QSL(
"operations"), operations);
236 if (shape.type() == smithy::Shape::Type::Operation) {
237 QVariantHash hash = shape.rawAst().toVariantHash();
238 hash.insert(QSL(
"name"), shape.id().shapeName());
239 hash.insert(QSL(
"shapeId"), shape.id().toString());
241 shape.traits().value(QSL(
"smithy.api#documentation")).toString()));
245 if (shape.type() == smithy::Shape::Type::Resource) {
246 QVariantHash hash = shape.rawAst().toVariantHash();
247 QVariantMap operations;
248 #define QTSMITHY_IF_VALID_INSERT(action) { \
249 const smithy::ShapeId action##TargetId = shape.action().target; \
250 if (action##TargetId.isValid()) { \
251 operations.insert(action##TargetId.shapeName(), \
252 toContext(model->shape(action##TargetId))); \
255 QTSMITHY_IF_VALID_INSERT(create)
256 QTSMITHY_IF_VALID_INSERT(put)
257 QTSMITHY_IF_VALID_INSERT(read)
258 QTSMITHY_IF_VALID_INSERT(update)
259 QTSMITHY_IF_VALID_INSERT(Delete)
260 QTSMITHY_IF_VALID_INSERT(list)
261 #undef QTSMITHY_IF_VALID_INSERT
263 #define QTSMITHY_ADD_SHAPES(property) { \
264 const smithy::Shape::ShapeReferences refs = shape.property(); \
265 for (const smithy::Shape::ShapeReference &ref: refs) { \
266 operations.insert(ref.target.shapeName(), toContext(model->shape(ref.target))); \
269 QTSMITHY_ADD_SHAPES(operations)
270 QTSMITHY_ADD_SHAPES(collectionOperations)
271 #undef QTSMITHY_ADD_SHAPES
273 const smithy::Shape::ShapeReferences resourceRefs = shape.resources();
274 for (
const smithy::Shape::ShapeReference &resourceRef: resourceRefs) {
275 const QVariantHash resourceContext = toContext(model->shape(resourceRef.target));
279 const QVariantMap resourceOperations = resourceContext.value(QSL(
"operations")).toMap();
280 for (
auto iter = resourceOperations.constBegin(); iter != resourceOperations.constEnd(); ++iter) {
281 operations.insert(iter.key(), iter.value());
285 hash.insert(QSL(
"operations"), operations);
289 qCCritical(lc).noquote() << tr(
"Cannot generate context for shape type 0x%1")
290 .arg((
int)shape.type(), 0, 16);
291 return QVariantHash{};
295const QString Generator::canonicalServiceId(
const QString &sdkId)
298 const QMap<QString,QString> specialCases{
299 { QSL(
"identitystore" ), QSL(
"IdentityStore") },
300 { QSL(
"ivschat" ), QSL(
"IvsChat") },
301 { QSL(
"forecastquery" ), QSL(
"ForecastQuery") },
302 { QSL(
"resiliencehub" ), QSL(
"ResilienceHub") },
303 { QSL(
"savingsplans" ), QSL(
"SavingsPlans") },
304 { QSL(
"codeartifact" ), QSL(
"CodeArtifact") },
305 { QSL(
"imagebuilder" ), QSL(
"ImageBuilder") },
306 { QSL(
"billingconductor"), QSL(
"BillingConductor") },
308 for (
const auto iter = specialCases.constFind(sdkId); iter != specialCases.constEnd(); ) {
316 auto iter = QRegularExpression(QSL(
"[A-Z]{2,}([^a-zA-Z]|$)")).globalMatch(
id);
317 while (iter.hasNext()) {
318 const auto match = iter.next();
319 id.replace(match.capturedStart()+1, match.capturedLength()-1,
320 match.captured().mid(1).toLower());
324 #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
325 if (
id ==
id.toLower()) {
329 const QStringList words =
id.split(QRegularExpression(QSL(
"[^a-zA-Z0-9]+")));
331 for (
const QString &word: words) {
332 id.append(word.at(0).toUpper() + word.mid(1));
337 id.replace(QRegularExpression(QSL(
"[^a-zA-Z0-9]|Amazon|AWS"),
338 QRegularExpression::PatternOption::CaseInsensitiveOption),QString());
341 if (
id == QSL(
"ConfigService"))
return id;
342 if (
id == QSL(
"DirectoryService"))
return id;
343 id.replace(QRegularExpression(QSL(
"(API|Client|Service)$"),
344 QRegularExpression::PatternOption::CaseInsensitiveOption),QString());
348bool Generator::promptToOverwrite(
const QString &pathName, ClobberMode &clobberMode)
350 Q_ASSERT(clobberMode == ClobberMode::Prompt);
352 qCWarning(lc).noquote() << tr(
"Overwrite %1 [y,n,a,s,q,?]? ").arg(pathName);
353 QTextStream stream(stdin);
354 const QString response = stream.readLine().toLower();
355 if (response == QSL(
"y")) {
357 }
else if (response == QSL(
"n")) {
359 }
else if (response == QSL(
"a")) {
360 clobberMode = ClobberMode::Overwrite;
362 }
else if (response == QSL(
"s")) {
363 clobberMode = ClobberMode::Skip;
365 }
else if (response == QSL(
"q")) {
366 QCoreApplication::exit(EXIT_FAILURE);
368 qCInfo(lc).noquote() << tr(
"y - overwrite this file");
369 qCInfo(lc).noquote() << tr(
"n - do not overwrite this file");
370 qCInfo(lc).noquote() << tr(
"a - overwrite this, and all remaining files");
371 qCInfo(lc).noquote() << tr(
"s - do not overwrite this, or any remaining files");
372 qCInfo(lc).noquote() << tr(
"q - quit now, without writing any further files");
373 qCInfo(lc).noquote() << tr(
"? - print help");
379Generator::Capitalisation Generator::getCase(
const QString &first,
const QString &second)
381 #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
382 if ((first == first.toLower()) && (second == second.toLower())) {
384 if (first.isLower() && second.isLower()) {
386 return Capitalisation::lowercase;
388 #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
389 if ((first == first.toUpper()) && (second == second.toUpper())) {
391 if (first.isUpper() && second.isUpper()) {
393 return Capitalisation::UPPERCASE;
395 #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
396 if ((first.at(0) == first.at(0).toLower()) && (second.at(0) == second.at(0).toUpper())) {
398 if (first.front().isLower() && second.front().isUpper()) {
400 return Capitalisation::camelCase;
402 #if (QT_VERSION < QT_VERSION_CHECK(5, 12, 0))
403 if ((first.at(0) == first.at(0).isUpper()) && (second.at(0) == second.at(0).toLower())) {
405 if (first.front().isUpper() && second.front().isUpper()) {
407 return Capitalisation::CamelCase;
409 return Capitalisation::NoChange;
412QString Generator::makeCase(
const QString &
string,
const Capitalisation &capitalisation)
414 switch (capitalisation) {
415 case Capitalisation::NoChange:
417 case Capitalisation::lowercase:
418 return string.toLower();
419 case Capitalisation::camelCase:
422 case Capitalisation::CamelCase:
425 case Capitalisation::UPPERCASE:
426 return string.toUpper();
428 qCCritical(lc).noquote() << tr(
"Unknown capitalisation %1 for %2")
429 .arg((
int)capitalisation).arg(
string);
433QString Generator::makeOutputPathName(
const QString &templateName,
const QRegularExpression &pattern,
434 const QHash<QString,QString> &ids,
const QString &outputDir)
436 QString outputPathName = templateName;
437 for (
auto iter = pattern.globalMatch(templateName); iter.hasNext(); ) {
438 const QRegularExpressionMatch match = iter.next();
439 const QString type = match.captured(QSL(
"type"));
440 const QString sep = match.captured(QSL(
"separator"));
441 const QString
id = match.captured(QSL(
"id"));
442 const QString newId = ids.value(
id.toLower());
443 if (newId.isNull()) {
444 qCCritical(lc).noquote() << tr(
"No pathname ID value for %1").arg(
id);
447 const QString label = makeCase(newId, getCase(type,
id)).replace(QLatin1Char(
' '), sep);
448 outputPathName.replace(type+sep+
id, label);
450 if (!outputDir.isEmpty()) {
451 outputPathName.prepend(outputDir + QLatin1Char(
'/'));
453 return outputPathName;
461 QString content(html);
468 content.replace(QSL(
"<function>"), QSL(
"<code>"));
469 content.replace(QSL(
"</function>"), QSL(
"</code>"));
471 content.replace(QSL(
"<important>"), QSL(
"<b>"));
472 content.replace(QSL(
"</important>"), QSL(
"</b>"));
475 content.replace(QSL(
"/*"), QSL(
"/*"));
476 content.replace(QSL(
"*/"), QSL(
"*/"));
480 #if (QT_VERSION > QT_VERSION_CHECK(5, 14, 0))
481 #define SKIP_EMPTY_PARTS Qt::SkipEmptyParts
483 #define SKIP_EMPTY_PARTS QString::SkipEmptyParts
485 for (QString word: content.split(QRegularExpression(QSL(
"\\s+")), SKIP_EMPTY_PARTS)) {
486 if (word.startsWith(QSL(
"<p>")) || word.endsWith(QSL(
"</p>"))) {
489 if (!lines.last().isEmpty()) {
490 lines.append(QString());
492 if (word.startsWith(QSL(
"<p>"))) {
495 if (word.endsWith(QSL(
"</p>"))) {
496 word.remove(word.size()-5,4);
500 if (line.isEmpty()) {
502 }
else if (line.size() + word.size() < 120) {
503 line += QLatin1Char(
' ') + word;
511 while ((!lines.isEmpty()) && (lines.first().isEmpty())) {
514 while ((!lines.isEmpty()) && (lines.last().isEmpty())) {
static QStringList formatHtmlDocumentation(const QString &html)
Declares the Model class.