diff --git a/Cargo.lock b/Cargo.lock index 7b26af7..fd20d18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -620,6 +620,17 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "console" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87" +dependencies = [ + "encode_unicode", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "const_fn" version = "0.4.12" @@ -804,6 +815,12 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "env_logger" version = "0.9.3" @@ -1429,6 +1446,18 @@ dependencies = [ "bytes", ] +[[package]] +name = "insta" +version = "1.47.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4a6248eb93a4401ed2f37dfe8ea592d3cf05b7cf4f8efa867b6895af7e094e" +dependencies = [ + "console", + "once_cell", + "similar", + "tempfile", +] + [[package]] name = "instant" version = "0.1.13" @@ -2188,6 +2217,12 @@ dependencies = [ "libc", ] +[[package]] +name = "similar" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" + [[package]] name = "slab" version = "0.4.12" @@ -2413,6 +2448,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand 2.3.0", + "getrandom 0.3.4", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + [[package]] name = "termcolor" version = "1.4.1" @@ -3254,6 +3302,7 @@ dependencies = [ "axum", "futures", "futures-util", + "insta", "log", "schemars", "serde", diff --git a/yerpc-derive/src/ts.rs b/yerpc-derive/src/bindings.rs similarity index 54% rename from yerpc-derive/src/ts.rs rename to yerpc-derive/src/bindings.rs index 9729a07..07badcf 100644 --- a/yerpc-derive/src/ts.rs +++ b/yerpc-derive/src/bindings.rs @@ -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, + qt_outdir: &Option, +) -> 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) => { @@ -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 = gen_types - .clone() - .into_iter() - .map(|ts| ts.to_string()) - .collect(); + let mut all_types: Vec = gen_types.iter().map(|ts| ts.to_string()).collect(); all_types.sort(); all_types.dedup(); let all_types: Vec = 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. @@ -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"); + } + } +} diff --git a/yerpc-derive/src/client.hpp b/yerpc-derive/src/client.hpp new file mode 100644 index 0000000..fa751d4 --- /dev/null +++ b/yerpc-derive/src/client.hpp @@ -0,0 +1,79 @@ +#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) {} + +#methods +}; diff --git a/yerpc-derive/src/lib.rs b/yerpc-derive/src/lib.rs index 4d076be..1f41462 100644 --- a/yerpc-derive/src/lib.rs +++ b/yerpc-derive/src/lib.rs @@ -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. @@ -28,6 +28,8 @@ pub(crate) mod util; /// which will be parsed as a tuple of this function's arguments. /// - `ts_outdir: Option` 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` 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` 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. /// @@ -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")] @@ -82,7 +80,7 @@ pub fn rpc(attr: TokenStream, tokens: TokenStream) -> TokenStream { quote! { #item #rpc_impl - #ts_impl + #bindings_impl #openrpc_impl } } @@ -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, + /// 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, /// 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, diff --git a/yerpc/Cargo.toml b/yerpc/Cargo.toml index 8c35531..6dd72de 100644 --- a/yerpc/Cargo.toml +++ b/yerpc/Cargo.toml @@ -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" diff --git a/yerpc/qt/generated/client.hpp b/yerpc/qt/generated/client.hpp new file mode 100644 index 0000000..c3f070d --- /dev/null +++ b/yerpc/qt/generated/client.hpp @@ -0,0 +1,97 @@ +#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/qt/generated/types.hpp b/yerpc/qt/generated/types.hpp new file mode 100644 index 0000000..a9a4f8c --- /dev/null +++ b/yerpc/qt/generated/types.hpp @@ -0,0 +1,31 @@ +// AUTO-GENERATED by yerpc-derive + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace Hidden { + template struct overloaded : Ts... { using Ts::operator()...; }; + template overloaded(Ts...) -> overloaded; +} + +inline QJsonValue toJson(const QString &o) { return {o}; } +inline bool tryFromJson(const QJsonValue &v, QString &out) { + if (!v.isString()) return false; + out = v.toString(); + return true; +} + +inline QJsonValue toJson(const float &o) { return static_cast(o); } +inline bool tryFromJson(const QJsonValue &v, float &out) { + if (!v.isDouble()) return false; + out = static_cast(v.toDouble()); + return true; +} diff --git a/yerpc/src/lib.rs b/yerpc/src/lib.rs index 0a126b2..a5da6f8 100644 --- a/yerpc/src/lib.rs +++ b/yerpc/src/lib.rs @@ -5,9 +5,12 @@ use serde::{Deserialize, Serialize}; pub use yerpc_derive::rpc; +pub mod method; #[cfg(feature = "openrpc")] pub mod openrpc; +pub mod qt; mod requests; +mod type_info; pub mod typescript; mod version; diff --git a/yerpc/src/method.rs b/yerpc/src/method.rs new file mode 100644 index 0000000..90fe83c --- /dev/null +++ b/yerpc/src/method.rs @@ -0,0 +1,33 @@ +use typescript_type_def::type_expr::TypeInfo; + +pub struct Method { + pub is_notification: bool, + pub is_positional: bool, + pub ts_name: String, + pub rpc_name: String, + pub args: Vec<(String, &'static TypeInfo)>, + pub output: Option<&'static TypeInfo>, + pub docs: Option, +} + +impl Method { + pub fn new( + ts_name: &str, + rpc_name: &str, + args: Vec<(String, &'static TypeInfo)>, + output: Option<&'static TypeInfo>, + is_notification: bool, + is_positional: bool, + docs: Option<&str>, + ) -> Self { + Self { + ts_name: ts_name.to_string(), + rpc_name: rpc_name.to_string(), + args, + output, + is_notification, + is_positional, + docs: docs.map(|d| d.to_string()), + } + } +} diff --git a/yerpc/src/qt.rs b/yerpc/src/qt.rs new file mode 100644 index 0000000..11589ff --- /dev/null +++ b/yerpc/src/qt.rs @@ -0,0 +1,542 @@ +use crate::method::Method; +use crate::type_info::{Field, Number, TypeInfo, Variant}; +use std::{collections::HashSet, fmt::Write}; +use std::{io, path::Path}; +use typescript_type_def::TypeDef; + +impl TypeInfo { + fn qt_type(&self) -> String { + match self { + TypeInfo::String => "QString".to_owned(), + TypeInfo::Bool => "bool".to_owned(), + TypeInfo::Number(n) => match n { + Number::I8 => "int8_t", + Number::I16 => "int16_t", + Number::I32 => "int32_t", + Number::I64 => "int64_t", + Number::U8 => "uint8_t", + Number::U16 => "uint16_t", + Number::U32 => "uint32_t", + Number::U64 => "uint64_t", + Number::F32 => "float", + Number::F64 => "double", + } + .to_owned(), + TypeInfo::Optional(o) => { + format!("std::optional<{}>", o.qt_type()) + } + TypeInfo::Array(a) => { + format!("QList<{}>", a.qt_type()) + } + TypeInfo::Map(v) => { + format!("QMap", v.qt_type()) + } + TypeInfo::Void => "void".to_owned(), + TypeInfo::Struct { name, .. } + | TypeInfo::TaggedEnum { name, .. } + | TypeInfo::StringEnum { name, .. } => name.clone(), + TypeInfo::Tuple { name, elements } => name.clone().unwrap_or_else(|| { + format!( + "std::tuple<{}>", + join_with_comma(elements.iter().map(Self::qt_type)) + ) + }), + } + } +} + +pub fn export_types_to_file(path: &Path) -> io::Result<()> { + let mut output = r#"// AUTO-GENERATED by yerpc-derive + +#pragma once + +#include +#include +#include + +#include +#include +#include +#include + +namespace Hidden { + template struct overloaded : Ts... { using Ts::operator()...; }; + template overloaded(Ts...) -> overloaded; +} + +"# + .to_owned(); + QtJsonGenerator::new().generate(&mut output, &TypeInfo::from(&T::INFO)); + std::fs::write(path, output) +} + +#[derive(Default)] +pub struct QtJsonGenerator { + seen: HashSet, +} + +impl QtJsonGenerator { + pub fn new() -> Self { + Self::default() + } + + fn generate(&mut self, output: &mut String, ty: &TypeInfo) { + if !self.seen.insert(ty.qt_type()) { + return; + }; + match ty { + TypeInfo::Struct { name, fields } => { + self.generate_cpp_struct(output, name, fields); + } + TypeInfo::TaggedEnum { name, variants } => { + self.generate_tagged_union(output, name, variants) + } + TypeInfo::StringEnum { name, variants } => { + self.generate_unit_enum(output, name, variants); + } + TypeInfo::Tuple { name, elements } => self.generate_tuple(output, name, elements), + TypeInfo::Array(v) => self.generate_array(output, v), + TypeInfo::Optional(inner) => self.generate_optional(output, inner), + TypeInfo::Map(v) => self.generate_map(output, v), + TypeInfo::Bool => generate_bool(output), + TypeInfo::Number(_) => generate_number(output, &ty.qt_type()), + TypeInfo::String => generate_string(output), + TypeInfo::Void => (), + } + } + + fn generate_tuple( + &mut self, + output: &mut String, + name: &Option, + elements: &Vec, + ) { + for e in elements { + self.generate(output, e); + } + let cpp_tuple = format!( + "std::tuple<{}>", + join_with_comma(elements.iter().map(|e| e.qt_type())) + ); + if let Some(name) = name { + writeln!(output, "using {name} = {cpp_tuple};",).unwrap(); + } + let name = name.as_ref().unwrap_or(&cpp_tuple); + writeln!(output, "inline QJsonValue toJson(const {name} &o) {{").unwrap(); + writeln!(output, " QJsonArray a;").unwrap(); + for i in 0..elements.len() { + writeln!(output, " a.push_back(toJson(std::get<{i}>(o)));").unwrap(); + } + writeln!(output, " return a;").unwrap(); + writeln!(output, "}}").unwrap(); + writeln!( + output, + "inline bool tryFromJson(const QJsonValue &v, {name} &out) {{" + ) + .unwrap(); + writeln!(output, " if (!v.isArray()) return false;").unwrap(); + writeln!(output, " QJsonArray a = v.toArray();").unwrap(); + writeln!( + output, + " if (a.size() != {}) return false;", + elements.len() + ) + .unwrap(); + for i in 0..elements.len() { + writeln!( + output, + " if (!tryFromJson(a.at({i}), std::get<{i}>(out))) return false;" + ) + .unwrap(); + } + writeln!(output, " return true;").unwrap(); + writeln!(output, "}}").unwrap(); + } + + fn generate_tagged_union(&mut self, output: &mut String, uname: &str, variants: &Vec) { + for v in variants { + for f in &v.fields { + self.generate(output, &f.ty); + } + } + for Variant { name, fields } in variants { + self.generate_cpp_struct(output, &format!("{uname}{name}"), fields); + } + writeln!( + output, + "using {uname} = std::variant<{}>;", + join_with_comma( + variants + .iter() + .map(|Variant { name, .. }| format! {"{uname}{name}"}) + ) + ) + .unwrap(); + + writeln!(output, "inline QJsonValue toJson(const {uname} &o) {{").unwrap(); + writeln!(output, " QJsonObject j;").unwrap(); + writeln!(output, " std::visit(Hidden::overloaded{{").unwrap(); + for Variant { name: vname, .. } in variants { + writeln!( + output, + r#" [&j]({uname}{vname} v){{ j = toJson(v).toObject(); j["kind"] = QString("{vname}"); }},"# + ) + .unwrap(); + } + writeln!(output, " }}, o);").unwrap(); + writeln!(output, " return j;").unwrap(); + writeln!(output, "}}").unwrap(); + + writeln!( + output, + "inline bool tryFromJson(const QJsonValue &v, {uname} &out) {{", + ) + .unwrap(); + writeln!(output, " if (!v.isObject()) return false;").unwrap(); + writeln!(output, " QJsonObject obj = v.toObject();").unwrap(); + writeln!( + output, + " if (!obj.contains(\"kind\") || !obj[\"kind\"].isString()) return false;" + ) + .unwrap(); + writeln!(output, " QString kind = obj.take(\"kind\").toString();").unwrap(); + for Variant { name, .. } in variants { + writeln!( + output, + r#" if (kind == "{name}") {{ {uname}{name} v; if (!tryFromJson(obj, v)) return false; out = v; return true; }};"# + ) + .unwrap() + } + writeln!(output, " return false;").unwrap(); + writeln!(output, "}}").unwrap(); + writeln!(output).unwrap(); + } + + fn generate_unit_enum(&mut self, output: &mut String, name: &str, variants: &[String]) { + writeln!(output, "enum class {name} {{").unwrap(); + for (i, v) in variants.iter().enumerate() { + let comma = if i + 1 < variants.len() { "," } else { "" }; + writeln!(output, " {}{}", v, comma).unwrap(); + } + writeln!(output, "}};").unwrap(); + + writeln!(output, "inline QJsonValue toJson({name} s) {{",).unwrap(); + writeln!(output, " switch (s) {{").unwrap(); + for v in variants { + writeln!(output, " case {name}::{v}: return \"{v}\";",).unwrap(); + } + writeln!(output, " }}").unwrap(); + writeln!(output, " return QJsonValue::Undefined;").unwrap(); + writeln!(output, "}}").unwrap(); + + writeln!( + output, + "inline bool tryFromJson(const QJsonValue &v, {name} &out) {{", + ) + .unwrap(); + writeln!(output, " if (!v.isString()) return false;").unwrap(); + writeln!(output, " QString s = v.toString();").unwrap(); + for v in variants { + writeln!( + output, + " if (s == \"{v}\") {{ out = {name}::{v}; return true; }}", + ) + .unwrap(); + } + writeln!(output, " return false;").unwrap(); + writeln!(output, "}}").unwrap(); + } + + fn generate_cpp_struct(&mut self, output: &mut String, sname: &str, fields: &[Field]) { + for Field { ty, .. } in fields.iter() { + self.generate(output, ty); + } + writeln!(output, "struct {sname} {{").unwrap(); + for Field { name, ty } in fields { + let ty_name = ty.qt_type(); + writeln!(output, " {ty_name} {name};").unwrap(); + } + writeln!(output, "}};").unwrap(); + writeln!(output, "inline QJsonValue toJson(const {sname} &o) {{",).unwrap(); + writeln!(output, " QJsonObject j;").unwrap(); + for Field { name, .. } in fields { + writeln!(output, " j[\"{name}\"] = toJson(o.{name});",).unwrap(); + } + writeln!(output, " return j;").unwrap(); + writeln!(output, "}}").unwrap(); + writeln!( + output, + "inline bool tryFromJson(const QJsonValue &v, {sname} &out) {{" + ) + .unwrap(); + writeln!(output, " if (!v.isObject()) return false;").unwrap(); + writeln!(output, " QJsonObject obj = v.toObject();").unwrap(); + for Field { name, .. } in fields { + writeln!( + output, + " if (!tryFromJson(obj[\"{name}\"], out.{name})) return false;", + ) + .unwrap(); + } + writeln!(output, " return true;").unwrap(); + writeln!(output, "}}").unwrap(); + } + fn generate_optional(&mut self, output: &mut String, inner: &TypeInfo) { + self.generate(output, inner); + let ty = inner.qt_type(); + writeln!(output, "inline QJsonValue toJson(const std::optional<{ty}> &o) {{ return o.has_value() ? toJson(o.value()) : QJsonValue::Undefined; }}").unwrap(); + writeln!( + output, + "inline bool tryFromJson(const QJsonValue &v, std::optional<{ty}> &out) {{" + ) + .unwrap(); + writeln!( + output, + " if (v.isUndefined() || v.isNull()) return true;" + ) + .unwrap(); + writeln!(output, " {ty} val;").unwrap(); + writeln!(output, " if (!tryFromJson(v, val)) return false;").unwrap(); + writeln!(output, " out = val;").unwrap(); + writeln!(output, " return true;").unwrap(); + writeln!(output, "}}").unwrap(); + } + fn generate_array(&mut self, output: &mut String, v: &TypeInfo) { + self.generate(output, v); + let ty = v.qt_type(); + writeln!(output, "inline QJsonValue toJson(const QList<{ty}> &o) {{").unwrap(); + writeln!(output, " QJsonArray a;").unwrap(); + writeln!(output, " for (const auto &i : o) a.append(toJson(i));").unwrap(); + writeln!(output, " return a;").unwrap(); + writeln!(output, "}}").unwrap(); + writeln!( + output, + "inline bool tryFromJson(const QJsonValue &v, QList<{ty}> &out) {{" + ) + .unwrap(); + writeln!(output, " if (!v.isArray()) return false;").unwrap(); + writeln!(output, " for (const auto &e : v.toArray()) {{").unwrap(); + writeln!(output, " {ty} val;").unwrap(); + writeln!(output, " if (!tryFromJson(e, val)) return false;").unwrap(); + writeln!(output, " out.append(val);").unwrap(); + writeln!(output, " }}").unwrap(); + writeln!(output, " return true;").unwrap(); + writeln!(output, "}}").unwrap(); + } + + fn generate_map(&mut self, output: &mut String, v: &TypeInfo) { + self.generate(output, v); + let ty = v.qt_type(); + writeln!( + output, + "inline QJsonValue toJson(const QMap &o) {{" + ) + .unwrap(); + writeln!(output, " QJsonObject j;").unwrap(); + writeln!( + output, + " for (auto it = o.constKeyValueBegin(); it != o.constKeyValueEnd(); ++it)" + ) + .unwrap(); + writeln!(output, " j[it->first] = toJson(it->second);").unwrap(); + writeln!(output, " return j;").unwrap(); + writeln!(output, "}}").unwrap(); + writeln!( + output, + "inline bool tryFromJson(const QJsonValue &v, QMap &out) {{" + ) + .unwrap(); + writeln!(output, " if (!v.isObject()) return false;").unwrap(); + writeln!(output, " auto o = v.toObject();").unwrap(); + writeln!( + output, + " for (auto it = o.constBegin(); it != o.constEnd(); ++it) {{" + ) + .unwrap(); + writeln!(output, " {ty} val;").unwrap(); + writeln!( + output, + " if (!tryFromJson(it.value(), val)) return false;" + ) + .unwrap(); + writeln!(output, " out[it.key()] = val;").unwrap(); + writeln!(output, " }}").unwrap(); + writeln!(output, " return true;").unwrap(); + writeln!(output, "}}").unwrap(); + } +} + +fn generate_bool(output: &mut String) { + output.push_str( + r#"inline QJsonValue toJson(const bool o) { return {o}; } +inline bool tryFromJson(const QJsonValue &v, bool &out) { + if (!v.isBool()) return false; + out = v.toBool(); + return true; +} +"#, + ) +} + +fn generate_string(output: &mut String) { + output.push_str( + r#"inline QJsonValue toJson(const QString &o) { return {o}; } +inline bool tryFromJson(const QJsonValue &v, QString &out) { + if (!v.isString()) return false; + out = v.toString(); + return true; +} +"#, + ) +} + +fn generate_number(output: &mut String, ty: &str) { + writeln!( + output, + r#" +inline QJsonValue toJson(const {ty} &o) {{ return static_cast(o); }} +inline bool tryFromJson(const QJsonValue &v, {ty} &out) {{ + if (!v.isDouble()) return false; + out = static_cast<{ty}>(v.toDouble()); + return true; +}}"# + ) + .unwrap() +} + +fn join_with_comma(els: impl IntoIterator) -> String { + els.into_iter() + .reduce(|a, b| a + ", " + &b) + .unwrap_or_default() +} + +#[test] +fn test_string_enum() { + #[derive(TypeDef)] + #[allow(unused)] + enum Foo { + Bar, + Baz, + } + let mut out = String::new(); + QtJsonGenerator::new().generate(&mut out, &TypeInfo::from(&Foo::INFO)); + insta::assert_snapshot!(out); +} + +#[test] +fn test_anonymous_tuple() { + #[derive(TypeDef)] + #[allow(unused)] + struct Foo(Option<(u32, String)>); + let mut out = String::new(); + QtJsonGenerator::new().generate(&mut out, &TypeInfo::from(&Foo::INFO)); + insta::assert_snapshot!(out); +} + +#[test] +fn test_tuple() { + #[derive(TypeDef)] + #[allow(unused)] + struct Foo(u32, String); + let mut out = String::new(); + QtJsonGenerator::new().generate(&mut out, &TypeInfo::from(&Foo::INFO)); + insta::assert_snapshot!(out); +} + +#[test] +fn test_nested_struct() { + #[derive(TypeDef)] + #[allow(unused)] + struct Foo { + bar: Vec, + baz: std::collections::HashMap, + } + let mut out = String::new(); + QtJsonGenerator::new().generate(&mut out, &TypeInfo::from(&Foo::INFO)); + insta::assert_snapshot!(out); +} + +#[test] +fn test_tagged_enum() { + #[derive(TypeDef)] + #[serde(tag = "kind")] + #[allow(unused)] + enum Foo { + Bar { name: String, count: u32 }, + Baz, + } + let mut out = String::new(); + QtJsonGenerator::new().generate(&mut out, &TypeInfo::from(&Foo::INFO)); + insta::assert_snapshot!(out); +} + +impl Method { + pub fn to_string_qt(&self /* root_namespace: Option<&str> */) -> String { + let (args, call) = if !self.is_positional { + if let Some((name, ty)) = self.args.first() { + ( + format!("{} {}", TypeInfo::from(*ty).qt_type(), name), + name.to_string(), + ) + } else { + ("".to_string(), "QJsonValue::Undefined".to_string()) + } + } else { + let args = self + .args + .iter() + .map(|(name, arg)| format!("{} {}", TypeInfo::from(*arg).qt_type(), name)) + .collect::>() + .join(", "); + let call = format!( + "QJsonArray{{{}}}", + self.args + .iter() + .map(|(name, _)| format!("toJson({name})")) + .collect::>() + .join(", ") + ); + (args, call) + }; + let output = self.output.map_or_else( + || "void".to_string(), + |output| TypeInfo::from(output).qt_type(), + ); + let (output, output_fut, inner_method) = if !self.is_notification { + ( + &output, + format!("std::future>"), + format!("request<{output}>"), + ) + } else { + unimplemented!() + // (output, "notification") + }; + let docs = if let Some(docs) = &self.docs { + let docs = docs.split('\n').fold(String::new(), |mut output, s| { + let _ = writeln!(output, " *{s}"); + output + }); + format!(" /**\n{docs} */") + } else { + "".into() + }; + let is_void = matches!(self.output.map(TypeInfo::from), Some(TypeInfo::Void) | None); + let unwrap_res = if is_void { ".get()" } else { ".get().result" }; + let ret = if is_void { "" } else { "return " }; + let qt_name = &self.ts_name; + let rpc_name = &self.rpc_name; + format!( + r#"{docs} + {output_fut} {qt_name}Async({args}) {{ + return {inner_method}("{rpc_name}", {call}); + }} +{docs} + {output} {qt_name}({args}) {{ + {ret}{inner_method}("{rpc_name}", {call}){unwrap_res}; + }} + +"#, + ) + } +} diff --git a/yerpc/src/snapshots/yerpc__qt__anonymous_tuple.snap b/yerpc/src/snapshots/yerpc__qt__anonymous_tuple.snap new file mode 100644 index 0000000..3a564e4 --- /dev/null +++ b/yerpc/src/snapshots/yerpc__qt__anonymous_tuple.snap @@ -0,0 +1,39 @@ +--- +source: yerpc/src/qt.rs +expression: out +--- + +inline QJsonValue toJson(const uint32_t &o) { return static_cast(o); } +inline bool tryFromJson(const QJsonValue &v, uint32_t &out) { + if (!v.isDouble()) return false; + out = static_cast(v.toDouble()); + return true; +} +inline QJsonValue toJson(const QString &o) { return {o}; } +inline bool tryFromJson(const QJsonValue &v, QString &out) { + if (!v.isString()) return false; + out = v.toString(); + return true; +} +inline QJsonValue toJson(const std::tuple &o) { + QJsonArray a; + a.push_back(toJson(std::get<0>(o))); + a.push_back(toJson(std::get<1>(o))); + return a; +} +inline bool tryFromJson(const QJsonValue &v, std::tuple &out) { + if (!v.isArray()) return false; + QJsonArray a = v.toArray(); + if (a.size() != 2) return false; + if (!tryFromJson(a.at(0), std::get<0>(out))) return false; + if (!tryFromJson(a.at(1), std::get<1>(out))) return false; + return true; +} +inline QJsonValue toJson(const std::optional> &o) { return o.has_value() ? toJson(o.value()) : QJsonValue::Undefined; } +inline bool tryFromJson(const QJsonValue &v, std::optional> &out) { + if (v.isUndefined() || v.isNull()) return true; + std::tuple val; + if (!tryFromJson(v, val)) return false; + out = val; + return true; +} diff --git a/yerpc/src/snapshots/yerpc__qt__nested_struct.snap b/yerpc/src/snapshots/yerpc__qt__nested_struct.snap new file mode 100644 index 0000000..bb3191f --- /dev/null +++ b/yerpc/src/snapshots/yerpc__qt__nested_struct.snap @@ -0,0 +1,65 @@ +--- +source: yerpc/src/qt.rs +expression: out +--- + +inline QJsonValue toJson(const uint8_t &o) { return static_cast(o); } +inline bool tryFromJson(const QJsonValue &v, uint8_t &out) { + if (!v.isDouble()) return false; + out = static_cast(v.toDouble()); + return true; +} +inline QJsonValue toJson(const QList &o) { + QJsonArray a; + for (const auto &i : o) a.append(toJson(i)); + return a; +} +inline bool tryFromJson(const QJsonValue &v, QList &out) { + if (!v.isArray()) return false; + for (const auto &e : v.toArray()) { + uint8_t val; + if (!tryFromJson(e, val)) return false; + out.append(val); + } + return true; +} + +inline QJsonValue toJson(const int8_t &o) { return static_cast(o); } +inline bool tryFromJson(const QJsonValue &v, int8_t &out) { + if (!v.isDouble()) return false; + out = static_cast(v.toDouble()); + return true; +} +inline QJsonValue toJson(const QMap &o) { + QJsonObject j; + for (auto it = o.constKeyValueBegin(); it != o.constKeyValueEnd(); ++it) + j[it->first] = toJson(it->second); + return j; +} +inline bool tryFromJson(const QJsonValue &v, QMap &out) { + if (!v.isObject()) return false; + auto o = v.toObject(); + for (auto it = o.constBegin(); it != o.constEnd(); ++it) { + int8_t val; + if (!tryFromJson(it.value(), val)) return false; + out[it.key()] = val; + } + return true; +} +struct Foo { + QList bar; + QMap baz; +}; +inline QJsonValue toJson(const Foo &o) { + QJsonObject j; + j["bar"] = toJson(o.bar); + j["baz"] = toJson(o.baz); + return j; +} +inline bool tryFromJson(const QJsonValue &v, Foo &out) { + if (!v.isObject()) return false; + QJsonObject obj = v.toObject(); + if (!tryFromJson(obj["bar"], out.bar)) return false; + if (!tryFromJson(obj["baz"], out.baz)) return false; + return true; +} diff --git a/yerpc/src/snapshots/yerpc__qt__string_enum.snap b/yerpc/src/snapshots/yerpc__qt__string_enum.snap new file mode 100644 index 0000000..7354336 --- /dev/null +++ b/yerpc/src/snapshots/yerpc__qt__string_enum.snap @@ -0,0 +1,22 @@ +--- +source: yerpc/src/qt.rs +expression: out +--- +enum class Foo { + Bar, + Baz +}; +inline QJsonValue toJson(Foo s) { + switch (s) { + case Foo::Bar: return "Bar"; + case Foo::Baz: return "Baz"; + } + return QJsonValue::Undefined; +} +inline bool tryFromJson(const QJsonValue &v, Foo &out) { + if (!v.isString()) return false; + QString s = v.toString(); + if (s == "Bar") { out = Foo::Bar; return true; } + if (s == "Baz") { out = Foo::Baz; return true; } + return false; +} diff --git a/yerpc/src/snapshots/yerpc__qt__tagged_enum.snap b/yerpc/src/snapshots/yerpc__qt__tagged_enum.snap new file mode 100644 index 0000000..3daa41b --- /dev/null +++ b/yerpc/src/snapshots/yerpc__qt__tagged_enum.snap @@ -0,0 +1,63 @@ +--- +source: yerpc/src/qt.rs +expression: out +--- +inline QJsonValue toJson(const QString &o) { return {o}; } +inline bool tryFromJson(const QJsonValue &v, QString &out) { + if (!v.isString()) return false; + out = v.toString(); + return true; +} + +inline QJsonValue toJson(const uint32_t &o) { return static_cast(o); } +inline bool tryFromJson(const QJsonValue &v, uint32_t &out) { + if (!v.isDouble()) return false; + out = static_cast(v.toDouble()); + return true; +} +struct FooBar { + QString name; + uint32_t count; +}; +inline QJsonValue toJson(const FooBar &o) { + QJsonObject j; + j["name"] = toJson(o.name); + j["count"] = toJson(o.count); + return j; +} +inline bool tryFromJson(const QJsonValue &v, FooBar &out) { + if (!v.isObject()) return false; + QJsonObject obj = v.toObject(); + if (!tryFromJson(obj["name"], out.name)) return false; + if (!tryFromJson(obj["count"], out.count)) return false; + return true; +} +struct FooBaz { +}; +inline QJsonValue toJson(const FooBaz &o) { + QJsonObject j; + return j; +} +inline bool tryFromJson(const QJsonValue &v, FooBaz &out) { + if (!v.isObject()) return false; + QJsonObject obj = v.toObject(); + return true; +} +using Foo = std::variant; +inline QJsonValue toJson(const Foo &o) { + QJsonObject j; + std::visit(Hidden::overloaded{ + [&j](FooBar v){ j = toJson(v).toObject(); j["kind"] = QString("Bar"); }, + [&j](FooBaz v){ j = toJson(v).toObject(); j["kind"] = QString("Baz"); }, + }, o); + return j; +} +inline bool tryFromJson(const QJsonValue &v, Foo &out) { + if (!v.isObject()) return false; + QJsonObject obj = v.toObject(); + if (!obj.contains("kind") || !obj["kind"].isString()) return false; + QString kind = obj.take("kind").toString(); + if (kind == "Bar") { FooBar v; if (!tryFromJson(obj, v)) return false; out = v; return true; }; + if (kind == "Baz") { FooBaz v; if (!tryFromJson(obj, v)) return false; out = v; return true; }; + return false; +} diff --git a/yerpc/src/snapshots/yerpc__qt__tuple.snap b/yerpc/src/snapshots/yerpc__qt__tuple.snap new file mode 100644 index 0000000..5ccab46 --- /dev/null +++ b/yerpc/src/snapshots/yerpc__qt__tuple.snap @@ -0,0 +1,32 @@ +--- +source: yerpc/src/qt.rs +expression: out +--- + +inline QJsonValue toJson(const uint32_t &o) { return static_cast(o); } +inline bool tryFromJson(const QJsonValue &v, uint32_t &out) { + if (!v.isDouble()) return false; + out = static_cast(v.toDouble()); + return true; +} +inline QJsonValue toJson(const QString &o) { return {o}; } +inline bool tryFromJson(const QJsonValue &v, QString &out) { + if (!v.isString()) return false; + out = v.toString(); + return true; +} +using Foo = std::tuple; +inline QJsonValue toJson(const Foo &o) { + QJsonArray a; + a.push_back(toJson(std::get<0>(o))); + a.push_back(toJson(std::get<1>(o))); + return a; +} +inline bool tryFromJson(const QJsonValue &v, Foo &out) { + if (!v.isArray()) return false; + QJsonArray a = v.toArray(); + if (a.size() != 2) return false; + if (!tryFromJson(a.at(0), std::get<0>(out))) return false; + if (!tryFromJson(a.at(1), std::get<1>(out))) return false; + return true; +} diff --git a/yerpc/src/type_info.rs b/yerpc/src/type_info.rs new file mode 100644 index 0000000..3af8e4d --- /dev/null +++ b/yerpc/src/type_info.rs @@ -0,0 +1,326 @@ +use typescript_type_def::type_expr as ts; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum TypeInfo { + Void, + Bool, + Number(Number), + String, + Optional(Box), + Array(Box), + Map(Box), + Tuple { + name: Option, + elements: Vec, + }, + Struct { + name: String, + fields: Vec, + }, + TaggedEnum { + name: String, + variants: Vec, + }, + StringEnum { + name: String, + variants: Vec, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Variant { + pub(crate) name: String, + pub(crate) fields: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Number { + I8, + I16, + I32, + I64, + U8, + U16, + U32, + U64, + F32, + F64, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Field { + pub(crate) name: String, + pub(crate) ty: TypeInfo, +} + +impl From<&ts::TypeInfo> for TypeInfo { + fn from(info: &ts::TypeInfo) -> Self { + match info { + ts::TypeInfo::Native(n) => ctype_from_expr(&n.r#ref, None), + ts::TypeInfo::Defined(d) => { + let name = d.def.name.0.to_string(); + match d.def.def { + ts::TypeExpr::Name(ts::TypeName { + name: ts::Ident("number"), + .. + }) => match name.as_str() { + "U8" => Self::Number(Number::U8), + "U16" => Self::Number(Number::U16), + "U32" => Self::Number(Number::U32), + "U64" | "Usize" => Self::Number(Number::U64), + "I8" => Self::Number(Number::I8), + "I16" => Self::Number(Number::I16), + "I32" => Self::Number(Number::I32), + "I64" | "ISize" => Self::Number(Number::I64), + "F32" => Self::Number(Number::F32), + "F64" => Self::Number(Number::F64), + n => panic!("Could not handle `{n}` in {info:?}"), + }, + ts::TypeExpr::Object(o) => TypeInfo::Struct { + name, + fields: generate_struct_fields(&o), + }, + ts::TypeExpr::Union(u) => { + if let Some(variants) = get_string_enum_variants(&u) { + TypeInfo::StringEnum { name, variants } + } else if let Some(variants) = parse_internally_tagged_union(&u) { + TypeInfo::TaggedEnum { name, variants } + } else { + panic!("Could not convert {u:?}",); + } + } + e => ctype_from_expr(&e, Some(name)), + } + } + } + } +} + +fn parse_internally_tagged_union(u: &ts::TypeUnion) -> Option> { + if u.members.is_empty() { + return None; + } + let mut tag_field = None; + let mut variants = vec![]; + for m in u.members { + match m { + ts::TypeExpr::Object(obj) if obj.fields.len() == 1 => { + let f = &obj.fields[0]; + if let ts::TypeExpr::String(s) = &f.r#type { + let tf = f.name.value.to_string(); + if tag_field.get_or_insert_with(|| tf.clone()) != &tf { + return None; + } + variants.push(Variant { + name: s.value.to_string(), + fields: vec![], + }); + } else { + return None; + } + } + ts::TypeExpr::Intersection(i) if i.members.len() == 2 => { + let tag_obj = match &i.members[0] { + ts::TypeExpr::Object(a) => a, + _ => return None, + }; + let data_obj = resolve_to_object(&i.members[1])?; + if tag_obj.fields.len() != 1 { + return None; + } + let f = &tag_obj.fields[0]; + if let ts::TypeExpr::String(s) = &f.r#type { + let tf = f.name.value.to_string(); + if tag_field.get_or_insert_with(|| tf.clone()) != &tf { + return None; + } + let fields = generate_struct_fields(&data_obj); + variants.push(Variant { + name: s.value.to_string(), + fields, + }); + } else { + return None; + } + } + _ => return None, + } + } + assert_eq!(tag_field?, "kind"); + Some(variants) +} +pub(crate) fn resolve_to_object(expr: &ts::TypeExpr) -> Option { + match expr { + ts::TypeExpr::Object(o) => Some(*o), + ts::TypeExpr::Ref(ts::TypeInfo::Defined(d)) => resolve_to_object(&d.def.def), + _ => None, + } +} +pub(crate) fn get_string_enum_variants(u: &ts::TypeUnion) -> Option> { + if !is_string_enum(u) { + return None; + } + Some( + u.members + .iter() + .filter_map(|m| match m { + ts::TypeExpr::String(s) => Some(s.value.to_string()), + _ => None, + }) + .collect(), + ) +} + +fn generate_struct_fields(obj: &ts::TypeObject) -> Vec { + obj.fields + .iter() + .map(|f| { + let name = f.name.value.to_string(); + let mut ty = ctype_from_expr(&f.r#type, None); + if f.optional && !matches!(ty, TypeInfo::Optional(_)) { + ty = TypeInfo::Optional(Box::new(ty)); + } + Field { name, ty } + }) + .collect() +} + +pub(crate) fn is_string_enum(u: &ts::TypeUnion) -> bool { + !u.members.is_empty() + && u.members + .iter() + .all(|m| matches!(m, ts::TypeExpr::String(_))) +} + +fn ctype_from_expr(expr: &ts::TypeExpr, name: Option) -> TypeInfo { + match expr { + ts::TypeExpr::Ref(info) => TypeInfo::from(*info), + ts::TypeExpr::Name(n) => match n.name.0 { + "number" => TypeInfo::Number(Number::F64), + "string" => TypeInfo::String, + "boolean" => TypeInfo::Bool, + "null" | "undefined" | "void" | "never" => TypeInfo::Void, + "Record" if n.generic_args.len() == 2 => { + if let Some(name) = name { + panic!("name {name} found in {expr:?}"); + } + TypeInfo::Map(Box::new(ctype_from_expr(&n.generic_args[1], None))) + } + _ => unimplemented!(), // other => Type::Struct(other.to_string()), + }, + ts::TypeExpr::Array(a) => { + if let Some(name) = name { + panic!("name {name} found in {expr:?}"); + } + TypeInfo::Array(Box::new(ctype_from_expr(a.item, None))) + } + ts::TypeExpr::Union(u) => { + let non_null: Vec<_> = u + .members + .iter() + .filter(|m| !matches!(m, ts::TypeExpr::Name(n) if n.name.0 == "null")) + .collect(); + if non_null.len() < u.members.len() && non_null.len() == 1 { + if let Some(name) = name { + panic!("name {name} found in {expr:?}"); + } + TypeInfo::Optional(Box::new(ctype_from_expr(non_null[0], None))) + } else if non_null.len() == 1 { + ctype_from_expr(non_null[0], None) + } else { + TypeInfo::Void // TODO: tagged union + } + } + ts::TypeExpr::Tuple(t) if t.elements.is_empty() => TypeInfo::Void, + ts::TypeExpr::Tuple(t) => TypeInfo::Tuple { + name, + elements: t + .elements + .iter() + .map(|e| ctype_from_expr(e, None)) + .collect(), + }, + ts::TypeExpr::String(_) => TypeInfo::String, + _ => TypeInfo::Void, + } +} + +#[cfg(test)] +use crate::typescript::TypeDef; + +#[test] +fn test_nested_struct() { + #[derive(TypeDef)] + struct Foo { + _bar: Vec, + _baz: std::collections::HashMap, + } + assert_eq!( + TypeInfo::from(&Foo::INFO), + TypeInfo::Struct { + name: "Foo".to_owned(), + fields: vec![ + Field { + name: "_bar".to_owned(), + ty: TypeInfo::Array(Box::new(TypeInfo::Number(Number::U8))) + }, + Field { + name: "_baz".to_owned(), + ty: TypeInfo::Map(Box::new(TypeInfo::Number(Number::I8))) + } + ] + } + ); +} + +#[test] +fn test_string_enum() { + #[derive(TypeDef)] + enum Foo { + _A, + _B, + _C, + } + assert_eq!( + TypeInfo::from(&Foo::INFO), + TypeInfo::StringEnum { + name: "Foo".to_owned(), + variants: vec!["_A".to_owned(), "_B".to_owned(), "_C".to_owned()] + } + ); +} + +#[test] +fn test_tagged_enum() { + #[derive(TypeDef)] + #[serde(tag = "kind")] + enum Foo { + _A { name: String, count: u32 }, + _B, + } + assert_eq!( + TypeInfo::from(&Foo::INFO), + TypeInfo::TaggedEnum { + name: "Foo".to_owned(), + variants: vec![ + Variant { + name: "_A".to_owned(), + fields: vec![ + Field { + name: "name".to_owned(), + ty: TypeInfo::String + }, + Field { + name: "count".to_owned(), + ty: TypeInfo::Number(Number::U32) + } + ] + }, + Variant { + name: "_B".to_owned(), + fields: vec![] + } + ] + } + ); +} diff --git a/yerpc/src/typescript.rs b/yerpc/src/typescript.rs index ee91194..62f19cb 100644 --- a/yerpc/src/typescript.rs +++ b/yerpc/src/typescript.rs @@ -6,6 +6,8 @@ use typescript_type_def::{type_expr::TypeInfo, write_definition_file, Definition pub use typescript_type_def as type_def; pub use typescript_type_def::TypeDef; +use crate::method::Method; + pub fn typedef_to_expr_string(root_namespace: Option<&str>) -> io::Result { let mut expr = vec![]; ::INFO.write_ref_expr(&mut expr, root_namespace)?; @@ -25,38 +27,8 @@ pub fn export_types_to_file( Ok(()) } -pub struct Method { - pub is_notification: bool, - pub is_positional: bool, - pub ts_name: String, - pub rpc_name: String, - pub args: Vec<(String, &'static TypeInfo)>, - pub output: Option<&'static TypeInfo>, - pub docs: Option, -} - impl Method { - pub fn new( - ts_name: &str, - rpc_name: &str, - args: Vec<(String, &'static TypeInfo)>, - output: Option<&'static TypeInfo>, - is_notification: bool, - is_positional: bool, - docs: Option<&str>, - ) -> Self { - Self { - ts_name: ts_name.to_string(), - rpc_name: rpc_name.to_string(), - args, - output, - is_notification, - is_positional, - docs: docs.map(|d| d.to_string()), - } - } - - pub fn to_string(&self, root_namespace: Option<&str>) -> String { + pub fn to_string_ts(&self, root_namespace: Option<&str>) -> String { let (args, call) = if !self.is_positional { if let Some((name, ty)) = self.args.first() { ( diff --git a/yerpc/tests/axum.rs b/yerpc/tests/axum.rs index 2817b06..6b2b98a 100644 --- a/yerpc/tests/axum.rs +++ b/yerpc/tests/axum.rs @@ -19,7 +19,11 @@ mod tests { } } - #[rpc(all_positional, ts_outdir = "typescript/generated")] + #[rpc( + all_positional, + ts_outdir = "typescript/generated", + qt_outdir = "qt/generated" + )] impl Api { async fn shout(&self, msg: String) -> String { msg.to_uppercase() diff --git a/yerpc/tests/basic.rs b/yerpc/tests/basic.rs index f397901..2305867 100644 --- a/yerpc/tests/basic.rs +++ b/yerpc/tests/basic.rs @@ -5,7 +5,11 @@ use yerpc::{rpc, RpcSession}; async fn basic() -> anyhow::Result<()> { struct Api {} - #[rpc(all_positional, ts_outdir = "typescript/generated")] + #[rpc( + all_positional, + ts_outdir = "typescript/generated", + qt_outdir = "qt/generated" + )] impl Api { pub async fn constant(&self) -> String { "example".to_string() @@ -51,7 +55,11 @@ async fn basic() -> anyhow::Result<()> { async fn basic_mixed_id_types() -> anyhow::Result<()> { struct Api {} - #[rpc(all_positional, ts_outdir = "typescript/generated")] + #[rpc( + all_positional, + ts_outdir = "typescript/generated", + qt_outdir = "qt/generated" + )] impl Api { pub async fn upper(&self, text: String) -> String { text.to_uppercase()