cutelyst  5.0.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 using namespace Qt::Literals::StringLiterals;
15 
17  : DispatchType(parent)
18  , d_ptr(new DispatchTypeChainedPrivate)
19 {
20 }
21 
23 {
24  delete d_ptr;
25 }
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 
138  QTextStream out(&buffer, QTextStream::WriteOnly);
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 {
199  Q_D(DispatchTypeChained);
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 
276  QString parent;
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 
360 BestActionMatch 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 
476 bool 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 
502 QString 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 
513 QString 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"
ParamsMultiMap attributes() const noexcept
Definition: action.cpp:68
Request request
Definition: context.h:72
Action * expandAction(const Context *c, Action *action) const final
QString & append(QChar ch)
MatchType match(Context *c, QStringView path, const QStringList &args) const override
bool registerAction(Action *action) override
virtual qint8 numberOfCaptures() const
Definition: action.cpp:130
QString & prepend(QChar ch)
qsizetype count() const const
qsizetype size() const const
QStringView mid(qsizetype start, qsizetype length) const const
Holds a chain of Cutelyst actions.
Definition: actionchain.h:25
QString join(QChar separator) const const
void setupMatchedAction(Context *c, Action *action) const
void setMatch(const QString &match)
Definition: request.cpp:143
QString reverse() const noexcept
Definition: component.cpp:45
QList< QStringView > split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
qsizetype size() const const
This class represents a Cutelyst Action.
Definition: action.h:34
The Cutelyst Context.
Definition: context.h:42
QString number(double n, char format, int precision)
DispatchTypeChained(QObject *parent=nullptr)
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
void setAttributes(const ParamsMultiMap &attributes)
Definition: action.cpp:80
int toInt(bool *ok, int base) const const
bool isEmpty() const const
bool isEmpty() const const
QString name() const noexcept
Definition: component.cpp:33
The Cutelyst namespace holds all public Cutelyst API.
T & first()
void setCaptures(const QStringList &captures)
Definition: request.cpp:167
void push_back(QChar ch)
QString uriForAction(Action *action, const QStringList &captures) const override
void setArguments(const QStringList &arguments)
Definition: request.cpp:155
void prepend(parameter_type value)
bool contains(const Key &key) const const
QString & replace(QChar before, QChar after, Qt::CaseSensitivity cs)
QString mid(qsizetype position, qsizetype n) const const
Describes a chained dispatch type.
void append(QList< T > &&value)
Abstract class to described a dispatch type.
Definition: dispatchtype.h:24
QList< T > mid(qsizetype pos, qsizetype length) const const
A request.
Definition: request.h:41
QByteArray list() const override
QString attribute(const QString &name, const QString &defaultValue={}) const
Definition: action.cpp:74
QObject * parent() const const
T value(const Key &key, const T &defaultValue) const const
QList< T > values() const const