Skip to content

feat: generate openrpc definitions #25

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
May 21, 2023
Merged
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## Unreleased

- Added `openrpc.json` output: enable it with an `openrpc` feature and `openrpc_outdir` attribute, like this `#[rpc(openrpc_outdir = "./")]`
- Breaking: you now need to specify that you want typescript bindings as they are not enabled by default `#[rpc(ts_outdir = "typescript/generated")]` instead of just `#[rpc]`

## 0.4.4

- add `RpcSession::server()` method
Expand Down
43 changes: 43 additions & 0 deletions Cargo.lock

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

4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use yerpc::axum::handle_ws_rpc;

struct Api;

#[rpc(all_positional)]
#[rpc(all_positional, ts_outdir = "typescript/generated", openrpc_outdir = "./")]
impl Api {
async fn shout(&self, msg: String) -> String {
msg.to_uppercase()
Expand Down Expand Up @@ -58,5 +58,5 @@ async fn handler(

Now you can connect any JSON-RPC client to `ws://localhost:3000/rpc` and call the `shout` and `add` methods.

After running `cargo test` you will find an autogenerated TypeScript client in the `typescript/generated` folder.
After running `cargo test` you will find an autogenerated TypeScript client in the `typescript/generated` folder and an `openrpc.json` file in the root fo your project.
See [`examples/axum`](examples/axum) for a full usage example with Rust server and TypeScript client for a chat server.
1 change: 1 addition & 0 deletions examples/axum/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,4 @@ tokio = { version = "1.19.2", features = ["full"] }
tracing = "0.1.35"
tracing-subscriber = "0.3.11"
tower-http = { version = "0.3.0", features = ["trace"] }
schemars = "0.8.11"
9 changes: 5 additions & 4 deletions examples/axum/src/webserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use axum::{
Extension, Router,
};
use futures::stream::StreamExt;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use std::sync::Arc;
Expand All @@ -17,19 +18,19 @@ use yerpc::{rpc, OutReceiver, RpcClient, RpcSession};
mod emitter;
use emitter::EventEmitter;

#[derive(Serialize, Deserialize, TypeDef, Clone, Debug)]
#[derive(Serialize, Deserialize, TypeDef, JsonSchema, Clone, Debug)]
struct User {
name: String,
color: String,
}

#[derive(Serialize, Deserialize, TypeDef, Clone, Debug)]
#[derive(Serialize, Deserialize, TypeDef, JsonSchema, Clone, Debug)]
struct ChatMessage {
content: String,
user: User,
}

#[derive(Serialize, Deserialize, TypeDef, Clone, Debug)]
#[derive(Serialize, Deserialize, TypeDef, JsonSchema, Clone, Debug)]
#[serde(tag = "type")]
enum Event {
Message(ChatMessage),
Expand Down Expand Up @@ -114,7 +115,7 @@ impl Session {
}
}

#[rpc]
#[rpc(ts_outdir = "typescript/generated", openrpc_outdir = ".")]
impl Session {
/// Send a chat message.
///
Expand Down
2 changes: 1 addition & 1 deletion examples/tide/src/webserver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ impl Session {
}
}

#[rpc]
#[rpc(ts_outdir = "typescript/generated")]
impl Session {
/// Send a chat message.
///
Expand Down
5 changes: 4 additions & 1 deletion yerpc-derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ proc-macro = true
[dependencies]
proc-macro2 = "1.0.37"
quote = "1.0.18"
syn = { version = "1.0.91", features = ["full", "parsing"] }
syn = { version = "1.0.91", features = ["full", "parsing", "printing"] }
darling = "0.14.0"
convert_case = "0.5.0"

[features]
openrpc = []
53 changes: 51 additions & 2 deletions yerpc-derive/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
extern crate darling;
use darling::{FromAttributes, FromMeta};
#[cfg(feature = "openrpc")]
use openrpc::generate_openrpc_generator;
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, AttributeArgs, Item};

#[cfg(feature = "openrpc")]
mod openrpc;
mod parse;
mod rpc;
mod ts;
Expand All @@ -12,6 +16,24 @@ pub(crate) use rpc::*;
pub(crate) use ts::*;
pub(crate) mod util;

/// Generates the jsonrpc handler and types.
///
/// ### Root Attribute Arguments:
/// - `all_positional: bool` Positional mode means that the parameters of the RPC call are expected to be a JSON array,
/// 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.
/// - `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.
///
/// Note that you need to specify atleast one type definition output: `ts_outdir`, `openrpc_outdir` or both.
///
/// ### Method Attribute Arguments:
/// - `name: Option<String>` Set the name of the RPC method. Defaults to the function name.
/// - `notification: bool` Make this a notification method. Notifications are received like method calls but cannot
/// return anything.
/// - `positional: bool` Positional mode means that the parameters of the RPC call are expected to be a JSON array,
/// which will be parsed as a tuple of this function's arguments.
#[proc_macro_attribute]
pub fn rpc(attr: TokenStream, tokens: TokenStream) -> TokenStream {
let item = parse_macro_input!(tokens as Item);
Expand All @@ -22,13 +44,37 @@ pub fn rpc(attr: TokenStream, tokens: TokenStream) -> TokenStream {
Ok(args) => args,
Err(err) => return err.write_errors().into(),
};
if attr_args.openrpc_outdir.is_none() && attr_args.ts_outdir.is_none() {
return syn::Error::new_spanned(
item,
"The #[rpc] attribute needs atleast one type definition output. Please either set ts_outdir, openrpc_outdir or both.",
)
.to_compile_error().into()
}

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

#[cfg(feature = "openrpc")]
let openrpc_impl = if let Some(outdir) = attr_args.openrpc_outdir.as_ref() {
generate_openrpc_generator(&info, outdir)
} else {
quote!()
};

#[cfg(not(feature = "openrpc"))]
let openrpc_impl = quote!();

quote! {
#item
#rpc_impl
#ts_impl
#openrpc_impl
}
}
Item::Fn(_) => quote!(#item),
Expand All @@ -48,8 +94,11 @@ pub(crate) struct RootAttrArgs {
/// which will be parsed as a tuple of this function's arguments.
all_positional: bool,
/// Set the path where typescript definitions are written to (relative to the crate root).
/// Defaults to `ts-bindings`.
/// If not set, no typescript definitions will be written
ts_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>,
}

#[derive(FromAttributes, Debug, Default)]
Expand Down
Loading