cutelyst 4.8.0
A C++ Web Framework built on top of Qt, using the simple approach of Catalyst (Perl) framework.
dispatchtypechained.cpp
1/*
2 * SPDX-FileCopyrightText: (C) 2015-2022 Daniel Nicoletti <dantti12@gmail.com>
3 * SPDX-License-Identifier: BSD-3-Clause
4 */
5#include "actionchain.h"
6#include "common.h"
7#include "context.h"
8#include "dispatchtypechained_p.h"
9#include "utils.h"
10
11#include <QtCore/QUrl>
12
13using namespace Cutelyst;
14using namespace Qt::Literals::StringLiterals;
15
18 , d_ptr(new DispatchTypeChainedPrivate)
19{
20}
21
26
28{
29 Q_D(const DispatchTypeChained);
30
31 QByteArray buffer;
32 Actions endPoints = d->endPoints;
33 std::sort(endPoints.begin(), endPoints.end(), [](Action *a, Action *b) -> bool {
34 return a->reverse() < b->reverse();
35 });
36
38 QVector<QStringList> unattachedTable;
39 for (Action *endPoint : endPoints) {
40 QStringList parts;
41 if (endPoint->numberOfArgs() == -1) {
42 parts.append(QLatin1String("..."));
43 } else {
44 for (int i = 0; i < endPoint->numberOfArgs(); ++i) {
45 parts.append(QLatin1String("*"));
46 }
47 }
48
50 QString extra = DispatchTypeChainedPrivate::listExtraHttpMethods(endPoint);
51 QString consumes = DispatchTypeChainedPrivate::listExtraConsumes(endPoint);
52 ActionList parents;
53 Action *current = endPoint;
54 while (current) {
55 for (int i = 0; i < current->numberOfCaptures(); ++i) {
56 parts.prepend(QLatin1String("*"));
57 }
58
59 const auto attributes = current->attributes();
60 const QStringList pathParts = attributes.values(QLatin1String("PathPart"));
61 for (const QString &part : pathParts) {
62 if (!part.isEmpty()) {
63 parts.prepend(part);
64 }
65 }
66
67 parent = attributes.value(QLatin1String("Chained"));
68 current = d->actions.value(parent);
69 if (current) {
70 parents.prepend(current);
71 }
72 }
73
74 if (parent.compare(u"/") != 0) {
75 QStringList row;
76 if (parents.isEmpty()) {
77 row.append(QLatin1Char('/') + endPoint->reverse());
78 } else {
79 row.append(QLatin1Char('/') + parents.first()->reverse());
80 }
81 row.append(parent);
82 unattachedTable.append(row);
83 continue;
84 }
85
87 for (Action *p : parents) {
88 QString name = QLatin1Char('/') + p->reverse();
89
90 QString extraHttpMethod = DispatchTypeChainedPrivate::listExtraHttpMethods(p);
91 if (!extraHttpMethod.isEmpty()) {
92 name.prepend(extraHttpMethod + QLatin1Char(' '));
93 }
94
95 const auto attributes = p->attributes();
96 auto it = attributes.constFind(QLatin1String("CaptureArgs"));
97 if (it != attributes.constEnd()) {
98 name.append(QLatin1String(" (") + it.value() + QLatin1Char(')'));
99 } else {
100 name.append(QLatin1String(" (0)"));
101 }
102
103 QString ct = DispatchTypeChainedPrivate::listExtraConsumes(p);
104 if (!ct.isEmpty()) {
105 name.append(QLatin1String(" :") + ct);
106 }
107
108 if (p != parents[0]) {
109 name = QLatin1String("-> ") + name;
110 }
111
112 rows.append({QString(), name});
113 }
114
115 QString line;
116 if (!rows.isEmpty()) {
117 line.append(QLatin1String("=> "));
118 }
119 if (!extra.isEmpty()) {
120 line.append(extra + QLatin1Char(' '));
121 }
122 line.append(QLatin1Char('/') + endPoint->reverse());
123 if (endPoint->numberOfArgs() == -1) {
124 line.append(QLatin1String(" (...)"));
125 } else {
126 line.append(QLatin1String(" (") + QString::number(endPoint->numberOfArgs()) +
127 QLatin1Char(')'));
128 }
129
130 if (!consumes.isEmpty()) {
131 line.append(QLatin1String(" :") + consumes);
132 }
133 rows.append({QString(), line});
134
135 rows[0][0] = QLatin1Char('/') + parts.join(QLatin1Char('/'));
136 paths.append(rows);
137 }
138
140
141 if (!paths.isEmpty()) {
142 out << Utils::buildTable(paths,
143 {QLatin1String("Path Spec"), QLatin1String("Private")},
144 QLatin1String("Loaded Chained actions:"));
145 }
146
147 if (!unattachedTable.isEmpty()) {
148 out << Utils::buildTable(unattachedTable,
149 {QLatin1String("Private"), QLatin1String("Missing parent")},
150 QLatin1String("Unattached Chained actions:"));
151 }
152
153 return buffer;
154}
155
158{
159 if (!args.isEmpty()) {
160 return NoMatch;
161 }
162
163 Q_D(const DispatchTypeChained);
164
165 // TODO avoid toString()
166 // TODO remove mid(1)
167 const BestActionMatch ret =
168 d->recurseMatch(args.size(), u"/"_s, path.mid(1).toString().split(QLatin1Char('/')));
169 const ActionList chain = ret.actions;
170 if (ret.isNull || chain.isEmpty()) {
171 return NoMatch;
172 }
173
174 QStringList decodedArgs;
175 const QStringList parts = ret.parts;
176 for (const QString &arg : parts) {
177 QString aux = arg;
178 decodedArgs.append(Utils::decodePercentEncoding(&aux));
179 }
180
181 ActionChain *action = new ActionChain(chain, c);
182 Request *request = c->request();
183 request->setArguments(decodedArgs);
184 request->setCaptures(ret.captures);
185 request->setMatch(QLatin1Char('/') + action->reverse());
186 setupMatchedAction(c, action);
187
188 return ExactMatch;
189}
190
192{
194
195 auto attributes = action->attributes();
196 const QStringList chainedList = attributes.values(QLatin1String("Chained"));
197 if (chainedList.isEmpty()) {
198 return false;
199 }
200
201 if (chainedList.size() > 1) {
202 qCCritical(CUTELYST_DISPATCHER_CHAINED)
203 << "Multiple Chained attributes not supported registering" << action->reverse();
204 return false;
205 }
206
207 const QString &chainedTo = chainedList.first();
208 if (chainedTo == u'/' + action->name()) {
209 qCCritical(CUTELYST_DISPATCHER_CHAINED)
210 << "Actions cannot chain to themselves registering /" << action->name();
211 return false;
212 }
213
214 const QStringList pathPart = attributes.values(QLatin1String("PathPart"));
215
216 QString part = action->name();
217
218 if (pathPart.size() == 1 && !pathPart[0].isEmpty()) {
219 part = pathPart[0];
220 } else if (pathPart.size() > 1) {
221 qCCritical(CUTELYST_DISPATCHER_CHAINED)
222 << "Multiple PathPart attributes not supported registering" << action->reverse();
223 return false;
224 }
225
226 if (part.startsWith(QLatin1Char('/'))) {
227 qCCritical(CUTELYST_DISPATCHER_CHAINED)
228 << "Absolute parameters to PathPart not allowed registering" << action->reverse();
229 return false;
230 }
231
232 attributes.replace(QStringLiteral("PathPart"), part);
233 action->setAttributes(attributes);
234
235 auto &childrenOf = d->childrenOf[chainedTo][part];
236 childrenOf.insert(childrenOf.begin(), action);
237
238 d->actions[QLatin1Char('/') + action->reverse()] = action;
239
240 if (!d->checkArgsAttr(action, QLatin1String("Args")) ||
241 !d->checkArgsAttr(action, QLatin1String("CaptureArgs"))) {
242 return false;
243 }
244
245 if (attributes.contains(QLatin1String("Args")) &&
246 attributes.contains(QLatin1String("CaptureArgs"))) {
247 qCCritical(CUTELYST_DISPATCHER_CHAINED)
248 << "Combining Args and CaptureArgs attributes not supported registering"
249 << action->reverse();
250 return false;
251 }
252
253 if (!attributes.contains(QLatin1String("CaptureArgs"))) {
254 d->endPoints.push_back(action);
255 }
256
257 return true;
258}
259
261{
262 Q_D(const DispatchTypeChained);
263
264 QString ret;
265 const ParamsMultiMap attributes = action->attributes();
266 if (!(attributes.contains(QStringLiteral("Chained")) &&
267 !attributes.contains(QStringLiteral("CaptureArgs")))) {
268 qCWarning(CUTELYST_DISPATCHER_CHAINED)
269 << "uriForAction: action is not an end point" << action;
270 return ret;
271 }
272
274 QStringList localCaptures = captures;
275 QStringList parts;
276 Action *curr = action;
277 while (curr) {
278 const ParamsMultiMap curr_attributes = curr->attributes();
279 if (curr_attributes.contains(QStringLiteral("CaptureArgs"))) {
280 if (localCaptures.size() < curr->numberOfCaptures()) {
281 // Not enough captures
282 qCWarning(CUTELYST_DISPATCHER_CHAINED)
283 << "uriForAction: not enough captures" << curr->numberOfCaptures()
284 << captures.size();
285 return ret;
286 }
287
288 parts = localCaptures.mid(localCaptures.size() - curr->numberOfCaptures()) + parts;
289 localCaptures = localCaptures.mid(0, localCaptures.size() - curr->numberOfCaptures());
290 }
291
292 const QString pp = curr_attributes.value(QStringLiteral("PathPart"));
293 if (!pp.isEmpty()) {
294 parts.prepend(pp);
295 }
296
297 parent = curr_attributes.value(QStringLiteral("Chained"));
298 curr = d->actions.value(parent);
299 }
300
301 if (parent.compare(u"/") != 0) {
302 // fail for dangling action
303 qCWarning(CUTELYST_DISPATCHER_CHAINED) << "uriForAction: dangling action" << parent;
304 return ret;
305 }
306
307 if (!localCaptures.isEmpty()) {
308 // fail for too many captures
309 qCWarning(CUTELYST_DISPATCHER_CHAINED)
310 << "uriForAction: too many captures" << localCaptures;
311 return ret;
312 }
313
314 ret = QLatin1Char('/') + parts.join(QLatin1Char('/'));
315 return ret;
316}
317
319{
320 Q_D(const DispatchTypeChained);
321
322 // Do not expand action if action already is an ActionChain
323 if (qobject_cast<ActionChain *>(action)) {
324 return action;
325 }
326
327 // The action must be chained to something
328 if (!action->attributes().contains(QStringLiteral("Chained"))) {
329 return nullptr;
330 }
331
332 ActionList chain;
333 Action *curr = action;
334
335 while (curr) {
336 chain.prepend(curr);
337 const QString parent = curr->attribute(QStringLiteral("Chained"));
338 curr = d->actions.value(parent);
339 }
340
341 return new ActionChain(chain, const_cast<Context *>(c));
342}
343
345{
346 Q_D(const DispatchTypeChained);
347
348 if (d->actions.isEmpty()) {
349 return false;
350 }
351
352 // Optimize end points
353
354 return true;
355}
356
357BestActionMatch DispatchTypeChainedPrivate::recurseMatch(int reqArgsSize,
358 const QString &parent,
359 const QStringList &pathParts) const
360{
361 BestActionMatch bestAction;
362 auto it = childrenOf.constFind(parent);
363 if (it == childrenOf.constEnd()) {
364 return bestAction;
365 }
366
367 const StringActionsMap &children = it.value();
368 QStringList keys = children.keys();
369 std::sort(keys.begin(), keys.end(), [](const QString &a, const QString &b) -> bool {
370 // action2 then action1 to try the longest part first
371 return b.size() < a.size();
372 });
373
374 for (const QString &tryPart : keys) {
375 QStringList parts = pathParts;
376 if (!tryPart.isEmpty()) {
377 // We want to count the number of parts a split would give
378 // and remove the number of parts from tryPart
379 int tryPartCount = tryPart.count(QLatin1Char('/')) + 1;
380 const QStringList possiblePart = parts.mid(0, tryPartCount);
381 if (tryPart != possiblePart.join(QLatin1Char('/'))) {
382 continue;
383 }
384 parts = parts.mid(tryPartCount);
385 }
386
387 const Actions tryActions = children.value(tryPart);
388 for (Action *action : tryActions) {
389 const ParamsMultiMap attributes = action->attributes();
390 if (attributes.contains(QStringLiteral("CaptureArgs"))) {
391 const int captureCount = action->numberOfCaptures();
392 // Short-circuit if not enough remaining parts
393 if (parts.size() < captureCount) {
394 continue;
395 }
396
397 // strip CaptureArgs into list
398 const QStringList captures = parts.mid(0, captureCount);
399
400 // check if the action may fit, depending on a given test by the app
401 if (!action->matchCaptures(captures.size())) {
402 continue;
403 }
404
405 const QStringList localParts = parts.mid(captureCount);
406
407 // try the remaining parts against children of this action
408 const BestActionMatch ret =
409 recurseMatch(reqArgsSize, QLatin1Char('/') + action->reverse(), localParts);
410
411 // No best action currently
412 // OR The action has less parts
413 // OR The action has equal parts but less captured data (ergo more defined)
414 ActionList actions = ret.actions;
415 const QStringList actionCaptures = ret.captures;
416 const QStringList actionParts = ret.parts;
417 int bestActionParts = bestAction.parts.size();
418
419 if (!actions.isEmpty() &&
420 (bestAction.isNull || actionParts.size() < bestActionParts ||
421 (actionParts.size() == bestActionParts &&
422 actionCaptures.size() < bestAction.captures.size() &&
423 ret.n_pathParts > bestAction.n_pathParts))) {
424 actions.prepend(action);
425 int pathparts =
426 attributes.value(QStringLiteral("PathPart")).count(QLatin1Char('/')) + 1;
427 bestAction.actions = actions;
428 bestAction.captures = captures + actionCaptures;
429 bestAction.parts = actionParts;
430 bestAction.n_pathParts = pathparts + ret.n_pathParts;
431 bestAction.isNull = false;
432 }
433 } else {
434 if (!action->match(reqArgsSize + parts.size())) {
435 continue;
436 }
437
438 const QString argsAttr = attributes.value(QStringLiteral("Args"));
439 const int pathparts =
440 attributes.value(QStringLiteral("PathPart")).count(QLatin1Char('/')) + 1;
441 // No best action currently
442 // OR This one matches with fewer parts left than the current best action,
443 // And therefore is a better match
444 // OR No parts and this expects 0
445 // The current best action might also be Args(0),
446 // but we couldn't chose between then anyway so we'll take the last seen
447
448 if (bestAction.isNull || parts.size() < bestAction.parts.size() ||
449 (parts.isEmpty() && !argsAttr.isEmpty() && action->numberOfArgs() == 0)) {
450 bestAction.actions = {action};
451 bestAction.captures = QStringList();
452 bestAction.parts = parts;
453 bestAction.n_pathParts = pathparts;
454 bestAction.isNull = false;
455 }
456 }
457 }
458 }
459
460 return bestAction;
461}
462
463bool DispatchTypeChainedPrivate::checkArgsAttr(Action *action, const QString &name) const
464{
465 const auto attributes = action->attributes();
466 if (!attributes.contains(name)) {
467 return true;
468 }
469
470 const QStringList values = attributes.values(name);
471 if (values.size() > 1) {
472 qCCritical(CUTELYST_DISPATCHER_CHAINED)
473 << "Multiple" << name << "attributes not supported registering" << action->reverse();
474 return false;
475 }
476
477 QString args = values[0];
478 bool ok;
479 if (!args.isEmpty() && args.toInt(&ok) < 0 && !ok) {
480 qCCritical(CUTELYST_DISPATCHER_CHAINED)
481 << "Invalid" << name << "(" << args << ") for action" << action->reverse() << "(use '"
482 << name << "' or '" << name << "(<number>)')";
483 return false;
484 }
485
486 return true;
487}
488
489QString DispatchTypeChainedPrivate::listExtraHttpMethods(Action *action)
490{
491 QString ret;
492 const auto attributes = action->attributes();
493 if (attributes.contains(QLatin1String("HTTP_METHODS"))) {
494 const QStringList extra = attributes.values(QLatin1String("HTTP_METHODS"));
495 ret = extra.join(QLatin1String(", "));
496 }
497 return ret;
498}
499
500QString DispatchTypeChainedPrivate::listExtraConsumes(Action *action)
501{
502 QString ret;
503 const auto attributes = action->attributes();
504 if (attributes.contains(QLatin1String("CONSUMES"))) {
505 const QStringList extra = attributes.values(QLatin1String("CONSUMES"));
506 ret = extra.join(QLatin1String(", "));
507 }
508 return ret;
509}
510
511#include "moc_dispatchtypechained.cpp"
Holds a chain of Cutelyst actions.
Definition actionchain.h:26
This class represents a Cutelyst Action.
Definition action.h:35
void setAttributes(const ParamsMultiMap &attributes)
Definition action.cpp:81
virtual qint8 numberOfCaptures() const
Definition action.cpp:131
ParamsMultiMap attributes() const noexcept
Definition action.cpp:69
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition action.cpp:75
QString reverse() const noexcept
Definition component.cpp:45
QString name() const noexcept
Definition component.cpp:33
The Cutelyst Context.
Definition context.h:42
Request * request
Definition context.h:71
MatchType match(Context *c, QStringView path, const QStringList &args) const override
QString uriForAction(Action *action, const QStringList &captures) const override
QByteArray list() const override
bool registerAction(Action *action) override
Action * expandAction(const Context *c, Action *action) const final
DispatchTypeChained(QObject *parent=nullptr)
DispatchType(QObject *parent=nullptr)
void setupMatchedAction(Context *c, Action *action) const
A request.
Definition request.h:42
void setCaptures(const QStringList &captures)
Definition request.cpp:168
void setArguments(const QStringList &arguments)
Definition request.cpp:156
void setMatch(const QString &match)
Definition request.cpp:144
QMultiMap< QString, QString > ParamsMultiMap
The Cutelyst namespace holds all public Cutelyst API.
QVector< Action * > ActionList
Definition action.h:161
void append(QList< T > &&value)
QList< T >::iterator begin()
QList< T >::iterator end()
T & first()
bool isEmpty() const const
QList< T > mid(qsizetype pos, qsizetype length) const const
void prepend(QList< T >::parameter_type value)
qsizetype size() const const
T value(qsizetype i) const const
bool contains(const Key &key) const const
T value(const Key &key, const T &defaultValue) const const
QList< T > values() const const
QObject(QObject *parent)
QObject * parent() const const
T qobject_cast(QObject *object)
QString & append(QChar ch)
bool isEmpty() const const
QString number(double n, char format, int precision)
QString & prepend(QChar ch)
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
int toInt(bool *ok, int base) const const
QString join(QChar separator) const const
QStringView mid(qsizetype start, qsizetype length) const const
QString toString() const const