Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

104 changes: 84 additions & 20 deletions yerpc-derive/src/ts.rs → yerpc-derive/src/bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@ use crate::{util::extract_result_ty, Inputs, RpcInfo};
use convert_case::{Case, Casing};
use proc_macro2::TokenStream;
use quote::quote;
pub(crate) fn generate_typescript_generator(info: &RpcInfo, outdir_path: &String) -> TokenStream {
pub(crate) fn generate_bindings_impl(
info: &RpcInfo,
ts_outdir: &Option<String>,
qt_outdir: &Option<String>,
) -> TokenStream {
let mut gen_types = vec![];
let mut gen_methods = vec![];
let mut gen_methods_ts = vec![];
let mut gen_methods_qt = vec![];
for method in &info.methods {
let (is_positional, gen_args) = match &method.input {
Inputs::Positional(ref inputs) => {
Expand Down Expand Up @@ -49,40 +54,53 @@ pub(crate) fn generate_typescript_generator(info: &RpcInfo, outdir_path: &String
} else {
quote!(None)
};
gen_methods.push(quote!(
gen_methods_ts.push(quote!(
let args = vec![#(#gen_args),*];
let method = Method::new(#ts_name, #rpc_name, args, #gen_output, #is_notification, #is_positional, #docs);
out.push_str(&method.to_string(root_namespace));
out.push_str(&method.to_string_ts(root_namespace));
));
gen_methods_qt.push(quote!(
let args = vec![#(#gen_args),*];
let method = Method::new(#ts_name, #rpc_name, args, #gen_output, #is_notification, #is_positional, #docs);
out.push_str(&method.to_string_qt());
));
}

let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let outdir = std::path::PathBuf::from(&manifest_dir).join(outdir_path);
let outdir = outdir.to_str().unwrap();

let ts_base = include_str!("client.ts");

let mut all_types: Vec<String> = gen_types
.clone()
.into_iter()
.map(|ts| ts.to_string())
.collect();
let mut all_types: Vec<String> = gen_types.iter().map(|ts| ts.to_string()).collect();
all_types.sort();
all_types.dedup();
let all_types: Vec<TokenStream> = all_types.into_iter().map(|s| s.parse().unwrap()).collect();

let ts = if let Some(outdir) = ts_outdir {
ts_impl(&outdir, &all_types, &gen_methods_ts)
} else {
quote!(None)
};
let qt = if let Some(outdir) = qt_outdir {
qt_impl(&outdir, &all_types, &gen_methods_qt)
} else {
quote!()
};
quote! {
#ts
#qt
}
}

fn ts_impl(outdir: &str, all_types: &[TokenStream], gen_methods: &[TokenStream]) -> TokenStream {
let ts_base = include_str!("client.ts");

quote! {
/// Generate typescript bindings for the JSON-RPC API.
#[cfg(test)]
#[test]
fn generate_ts_bindings() {
pub fn generate_ts_bindings() {
use ::yerpc::typescript::type_def::{TypeDef, type_expr::TypeInfo, DefinitionFileOptions};
use ::yerpc::typescript::{typedef_to_expr_string, export_types_to_file, Method};
use ::yerpc::{method::Method, typescript::{typedef_to_expr_string, export_types_to_file}};
use ::std::{fs, path::Path};
use ::std::io::Write;

// Create output directory.
let outdir = Path::new(#outdir);
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let outdir = std::path::PathBuf::from(&manifest_dir).join(#outdir);
fs::create_dir_all(&outdir).expect(&format!("Failed to create directory `{}`", outdir.display()));

// Create helper type with all exported types.
Expand Down Expand Up @@ -117,3 +135,49 @@ pub(crate) fn generate_typescript_generator(info: &RpcInfo, outdir_path: &String
}
}
}

fn qt_impl(outdir: &str, all_types: &[TokenStream], gen_methods: &[TokenStream]) -> TokenStream {
let qt_base = include_str!("client.hpp");
quote! {
/// Generate typescript bindings for the JSON-RPC API.
pub fn generate_qt_bindings() {
use ::yerpc::typescript::type_def::{TypeDef, type_expr::TypeInfo, DefinitionFileOptions};
use ::yerpc::{method::Method, qt::export_types_to_file};
use ::std::{fs, path::Path};
use ::std::io::Write;

// Create output directory.
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");
let outdir = std::path::PathBuf::from(&manifest_dir).join(#outdir);
fs::create_dir_all(&outdir).expect(&format!("Failed to create directory `{}`", outdir.display()));

// Create helper type with all exported types.
// #(#gen_definitions)*
#[derive(TypeDef)]
struct __AllTyps(#(#all_types),*);
// Write typescript types to file.
export_types_to_file::<__AllTyps>(&outdir.join("types.hpp")).expect("Failed to write Qt out");
// remove __AllTyps type from output,
// it's only used as a woraround to export all types and is not needed anymore now
let new_content = {
let string =
::std::fs::read_to_string(&outdir.join("types.hpp")).expect("Failed to find Qt out");
if let Some(index) = string.find("using __AllTyps = ") {
string[..index].to_string()
} else {
panic!("did not find __AllTyps in Qt out");
}
};
::std::fs::File::create(&outdir.join("types.hpp"))
.expect("failed to open Qt out")
.write_all(new_content.as_bytes())
.expect("removing __AllTyps from Qt failed");

// // Generate a raw client.
let mut out = String::new();
#(#gen_methods)*
let qt_header = #qt_base.replace("#methods", &out);
fs::write(&outdir.join("client.hpp"), &qt_header).expect("Failed to write Qt bindings");
}
}
}
79 changes: 79 additions & 0 deletions yerpc-derive/src/client.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#pragma once

#include "types.hpp"

#include <QString>
#include <QJsonValue>
#include <QJsonObject>
#include <QJsonArray>
#include <QJsonDocument>

#include <future>

template<typename T>
struct Result {
T result;
QString error_message;
int32_t error_code = 0;
};

template<>
struct Result<void> {
QString error_message;
int32_t error_code = 0;
};

static Result<QJsonValue> 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<Result<QJsonValue>> send(const QString method, const QJsonValue request) = 0;
// virtual void send_notify(const QJsonValue request) = 0; not implemented
};

class RawClient {
Transport* transport_;

template <typename T>
std::future<Result<T>> request(const QString method,
const QJsonArray params) {
std::future<Result<QJsonValue>> inner = transport_->send(method, params);
return std::async(
std::launch::deferred,
[method, inner = std::move(inner)]() mutable -> Result<T> {
auto val = inner.get();
if constexpr (std::is_void_v<T>) {
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) {}

#methods
};
17 changes: 9 additions & 8 deletions yerpc-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, Item};

mod bindings;
#[cfg(feature = "openrpc")]
mod openrpc;
mod parse;
mod rpc;
mod ts;
pub(crate) use bindings::generate_bindings_impl;
pub(crate) use parse::{Inputs, RpcInfo};
pub(crate) use rpc::generate_rpc_impl;
pub(crate) use ts::generate_typescript_generator;
pub(crate) mod util;

/// Generates the jsonrpc handler and types.
Expand All @@ -28,6 +28,8 @@ pub(crate) mod util;
/// which will be parsed as a tuple of this function's arguments.
/// - `ts_outdir: Option<String>` Set the path where typescript definitions are written to (relative to the crate root).
/// If not set, no typescript definitions will be written.
/// - `qt_outdir: Option<String>` Set the path where openrpc definitions will be written to (relative to the crate root).
/// If not set, no openrpc definitions will be written.
/// - `openrpc_outdir: Option<String>` Set the path where openrpc specification file will be written to (relative to the crate root).
/// If not set, no openrpc definition file will be written.
///
Expand Down Expand Up @@ -62,11 +64,7 @@ pub fn rpc(attr: TokenStream, tokens: TokenStream) -> TokenStream {
}

let info = RpcInfo::from_impl(&attr_args, input);
let ts_impl = if let Some(outdir) = attr_args.ts_outdir.as_ref() {
generate_typescript_generator(&info,outdir)
} else {
quote!()
};
let bindings_impl = generate_bindings_impl(&info, &attr_args.ts_outdir, &attr_args.qt_outdir);
let rpc_impl = generate_rpc_impl(&info);

#[cfg(feature = "openrpc")]
Expand All @@ -82,7 +80,7 @@ pub fn rpc(attr: TokenStream, tokens: TokenStream) -> TokenStream {
quote! {
#item
#rpc_impl
#ts_impl
#bindings_impl
#openrpc_impl
}
}
Expand All @@ -105,6 +103,9 @@ pub(crate) struct RootAttrArgs {
/// Set the path where typescript definitions are written to (relative to the crate root).
/// If not set, no typescript definitions will be written
ts_outdir: Option<String>,
/// Set the path where qt json definitions are written to (relative to the crate root).
/// If not set, no qt json definitions will be written
qt_outdir: Option<String>,
/// Set the path where openrpc definitions will be written to (relative to the crate root).
/// If not set, no openrpc definitions will be written.
openrpc_outdir: Option<String>,
Expand Down
1 change: 1 addition & 0 deletions yerpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,4 @@ axum = { version = "0.8.1", features = ["ws"] }
tokio-tungstenite = { version = "0.26.1" }
tokio = { version = "1.43.0", features = ["rt", "macros"] }
url = "2.5.4"
insta = "1.47.2"
Loading
Loading