Skip to content

Commit ff9c216

Browse files
http: add http client
1 parent 9a9c605 commit ff9c216

8 files changed

Lines changed: 516 additions & 0 deletions

File tree

src/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ endif()
3737
if (NETWORK)
3838
add_subdirectory(network)
3939
endif()
40+
41+
add_subdirectory(http)

src/http/CMakeLists.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
qt_add_library(quickshell-http STATIC
2+
client.cpp
3+
response.cpp
4+
)
5+
6+
qt_add_qml_module(quickshell-http
7+
URI Quickshell.Http
8+
VERSION 0.1
9+
DEPENDENCIES QtQml
10+
)
11+
12+
install_qml_module(quickshell-http)
13+
14+
target_link_libraries(quickshell-http PRIVATE
15+
Qt::Qml
16+
Qt::Network
17+
)
18+
19+
qs_module_pch(quickshell-http)
20+
21+
target_link_libraries(quickshell PRIVATE quickshell-httpplugin)

src/http/client.cpp

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#include "client.hpp"
2+
3+
#include <qjsengine.h>
4+
#include <qjsvalue.h>
5+
#include <qjsvalueiterator.h>
6+
#include <qnetworkaccessmanager.h>
7+
#include <qnetworkreply.h>
8+
#include <qnetworkrequest.h>
9+
#include <qobject.h>
10+
#include <qstringview.h>
11+
#include <qtimer.h>
12+
#include <qurl.h>
13+
14+
#include "response.hpp"
15+
16+
namespace {
17+
18+
QNetworkRequest createRequest(const QString& url, const QJSValue& headers) {
19+
QNetworkRequest req;
20+
21+
req.setUrl(QUrl(url));
22+
23+
if (headers.isObject()) {
24+
QJSValueIterator iter(headers);
25+
while (iter.hasNext()) {
26+
iter.next();
27+
req.setRawHeader(iter.name().toUtf8(), iter.value().toString().toUtf8());
28+
}
29+
}
30+
31+
return req;
32+
}
33+
34+
} // namespace
35+
36+
namespace qs::http {
37+
38+
HttpClient::HttpClient(QObject* parent)
39+
: QObject(parent)
40+
, mManager(new QNetworkAccessManager(this)) {}
41+
42+
void HttpClient::connectReply(QNetworkReply* reply, const QJSValue& callback, int timeout) {
43+
if (timeout > 0) {
44+
auto* timer = new QTimer(reply);
45+
timer->setSingleShot(true);
46+
timer->setInterval(timeout);
47+
connect(timer, &QTimer::timeout, reply, &QNetworkReply::abort);
48+
timer->start();
49+
}
50+
51+
connect(reply, &QNetworkReply::finished, reply, &QNetworkReply::deleteLater);
52+
53+
QJSEngine* engine = qjsEngine(this);
54+
if (!engine || !callback.isCallable()) {
55+
return;
56+
}
57+
58+
connect(reply, &QNetworkReply::finished, this, [reply, engine, callback]() mutable {
59+
auto* resp = new HttpResponse(reply);
60+
auto jsResp = engine->newQObject(resp);
61+
QJSEngine::setObjectOwnership(resp, QJSEngine::JavaScriptOwnership);
62+
63+
callback.call(QJSValueList() << jsResp);
64+
});
65+
}
66+
67+
void HttpClient::request(
68+
const QString& url,
69+
const QString& verb,
70+
const QJSValue& options,
71+
const QJSValue& callback
72+
) {
73+
QJSValue headers;
74+
QByteArray body;
75+
int timeout = -1;
76+
77+
if (options.isObject()) {
78+
if (options.hasOwnProperty("headers")) {
79+
headers = options.property("headers");
80+
}
81+
if (options.hasOwnProperty("body")) {
82+
auto bodyProp = options.property("body");
83+
body = bodyProp.toVariant().toByteArray();
84+
}
85+
if (options.hasOwnProperty("timeout")) {
86+
auto timeoutProp = options.property("timeout");
87+
if (timeoutProp.isNumber()) {
88+
timeout = static_cast<int>(timeoutProp.toNumber());
89+
}
90+
}
91+
}
92+
93+
auto req = createRequest(url, headers);
94+
auto* reply = this->mManager->sendCustomRequest(req, verb.toUtf8(), body);
95+
96+
this->connectReply(reply, callback, timeout);
97+
}
98+
99+
void HttpClient::get(const QString& url, const QJSValue& options, const QJSValue& callback) {
100+
QJSValue headers;
101+
int timeout = -1;
102+
103+
if (options.isObject()) {
104+
if (options.hasOwnProperty("headers")) {
105+
headers = options.property("headers");
106+
}
107+
if (options.hasOwnProperty("timeout")) {
108+
auto timeoutProp = options.property("timeout");
109+
if (timeoutProp.isNumber()) {
110+
timeout = static_cast<int>(timeoutProp.toNumber());
111+
}
112+
}
113+
}
114+
115+
auto req = createRequest(url, headers);
116+
auto* reply = this->mManager->get(req);
117+
118+
this->connectReply(reply, callback, timeout);
119+
}
120+
121+
void HttpClient::post(
122+
const QString& url,
123+
const QVariant& body,
124+
const QJSValue& options,
125+
const QJSValue& callback
126+
) {
127+
auto bodyBytes = body.toByteArray();
128+
QJSValue headers;
129+
int timeout = -1;
130+
131+
if (options.isObject()) {
132+
if (options.hasOwnProperty("headers")) {
133+
headers = options.property("headers");
134+
}
135+
if (options.hasOwnProperty("timeout")) {
136+
auto timeoutProp = options.property("timeout");
137+
if (timeoutProp.isNumber()) {
138+
timeout = static_cast<int>(timeoutProp.toNumber());
139+
}
140+
}
141+
}
142+
143+
auto req = createRequest(url, headers);
144+
auto* reply = this->mManager->post(req, bodyBytes);
145+
146+
this->connectReply(reply, callback, timeout);
147+
}
148+
149+
void HttpClient::put(
150+
const QString& url,
151+
const QVariant& body,
152+
const QJSValue& options,
153+
const QJSValue& callback
154+
) {
155+
auto bodyBytes = body.toByteArray();
156+
QJSValue headers;
157+
int timeout = -1;
158+
159+
if (options.isObject()) {
160+
if (options.hasOwnProperty("headers")) {
161+
headers = options.property("headers");
162+
}
163+
if (options.hasOwnProperty("timeout")) {
164+
auto timeoutProp = options.property("timeout");
165+
if (timeoutProp.isNumber()) {
166+
timeout = static_cast<int>(timeoutProp.toNumber());
167+
}
168+
}
169+
}
170+
171+
auto req = createRequest(url, headers);
172+
auto* reply = this->mManager->put(req, bodyBytes);
173+
174+
this->connectReply(reply, callback, timeout);
175+
}
176+
177+
void HttpClient::del(const QString& url, const QJSValue& options, const QJSValue& callback) {
178+
QJSValue headers;
179+
int timeout = -1;
180+
181+
if (options.isObject()) {
182+
if (options.hasOwnProperty("headers")) {
183+
headers = options.property("headers");
184+
}
185+
if (options.hasOwnProperty("timeout")) {
186+
auto timeoutProp = options.property("timeout");
187+
if (timeoutProp.isNumber()) {
188+
timeout = static_cast<int>(timeoutProp.toNumber());
189+
}
190+
}
191+
}
192+
193+
auto req = createRequest(url, headers);
194+
auto* reply = this->mManager->deleteResource(req);
195+
196+
this->connectReply(reply, callback, timeout);
197+
}
198+
199+
void HttpClient::head(const QString& url, const QJSValue& options, const QJSValue& callback) {
200+
QJSValue headers;
201+
int timeout = -1;
202+
203+
if (options.isObject()) {
204+
if (options.hasOwnProperty("headers")) {
205+
headers = options.property("headers");
206+
}
207+
if (options.hasOwnProperty("timeout")) {
208+
auto timeoutProp = options.property("timeout");
209+
if (timeoutProp.isNumber()) {
210+
timeout = static_cast<int>(timeoutProp.toNumber());
211+
}
212+
}
213+
}
214+
215+
auto req = createRequest(url, headers);
216+
auto* reply = this->mManager->head(req);
217+
218+
this->connectReply(reply, callback, timeout);
219+
}
220+
221+
} // namespace qs::http

