From bf06697b38de2019d43d5199cd544c8c14732cc5 Mon Sep 17 00:00:00 2001 From: d2weber <29163905+d2weber@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:11:51 +0200 Subject: [PATCH 1/2] fix: readd generated code In #78 the `#[test]` generation has been removed, this left the folders `yerpc/tests/{qt,typescript}` stale. This PR uses [insta](https://docs.rs/insta/latest/insta/index.html) to test the generated code. When using insta, a test run will make sure the snapshots are always in sync (as opposed to the former approach, where updates were silently overwritten) --- Cargo.lock | 1 + yerpc/Cargo.toml | 1 + yerpc/qt/generated/client.hpp | 97 ---------- yerpc/tests/axum.rs | 36 ++++ .../axum__tests__qt__client_hpp.snap | 182 ++++++++++++++++++ .../axum__tests__qt__types_hpp.snap} | 9 +- .../axum__tests__typescript__client_ts.snap} | 4 + .../axum__tests__typescript__jsonrpc_ts.snap} | 4 + .../axum__tests__typescript__types_ts.snap} | 4 + 9 files changed, 240 insertions(+), 98 deletions(-) delete mode 100644 yerpc/qt/generated/client.hpp create mode 100644 yerpc/tests/snapshots/axum__tests__qt__client_hpp.snap rename yerpc/{qt/generated/types.hpp => tests/snapshots/axum__tests__qt__types_hpp.snap} (90%) rename yerpc/{typescript/generated/client.ts => tests/snapshots/axum__tests__typescript__client_ts.snap} (92%) rename yerpc/{typescript/generated/jsonrpc.ts => tests/snapshots/axum__tests__typescript__jsonrpc_ts.snap} (96%) rename yerpc/{typescript/generated/types.ts => tests/snapshots/axum__tests__typescript__types_ts.snap} (54%) diff --git a/Cargo.lock b/Cargo.lock index fd20d18..9cb1f3f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3307,6 +3307,7 @@ dependencies = [ "schemars", "serde", "serde_json", + "tempfile", "tokio", "tokio-tungstenite 0.26.2", "tracing", diff --git a/yerpc/Cargo.toml b/yerpc/Cargo.toml index 6dd72de..66457d6 100644 --- a/yerpc/Cargo.toml +++ b/yerpc/Cargo.toml @@ -50,3 +50,4 @@ tokio-tungstenite = { version = "0.26.1" } tokio = { version = "1.43.0", features = ["rt", "macros"] } url = "2.5.4" insta = "1.47.2" +tempfile = "3.27.0" diff --git a/yerpc/qt/generated/client.hpp b/yerpc/qt/generated/client.hpp deleted file mode 100644 index c3f070d..0000000 --- a/yerpc/qt/generated/client.hpp +++ /dev/null @@ -1,97 +0,0 @@ -#pragma once - -#include "types.hpp" - -#include -#include -#include -#include -#include - -#include - -template -struct Result { - T result; - QString error_message; - int32_t error_code = 0; -}; - -template<> -struct Result { - QString error_message; - int32_t error_code = 0; -}; - -static Result parseResult(const QJsonObject& val) { - if (val.contains("error")) { - QJsonObject err = val["error"].toObject(); - if (err.isEmpty()) - return {{}, "Invalid error in response: " + QJsonDocument(val).toJson(QJsonDocument::Compact), -32700}; - return {{}, err["message"].toString(), err["code"].toInt()}; - } - if (!val.contains("result")) - return {{}, "Neither error nor result in response: " + QJsonDocument(val).toJson(QJsonDocument::Compact), -32700}; - return {val["result"], {}, 0}; -} - -class Transport { -public: - virtual std::future> send(const QString method, const QJsonValue request) = 0; - // virtual void send_notify(const QJsonValue request) = 0; not implemented -}; - -class RawClient { - Transport* transport_; - - template - std::future> request(const QString method, - const QJsonArray params) { - std::future> inner = transport_->send(method, params); - return std::async( - std::launch::deferred, - [method, inner = std::move(inner)]() mutable -> Result { - auto val = inner.get(); - if constexpr (std::is_void_v) { - if (val.error_code) - return {method + ": " + val.error_message, val.error_code}; - return {{}, 0}; - } else { - if (val.error_code) - return {{}, method + ": " + val.error_message, val.error_code}; - T out; - if (!tryFromJson(val.result, out)) { - return {{}, - method + ": Could not parse result " + - QJsonDocument(QJsonArray{val.result}) - .toJson(QJsonDocument::Compact), - -32700}; - } - return {out, {}, 0}; - } - }); - } -public: - RawClient(Transport* transport) - : transport_(transport) {} - - - std::future> shoutAsync(QString msg) { - return request("shout", QJsonArray{toJson(msg)}); - } - - QString shout(QString msg) { - return request("shout", QJsonArray{toJson(msg)}).get().result; - } - - - std::future> addAsync(float a, float b) { - return request("add", QJsonArray{toJson(a), toJson(b)}); - } - - float add(float a, float b) { - return request("add", QJsonArray{toJson(a), toJson(b)}).get().result; - } - - -}; diff --git a/yerpc/tests/axum.rs b/yerpc/tests/axum.rs index 2420c5f..130fbaf 100644 --- a/yerpc/tests/axum.rs +++ b/yerpc/tests/axum.rs @@ -76,4 +76,40 @@ mod tests { assert_eq!(res.as_str(), "FOO"); Ok(()) } + + use std::path::Path; + pub fn assert_dir_snapshot(prefix: &str, f: impl FnOnce(&Path)) { + let dir = tempfile::tempdir().unwrap(); + f(dir.path()); + assert_dir_snapshot_inner(prefix, dir.path()); + } + + fn assert_dir_snapshot_inner(prefix: &str, dir: &Path) { + let mut entries: Vec<_> = std::fs::read_dir(dir) + .unwrap() + .map(|e| e.unwrap().path()) + .collect(); + entries.sort(); + + for file_path in entries { + let relative = file_path + .strip_prefix(dir) + .unwrap() + .to_string_lossy() + .replace('.', "_"); + let contents = std::fs::read_to_string(&file_path).unwrap(); + let snapshot_name = format!("{prefix}__{relative}"); + insta::assert_snapshot!(snapshot_name, contents); + } + } + + #[test] + fn ts_bindings() { + assert_dir_snapshot("typescript", |p| write_ts_bindings(p)) + } + + #[test] + fn qt_bindings() { + assert_dir_snapshot("qt", |p| write_qt_bindings(p, "my_namespace")) + } } diff --git a/yerpc/tests/snapshots/axum__tests__qt__client_hpp.snap b/yerpc/tests/snapshots/axum__tests__qt__client_hpp.snap new file mode 100644 index 0000000..1297a82 --- /dev/null +++ b/yerpc/tests/snapshots/axum__tests__qt__client_hpp.snap @@ -0,0 +1,182 @@ +--- +source: yerpc/tests/axum.rs +expression: contents +--- +#pragma once + +#include "types.hpp" + +#include +#include +#include +#include +#include +#include + +#include +#include + +namespace my_namespace { + +struct ResultError { + /** Error code of the result. Equals `0` if there is no error. */ + int32_t error_code = 0; + + /** Error message of the result. Equals an empty string if there is no error. + */ + QString error_message{}; + + /** Returns `true` if the result represents an error. */ + bool isError() const { return error_code != 0; } + + /** + * Explicitly discards the result. Used to silence `[[nodiscard]]` warnings + */ + void ignore() const {} +}; + +template struct [[nodiscard]] Result : ResultError { + /* Constructs a successful result containing the given value */ + static Result ok(T value) { return {{0, {}}, std::move(value)}; } + + /* Constructs an error result with the given error code and message */ + static Result error(int32_t error_code, QString error_message) { + return {{error_code, std::move(error_message)}, {}}; + } + + /** Value of the result. Is a default constructed value if the result + * represents an error. */ + T value{}; + + /** + * If the result represents an error, logs a warning with the error details + * and the caller's source location. Returns a copy of this result for + * chaining. + */ + Result logError(const char *file = __builtin_FILE(), + int line = __builtin_LINE()) const { + if (error_code) { + qWarning().noquote() + << QStringLiteral("Result::logError() from %1:%2 | Error %3: %4") + .arg(QLatin1String(file)) + .arg(line) + .arg(error_code) + .arg(error_message); + } + return *this; + } + + /** Return the `value` converted to json */ + QJsonValue valueToJson() const { return toJson(value); } +}; + +template <> struct [[nodiscard]] Result : ResultError { + /* Constructs a successful result */ + static Result ok() { return {{0, {}}}; } + + /* Constructs an error result with the given error code and message */ + static Result error(int32_t error_code, QString error_message) { + return {{error_code, std::move(error_message)}}; + } + + /** + * If the result represents an error, logs a warning with the error details + * and the caller's source location. Returns a copy of this result for + * chaining. + */ + Result logError(const char *file = __builtin_FILE(), + int line = __builtin_LINE()) const { + if (error_code) { + qWarning().noquote() + << QStringLiteral("Result::logError() from %1:%2 | Error %3: %4") + .arg(QLatin1String(file)) + .arg(line) + .arg(error_code) + .arg(error_message); + } + return *this; + } +}; + +static Result parseResult(const QJsonObject &val) { + if (val.contains("error")) { + QJsonObject err = val["error"].toObject(); + QJsonValue error_message = err["message"]; + int error_code = err["code"].toInt(); + if (!error_message.isString() || error_code == 0) + return Result::error( + -32700, "Invalid error in response: " + + QJsonDocument(val).toJson(QJsonDocument::Compact)); + return Result::error(error_code, error_message.toString()); + } + if (!val.contains("result")) + return Result::error( + -32700, "Neither error nor result in response: " + + QJsonDocument(val).toJson(QJsonDocument::Compact)); + return Result::ok(val["result"]); +} + +class Transport { +public: + using CompletionHandler = std::function)>; + virtual void send(const QString method, const QJsonValue request, + CompletionHandler onCompleted) = 0; + virtual ~Transport() = default; +}; + +class RawClient { + template + QFuture> request(const QString method, const QJsonArray params) { + QFutureInterface> interface; + interface.reportStarted(); + transport_->send(method, params, + [method, interface](const Result val) mutable { + interface.reportResult( + mapToConcreteType(val, method)); + interface.reportFinished(); + }); + return interface.future(); + } + + template + static Result mapToConcreteType(const Result val, + const QString method) { + if constexpr (std::is_void_v) { + if (val.error_code) + return Result::error(val.error_code, + method + ": " + val.error_message); + return Result::ok(); + } else { + if (val.error_code) + return Result::error(val.error_code, + method + ": " + val.error_message); + T out; + if (!tryFromJson(val.value, out)) { + return Result::error(-32700, + method + ": Could not parse result " + + QJsonDocument(QJsonArray{val.value}) + .toJson(QJsonDocument::Compact)); + } + return Result::ok(out); + } + } + +public: + RawClient(std::unique_ptr t) : transport_{std::move(t)} {} + + std::unique_ptr transport_; + + + [[nodiscard]] QFuture> shout(QString msg) { + return request("shout", QJsonArray{toJson(msg)}); + } + + + [[nodiscard]] QFuture> add(float a, float b) { + return request("add", QJsonArray{toJson(a), toJson(b)}); + } + + +}; + +} diff --git a/yerpc/qt/generated/types.hpp b/yerpc/tests/snapshots/axum__tests__qt__types_hpp.snap similarity index 90% rename from yerpc/qt/generated/types.hpp rename to yerpc/tests/snapshots/axum__tests__qt__types_hpp.snap index a9a4f8c..e6d6c1b 100644 --- a/yerpc/qt/generated/types.hpp +++ b/yerpc/tests/snapshots/axum__tests__qt__types_hpp.snap @@ -1,16 +1,21 @@ +--- +source: yerpc/tests/axum.rs +expression: contents +--- // AUTO-GENERATED by yerpc-derive #pragma once #include #include -#include #include #include #include #include +namespace my_namespace { + namespace Hidden { template struct overloaded : Ts... { using Ts::operator()...; }; template overloaded(Ts...) -> overloaded; @@ -29,3 +34,5 @@ inline bool tryFromJson(const QJsonValue &v, float &out) { out = static_cast(v.toDouble()); return true; } + +} diff --git a/yerpc/typescript/generated/client.ts b/yerpc/tests/snapshots/axum__tests__typescript__client_ts.snap similarity index 92% rename from yerpc/typescript/generated/client.ts rename to yerpc/tests/snapshots/axum__tests__typescript__client_ts.snap index 42ede3a..a614511 100644 --- a/yerpc/typescript/generated/client.ts +++ b/yerpc/tests/snapshots/axum__tests__typescript__client_ts.snap @@ -1,3 +1,7 @@ +--- +source: yerpc/tests/axum.rs +expression: contents +--- // AUTO-GENERATED by yerpc-derive import * as T from "./types.js" diff --git a/yerpc/typescript/generated/jsonrpc.ts b/yerpc/tests/snapshots/axum__tests__typescript__jsonrpc_ts.snap similarity index 96% rename from yerpc/typescript/generated/jsonrpc.ts rename to yerpc/tests/snapshots/axum__tests__typescript__jsonrpc_ts.snap index 89ae9ed..653cb34 100644 --- a/yerpc/typescript/generated/jsonrpc.ts +++ b/yerpc/tests/snapshots/axum__tests__typescript__jsonrpc_ts.snap @@ -1,3 +1,7 @@ +--- +source: yerpc/tests/axum.rs +expression: contents +--- // AUTO-GENERATED by typescript-type-def export type JSONValue = (null | boolean | number | string | (JSONValue)[] | { diff --git a/yerpc/typescript/generated/types.ts b/yerpc/tests/snapshots/axum__tests__typescript__types_ts.snap similarity index 54% rename from yerpc/typescript/generated/types.ts rename to yerpc/tests/snapshots/axum__tests__typescript__types_ts.snap index 620b4a5..ee4f923 100644 --- a/yerpc/typescript/generated/types.ts +++ b/yerpc/tests/snapshots/axum__tests__typescript__types_ts.snap @@ -1,3 +1,7 @@ +--- +source: yerpc/tests/axum.rs +expression: contents +--- // AUTO-GENERATED by typescript-type-def export type F32 = number; From 844b6c4ca1e3917fea6e042535a2088485268d88 Mon Sep 17 00:00:00 2001 From: d2weber <29163905+d2weber@users.noreply.github.com> Date: Thu, 3 Sep 2026 09:51:37 +0200 Subject: [PATCH 2/2] fixup: move import --- yerpc/tests/axum.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yerpc/tests/axum.rs b/yerpc/tests/axum.rs index 130fbaf..301dd1d 100644 --- a/yerpc/tests/axum.rs +++ b/yerpc/tests/axum.rs @@ -3,6 +3,7 @@ mod tests { use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Router}; use futures_util::{SinkExt, StreamExt}; use std::net::SocketAddr; + use std::path::Path; use tokio::net::TcpStream; use tokio_tungstenite::client_async; use tokio_tungstenite::tungstenite::http::StatusCode; @@ -77,7 +78,6 @@ mod tests { Ok(()) } - use std::path::Path; pub fn assert_dir_snapshot(prefix: &str, f: impl FnOnce(&Path)) { let dir = tempfile::tempdir().unwrap(); f(dir.path());