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