src/http/client.hpp

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
#pragma once
2+
3+
#include <qcontainerfwd.h>
4+
#include <qjsvalue.h>
5+
#include <qnetworkaccessmanager.h>
6+
#include <qnetworkreply.h>
7+
#include <qnetworkrequest.h>
8+
#include <qobject.h>
9+
#include <qqmlintegration.h>
10+
#include <qtmetamacros.h>
11+
12+
namespace qs::http {
13+
14+
class HttpClient: public QObject {
15+
Q_OBJECT;
16+
QML_ELEMENT;
17+
QML_SINGLETON;
18+
19+
public:
20+
explicit HttpClient(QObject* parent = nullptr);
21+
22+
void connectReply(QNetworkReply* reply, const QJSValue& callback, int timeout);
23+
24+
Q_INVOKABLE void request(
25+
const QString& url,
26+
const QString& verb,
27+
const QJSValue& options,
28+
const QJSValue& callback = QJSValue()
29+
);
30+
Q_INVOKABLE void
31+
get(const QString& url, const QJSValue& options, const QJSValue& callback = QJSValue());
32+
Q_INVOKABLE void post(
33+
const QString& url,
34+
const QVariant& body,
35+
const QJSValue& options,
36+
const QJSValue& callback = QJSValue()
37+
);
38+
Q_INVOKABLE void
39+
put(const QString& url,
40+
const QVariant& body,
41+
const QJSValue& options,
42+
const QJSValue& callback = QJSValue());
43+
// cannot use delete since it's a reserved keyword
44+
Q_INVOKABLE void
45+
del(const QString& url, const QJSValue& options, const QJSValue& callback = QJSValue());
46+
Q_INVOKABLE void
47+
head(const QString& url, const QJSValue& options, const QJSValue& callback = QJSValue());
48+
49+
private:
50+
QNetworkAccessManager* mManager;
51+
};
52+
53+
} // namespace qs::http

