{"id":16019776,"url":"https://github.com/zduny/zzrpc-tutorial","last_synced_at":"2025-04-05T03:26:16.634Z","repository":{"id":83345307,"uuid":"582659945","full_name":"zduny/zzrpc-tutorial","owner":"zduny","description":"Tutorial for zzrpc.","archived":false,"fork":false,"pushed_at":"2023-01-02T07:57:26.000Z","size":28,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-02-10T11:44:47.960Z","etag":null,"topics":["rpc","rpc-framework","zzrpc"],"latest_commit_sha":null,"homepage":"https://github.com/zduny/zzrpc","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/zduny.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-12-27T13:59:26.000Z","updated_at":"2022-12-27T19:34:51.000Z","dependencies_parsed_at":"2023-03-12T17:59:35.519Z","dependency_job_id":null,"html_url":"https://github.com/zduny/zzrpc-tutorial","commit_stats":{"total_commits":9,"total_committers":1,"mean_commits":9.0,"dds":0.0,"last_synced_commit":"7de172982d84e5f95edfccb3500e0c267b92444e"},"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zduny%2Fzzrpc-tutorial","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zduny%2Fzzrpc-tutorial/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zduny%2Fzzrpc-tutorial/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/zduny%2Fzzrpc-tutorial/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/zduny","download_url":"https://codeload.github.com/zduny/zzrpc-tutorial/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247284025,"owners_count":20913671,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["rpc","rpc-framework","zzrpc"],"created_at":"2024-10-08T17:05:22.106Z","updated_at":"2025-04-05T03:26:16.607Z","avatar_url":"https://github.com/zduny.png","language":"Rust","readme":"# zzrpc-tutorial\n\nTutorial example for [zzrpc](https://github.com/zduny/zzrpc).\n\n# usage\n\nBuild:\n\n```bash\ncargo build\n```\n\nStart server:\n\n```bash\ncd server\ncargo run\n```\n\nOpen new terminal tab/window and start client:\n\n```bash\ncd client\ncargo run\n```\n\n# tutorial\n\n## Step 0 (optional) - `common` crate\n\nCreate separate `common` crate for definition of your api.\n\nThis step is optional, but doing so makes it easier to share code between client and server \nimplementations, especially when they're targeting different platforms (for example: server \ntargeting native platform and client targeting WebAssembly running in a browser).\n\n```bash\ncargo new --lib common\n```\n\n## Step 1 - dependencies\n\nAdd [`mezzenger`](https://crates.io/crates/mezzenger), [`serde`](https://crates.io/crates/serde) with `derive` feature enabled and finally [`zzrpc`](https://crates.io/crates/zzrpc) to `Cargo.toml`:\n\n```toml\n# ...\n\n[dependencies]\n# ...\nmezzenger = \"0.1.3\"\nserde = { version = \"1.0.150\", features = [\"derive\"] } \nzzrpc = \"0.1.2\"\n```\n\nFor your server/clients you'll also need some [`kodec`](https://crates.io/crates/kodec), \nsome `mezzenger` transport implementation \n(here: [`mezzenger-tcp`](https://crates.io/crates/mezzenger-tcp)), \n[`futures`](https://crates.io/crates/futures) and on native server/client \n[`tokio`](https://crates.io/crates/tokio):\n\n\n```toml\n# ...\n\n[dependencies]\n# ...\nmezzenger = \"0.1.3\"\nserde = { version = \"1.0.150\", features = [\"derive\"] } \nzzrpc = \"0.1.2\"\nkodec = { version = \"0.1.0\", features = [\"binary\"] }\nmezzenger-tcp = \"0.1.2\"\nfutures = \"0.3.25\"\ntokio = { version = \"1.23.0\", features = [\"full\"] } # only when targeting native platforms\ntokio-stream = { version = \"0.1.11\", features = [\"sync\"] } # optional but useful when creating stream responses \n```\n\nIf you followed **Step 0** then add `common` to your client/server crate dependencies:\n\n```toml\n# ...\n\n[dependencies]\n# ...\ncommon = { path = \"../common\" }\nmezzenger = \"0.1.3\"\nserde = { version = \"1.0.150\", features = [\"derive\"] } \nzzrpc = \"0.1.2\"\nkodec = { version = \"0.1.0\", features = [\"binary\"] }\nmezzenger-tcp = \"0.1.2\"\nfutures = \"0.3.25\"\ntokio = { version = \"1.23.0\", features = [\"full\"] } # only when targeting native platforms\ntokio-stream = { version = \"0.1.11\", features = [\"sync\"] } # optional but useful when creating stream responses \n```\n\n## Step 2 - api definition\n\nDefine your api:\n\n```rust\nuse zzrpc::api;\n\n/// Service API.\n#[api]\npub trait Api {\n    /// Print \"Hello World!\" message on the server.\n    async fn hello_world(\u0026self);\n\n    /// Add two integers together and return result.\n    async fn add_numbers(\u0026self, a: i32, b: i32) -\u003e i32;\n\n    /// Concatenate two strings and return resulting string.\n    async fn concatenate_strings(\u0026self, a: String, b: String) -\u003e String;\n\n    /// Send (string) message to server.\n    async fn message(\u0026self, message: String);\n\n    /// Stream of messages.\n    async fn messages(\u0026self) -\u003e impl Stream\u003cItem = String\u003e;\n}\n```\n\nNotice the `#[api]` macro attribute at the top.\n\nMethods must be marked with `async`.\u003cbr\u003e\nThere are two types of supported methods:\n- regular \"value\" methods like `hello_world`, `add_numbers`, `concatenate_strings`, `message` above,\n- streaming methods - where single request instructs producer to send multiple values (not necessarily \nat once) without consumer having to request them individually with separate requests. Return types of streaming request methods must follow form: `impl Stream\u003cItem = [ITEM TYPE]\u003e`.\n\n## Step 3 - producing\n\nNow, let's create a server for defined api.\n\n### Step 3a - server\n\nWe'll use TCP for communication, first we have to accept TCP connections.\u003cbr\u003e\nAlso we'll create common state shared state by our producers.\n\n```rust\nuse std::sync::Arc;\nuse tokio::{\n    net::TcpListener,\n    select, spawn,\n    sync::RwLock,\n};\nuse futures::{pin_mut, Stream};\nuse kodec::binary::Codec;\nuse mezzenger_tcp::Transport;\n\n#[derive(Debug)]\nstruct State {\n    // ... to do in next steps\n}\n\n#[tokio::main]\nasync fn main() {\n    let state = Arc::new(RwLock::new(State {}));\n\n    let listener = TcpListener::bind(\"127.0.0.1\").await\n        .expect(\"failed to bind tcp listener to specified address\");\n    let break_signal = tokio::signal::ctrl_c();\n    pin_mut!(break_signal);\n\n    loop {\n        select! {\n            listener_result = listener.accept() =\u003e {\n                let (stream, _address) = listener_result.expect(\"failed to connect client\");\n                let state = state.clone();\n                spawn(async move {\n                    // ... to do in next steps\n                });\n            },\n            break_result = \u0026mut break_signal =\u003e {\n                break_result.expect(\"failed to listen for break signal event\");\n                break;\n            }\n        }\n    }\n}\n```\n\n### Step 3b - producer\n\nNow it's a good time to implement a producer for your api:\n\n```rust\nuse tokio::sync::broadcast;\nuse tokio_stream::{wrappers::BroadcastStream, StreamExt};\nuse zzrpc::Produce;\nuse common::api::{impl_produce, Request, Response}; // or simply: use common::api::*;\n\n// ...\n\n#[derive(Debug, Produce)]\nstruct Producer {\n    state: Arc\u003cRwLock\u003cState\u003e\u003e,\n}\n\n// Note we're not implementing any trait here - zzrpc is designed like that\n// on purpose to avoid dealing with current Rust's async trait troubles.\n//\n// Instead simply copy your method signatures from the api's trait, add method \n// bodies and implement them - `Produce` derive macro will do the rest for you.\nimpl Producer {\n    /// Print \"Hello World!\" message on the server.\n    async fn hello_world(\u0026self) {\n        println!(\"Hello World!\");\n    }\n\n    /// Add two integers together and return result.\n    async fn add_numbers(\u0026self, a: i32, b: i32) -\u003e i32 {\n        a + b\n    }\n\n    /// Concatenate two strings and return resulting string.\n    async fn concatenate_strings(\u0026self, a: String, b: String) -\u003e String {\n        format!(\"{a}{b}\")\n    }\n\n    /// Send (string) message to server.\n    async fn message(\u0026self, message: String) {\n        println!(\"Message received: {message}\");\n        let _ = self.state.read().await.sender.send(message);\n    }\n\n    /// Stream of messages.\n    async fn messages(\u0026self) -\u003e impl Stream\u003cItem = String\u003e {\n        BroadcastStream::new(self.state.read().await.sender.subscribe()).filter_map(Result::ok)\n    }\n}\n```\n\nRemember to update `State` struct:\n\n```rust\n// ...\n#[derive(Debug)]\nstruct State {\n    sender: broadcast::Sender\u003cString\u003e,\n}\n\n#[tokio::main]\nasync fn main() {\n    let (sender, _) = broadcast::channel(16);\n    let state = Arc::new(RwLock::new(State { sender }));\n// ...\n```\n\n### Step 3c - serving\n\nNow we can serve our clients:\n\n```rust\n// ...\nlet (stream, _address) = listener_result.expect(\"failed to connect client\");\nlet state = state.clone();\nspawn(async move {\n    // create mezzenger transport wrapping a TCP stream.\n    let transport = Transport::new(stream, Codec::default());\n\n    // create producer.\n    let producer = Producer { state };\n\n    // produce for the transport using default producer configuration.\n    producer.produce(transport, Configuration::default());\n});\n// ...\n```\n\n## Step 4 - consuming\n\nLet's write a client for our service:\n\n```rust\nuse std::time::Duration;\n\nuse futures::StreamExt;\nuse kodec::binary::Codec;\nuse mezzenger_tcp::Transport;\nuse tokio::{net::TcpStream, spawn, time::sleep};\nuse zzrpc::{consumer::Configuration, Consume};\n\nuse common::api::{Api, Consumer};\n\n#[tokio::main]\nasync fn main() {\n    // first connect to the server.\n    let stream = TcpStream::connect(\"127.0.0.1:8080\")\n        .await\n        .expect(\"failed to connect to server\");\n    let address = stream.local_addr().expect(\"failed to get address\");\n\n    // wrap TCP stream in mezzenger transport.\n    let transport = Transport::new(stream, Codec::default());\n\n    // create consumer communicating over transport and using default configuration.\n    let consumer = Consumer::consume(transport, Configuration::default());\n\n\n    // now we can make our first request.\n    consumer.hello_world().await.expect(\"failed to call method\");\n\n    // another request, this time with return value.\n    let result = consumer\n        .add_numbers(2, 3)\n        .await\n        .expect(\"failed to call method\");\n    println!(\"2 + 3 = {result}\");\n\n    let result = consumer\n        .concatenate_strings(\"Hello \".to_string(), \"World\".to_string())\n        .await\n        .expect(\"failed to call method\");\n    println!(\"'Hello ' + 'World' = '{result}'\");\n\n    // let's create a message stream.\n    let mut messages = consumer\n        .messages()\n        .await\n        .expect(\"failed get message stream\");\n\n    // send some messages to server.\n    consumer\n        .message(format!(\"{address} - Message 1\"))\n        .await\n        .expect(\"failed to call method\");\n\n    consumer\n        .message(format!(\"{address} - Message 2\"))\n        .await\n        .expect(\"failed to call method\");\n\n    consumer\n        .message(format!(\"{address} - Message 3\"))\n        .await\n        .expect(\"failed to call method\");\n\n    let aborter = messages.aborter();\n    spawn(async move {\n        // abort stream after 30 seconds\n        sleep(Duration::from_secs(30)).await;\n        aborter.abort();\n    });\n\n    // loop over and print received messages.\n    while let Some(message) = messages.next().await {\n        println!(\"Received message: {message}\");\n    }\n}\n```\n\n## Limitations\n\nHave in mind following limitations of `zzrpc`'s `#[api]` macro:\n- generic traits are not supported,\n- generic methods are not supported,\n- only one api is allowed per module \n  (but multiple apis in separate modules of the same crate are fine),\n- only one producer implementation is allowed per module\n  (again: if you need more you can simply define them in separate modules).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzduny%2Fzzrpc-tutorial","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzduny%2Fzzrpc-tutorial","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzduny%2Fzzrpc-tutorial/lists"}