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