src/http/modules.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
name = "Quickshell.Http"
2+
description = "HTTP fetch API"
3+
headers = [
4+
"client.hpp",
5+
"response.hpp",
6+
]
7+
-----
8+
Quickshell's HTTP module.

src/http/response.cpp

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
#include "response.hpp"
2+
3+
#include <qcontainerfwd.h>
4+
#include <qjsonarray.h> // NOLINT(misc-include-cleaner)
5+
#include <qjsondocument.h>
6+
#include <qjsonvalue.h>
7+
#include <qnetworkreply.h>
8+
#include <qnetworkrequest.h>
9+
#include <qobject.h>
10+
#include <qqmlinfo.h>
11+
12+
namespace qs::http {
13+
14+
HttpResponse::HttpResponse(QNetworkReply* reply, QObject* parent): QObject(parent) {
15+
this->mStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
16+
this->mStatusText = reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString();
17+
this->mUrl = reply->url();
18+
this->mData = reply->readAll();
19+
20+
auto replyErr = reply->error();
21+
this->mTimeouted =
22+
replyErr == QNetworkReply::TimeoutError || replyErr == QNetworkReply::OperationCanceledError;
23+
24+
auto headersList = reply->rawHeaderList();
25+
for (auto& header: headersList) {
26+
this->mHeadersMap.insert(
27+
QString::fromUtf8(header),
28+
QString::fromUtf8(reply->rawHeader(header))
29+
);
30+
}
31+
}
32+
33+
QUrl HttpResponse::url() { return this->mUrl; }
34+
35+
QString HttpResponse::text() { return QString::fromUtf8(this->mData); }
36+
37+
QJsonValue HttpResponse::json() {
38+
QJsonParseError error; // NOLINT(misc-include-cleaner)
39+
auto json = QJsonDocument::fromJson(this->mData, &error);
40+
41+
if (error.error != QJsonParseError::NoError) {
42+
qmlWarning(this) << "Failed to deserialize json: " << error.errorString();
43+
return QJsonValue::Undefined;
44+
}
45+
46+
if (json.isArray()) {
47+
return json.array();
48+
}
49+
50+
return json.object();
51+
}
52+
53+
QByteArray HttpResponse::arrayBuffer() { return this->mData; }
54+
55+
QVariantMap HttpResponse::headers() { return this->mHeadersMap; }
56+
57+
QString HttpResponse::header(const QString& name) {
58+
return this->mHeadersMap.value(name).toString();
59+
}
60+
61+
} // namespace qs::http

0 commit comments

Comments
 (0)