pub struct ClientBuilder { /* private fields */ }Expand description
Fluent builder for Client with optional timeout and other configuration.
Use this instead of the bare Client::new_* constructors when you need to
configure a request timeout.
§Example
use std::time::Duration;
use toolkit_zero::socket::client::{ClientBuilder, Target};
// Async client with a 10-second timeout
let client = ClientBuilder::new(Target::Localhost(8080))
.timeout(Duration::from_secs(10))
.build_async();
// Sync client with a 30-second timeout
let client = ClientBuilder::new(Target::Remote("https://api.example.com".to_string()))
.timeout(Duration::from_secs(30))
.build_sync();Implementations§
Source§impl ClientBuilder
impl ClientBuilder
Sourcepub fn new(target: Target) -> Self
pub fn new(target: Target) -> Self
Create a new builder for the given Target.
Examples found in repository?
62async fn main() -> Result<(), Box<dyn std::error::Error>> {
63 // ── Build routes ──────────────────────────────────────────────────────────
64 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
65
66 let mut server = Server::default();
67
68 // Plain GET — no body, no state.
69 server.mechanism(
70 ServerMechanism::get("/health")
71 .onconnect(|| async { reply!(json => Health { ok: true }) })
72 );
73
74 // POST with a JSON body — echoes back the created item.
75 server.mechanism(
76 ServerMechanism::post("/items")
77 .json::<NewItem>()
78 .onconnect(|body: NewItem| async move {
79 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
80 })
81 );
82
83 // GET with query parameters — filters the stored items by prefix.
84 server.mechanism({
85 let store = store.clone();
86 ServerMechanism::get("/items/search")
87 .state(store)
88 .query::<Filter>()
89 .onconnect(|state: Arc<Mutex<Vec<Item>>>, f: Filter| async move {
90 let items = state.lock().unwrap();
91 let matches: Vec<Item> = items
92 .iter()
93 .filter(|i| i.name.starts_with(&f.prefix))
94 .cloned()
95 .collect();
96 reply!(json => SearchResult { matches })
97 })
98 });
99
100 // POST with shared state — stores the item and returns it.
101 server.mechanism({
102 let store = store.clone();
103 ServerMechanism::post("/items/store")
104 .state(store)
105 .json::<NewItem>()
106 .onconnect(|state: Arc<Mutex<Vec<Item>>>, body: NewItem| async move {
107 let mut s = state.lock().unwrap();
108 let id = s.len() as u32 + 1;
109 let item = Item { id, name: body.name };
110 s.push(item.clone());
111 reply!(json => item, status => Status::Created)
112 })
113 });
114
115 // POST with authenticated-encrypted body (ChaCha20-Poly1305 via SerializationKey).
116 // The body is decrypted before the handler is called; a wrong key returns 403.
117 server.mechanism(
118 ServerMechanism::post("/items/secure")
119 .encryption::<NewItem>(SerializationKey::Default)
120 .onconnect(|body: NewItem| async move {
121 let item = Item { id: 99, name: body.name };
122 // The response must also be sealed so the client can open it.
123 reply!(sealed => item, key => SerializationKey::Default)
124 })
125 );
126
127 // ── Serve with graceful shutdown ──────────────────────────────────────────
128 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
129 let server_handle = tokio::spawn(async move {
130 server.serve_with_graceful_shutdown(
131 ([127, 0, 0, 1], PORT),
132 async { rx.await.ok(); },
133 ).await;
134 });
135
136 // Give the server time to bind before firing requests.
137 tokio::time::sleep(Duration::from_millis(200)).await;
138 println!("Server started on port {PORT}");
139
140 // ── Client requests ───────────────────────────────────────────────────────
141 let client = ClientBuilder::new(Target::Localhost(PORT))
142 .timeout(Duration::from_secs(5))
143 .build_async();
144
145 // GET /health
146 let health: Health = client.get("/health").send().await?;
147 assert!(health.ok);
148 println!("GET /health → ok={}", health.ok);
149
150 // POST /items (JSON body)
151 let created: Item = client
152 .post("/items")
153 .json(NewItem { name: "widget".into() })
154 .send()
155 .await?;
156 assert_eq!(created.name, "widget");
157 println!("POST /items → {:?}", created);
158
159 // POST /items/store (shared state — populates the store)
160 let stored: Item = client
161 .post("/items/store")
162 .json(NewItem { name: "gadget".into() })
163 .send()
164 .await?;
165 println!("POST /items/store → {:?}", stored);
166
167 let stored2: Item = client
168 .post("/items/store")
169 .json(NewItem { name: "gizmo".into() })
170 .send()
171 .await?;
172 println!("POST /items/store → {:?}", stored2);
173
174 // GET /items/search?prefix=ga (query params)
175 let result: SearchResult = client
176 .get("/items/search")
177 .query(Filter { prefix: "ga".into() })
178 .send()
179 .await?;
180 assert!(result.matches.iter().all(|i| i.name.starts_with("ga")));
181 println!("GET /items/search?prefix=ga → {} match(es): {:?}", result.matches.len(), result.matches);
182
183 // POST /items/secure (authenticated-encrypted body)
184 let secure_item = client
185 .post("/items/secure")
186 .encryption(NewItem { name: "secret".into() }, SerializationKey::Default)
187 .send::<Item>()
188 .await?;
189 assert_eq!(secure_item.name, "secret");
190 println!("POST /items/secure → {:?}", secure_item);
191
192 // ── Shutdown ──────────────────────────────────────────────────────────────
193 tx.send(()).ok();
194 server_handle.await?;
195 println!("\nAll requests successful ✓");
196 Ok(())
197}More examples
80async fn main() -> Result<(), Box<dyn std::error::Error>> {
81 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
82 let mut server = Server::default();
83
84 // ── Register routes via #[mechanism] ────────────────────────────────────
85 //
86 // Each attribute expands to:
87 // server.mechanism(ServerMechanism::METHOD(path)[.modifier()].onconnect(fn));
88
89 // Plain GET — no body, no state.
90 #[mechanism(server, GET, "/health")]
91 async fn health_handler() {
92 reply!(json => Health { ok: true })
93 }
94
95 // POST + JSON body — returns the created item.
96 #[mechanism(server, POST, "/items", json)]
97 async fn create_item(body: NewItem) {
98 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
99 }
100
101 // GET + query params — echoes back the search prefix.
102 // (Filtered search requires access to the store; see /items/store below.)
103 #[mechanism(server, GET, "/items/search", state(store.clone()), query)]
104 async fn search_items(state: Arc<Mutex<Vec<Item>>>, f: Filter) {
105 let items = state.lock().unwrap();
106 let matches: Vec<Item> = items
107 .iter()
108 .filter(|i| i.name.starts_with(&f.prefix))
109 .cloned()
110 .collect();
111 reply!(json => SearchResult { matches })
112 }
113
114 // POST + shared state + JSON body — persists the item.
115 #[mechanism(server, POST, "/items/store", state(store.clone()), json)]
116 async fn store_item(state: Arc<Mutex<Vec<Item>>>, body: NewItem) {
117 let mut s = state.lock().unwrap();
118 let id = s.len() as u32 + 1;
119 let item = Item { id, name: body.name };
120 s.push(item.clone());
121 reply!(json => item, status => Status::Created)
122 }
123
124 // POST + AEAD-encrypted body (ChaCha20-Poly1305 via SerializationKey).
125 // The body is decrypted before the handler is called; the response is
126 // sealed so the client's `.send::<Item>()` can open it automatically.
127 #[mechanism(server, POST, "/items/secure", encrypted(SerializationKey::Default))]
128 async fn secure_create(body: NewItem) {
129 let item = Item { id: 99, name: body.name };
130 reply!(sealed => item, key => SerializationKey::Default)
131 }
132
133 // ── Start the server in the background via .background() ────────────────
134 //
135 // Previously this required manually wrapping the `.await` call inside
136 // `tokio::spawn(async move { server.serve_*(…).await; })`.
137 // `.background()` is the idiomatic shorthand for exactly that pattern.
138 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
139 let server_handle = server
140 .serve_with_graceful_shutdown(
141 ([127, 0, 0, 1], PORT),
142 async { rx.await.ok(); },
143 )
144 .background(); // ← non-blocking spawn
145
146 // Give the server a moment to bind before firing requests.
147 tokio::time::sleep(Duration::from_millis(200)).await;
148 println!("Server started on port {PORT}");
149
150 // ── Issue all requests via #[request] ───────────────────────────────────
151 //
152 // Each attribute expands to an expression that sends the request and awaits
153 // the response, binding the decoded value to a local with the function name.
154
155 let client = ClientBuilder::new(Target::Localhost(PORT))
156 .timeout(Duration::from_secs(5))
157 .build_async();
158
159 // Async GET /health — plain request, no body.
160 #[request(client, GET, "/health", async)]
161 async fn health_resp() -> Health {}
162 assert!(health_resp.ok);
163 println!("GET /health → ok={}", health_resp.ok);
164
165 // Async POST /items — JSON body.
166 #[request(client, POST, "/items", json(NewItem { name: "widget".to_string() }), async)]
167 async fn created() -> Item {}
168 assert_eq!(created.name, "widget");
169 println!("POST /items → {:?}", created);
170
171 // Store a couple of items so the search route has data.
172 #[request(client, POST, "/items/store", json(NewItem { name: "gadget".to_string() }), async)]
173 async fn stored1() -> Item {}
174 println!("POST /items/store → {:?}", stored1);
175
176 #[request(client, POST, "/items/store", json(NewItem { name: "gizmo".to_string() }), async)]
177 async fn stored2() -> Item {}
178 println!("POST /items/store → {:?}", stored2);
179
180 // Async GET /items/search?prefix=ga — query params with shared state.
181 #[request(client, GET, "/items/search", query(Filter { prefix: "ga".to_string() }), async)]
182 async fn results() -> SearchResult {}
183 assert!(results.matches.iter().all(|i| i.name.starts_with("ga")));
184 println!(
185 "GET /items/search?prefix=ga → {} match(es): {:?}",
186 results.matches.len(),
187 results.matches
188 );
189
190 // Async POST /items/secure — AEAD-encrypted body.
191 #[request(client, POST, "/items/secure", encrypted(NewItem { name: "secret".to_string() }, SerializationKey::Default), async)]
192 async fn secure_item() -> Item {}
193 assert_eq!(secure_item.name, "secret");
194 println!("POST /items/secure → {:?}", secure_item);
195
196 // ── Graceful shutdown ───────────────────────────────────────────────────
197 tx.send(()).ok();
198 server_handle.await?;
199 println!("\nAll requests successful ✓");
200 Ok(())
201}Sourcepub fn timeout(self, duration: Duration) -> Self
pub fn timeout(self, duration: Duration) -> Self
Set a request timeout.
Both the async and blocking reqwest clients will respect this duration.
Requests that do not complete within the timeout are cancelled and return
a reqwest::Error with is_timeout() = true.
Examples found in repository?
62async fn main() -> Result<(), Box<dyn std::error::Error>> {
63 // ── Build routes ──────────────────────────────────────────────────────────
64 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
65
66 let mut server = Server::default();
67
68 // Plain GET — no body, no state.
69 server.mechanism(
70 ServerMechanism::get("/health")
71 .onconnect(|| async { reply!(json => Health { ok: true }) })
72 );
73
74 // POST with a JSON body — echoes back the created item.
75 server.mechanism(
76 ServerMechanism::post("/items")
77 .json::<NewItem>()
78 .onconnect(|body: NewItem| async move {
79 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
80 })
81 );
82
83 // GET with query parameters — filters the stored items by prefix.
84 server.mechanism({
85 let store = store.clone();
86 ServerMechanism::get("/items/search")
87 .state(store)
88 .query::<Filter>()
89 .onconnect(|state: Arc<Mutex<Vec<Item>>>, f: Filter| async move {
90 let items = state.lock().unwrap();
91 let matches: Vec<Item> = items
92 .iter()
93 .filter(|i| i.name.starts_with(&f.prefix))
94 .cloned()
95 .collect();
96 reply!(json => SearchResult { matches })
97 })
98 });
99
100 // POST with shared state — stores the item and returns it.
101 server.mechanism({
102 let store = store.clone();
103 ServerMechanism::post("/items/store")
104 .state(store)
105 .json::<NewItem>()
106 .onconnect(|state: Arc<Mutex<Vec<Item>>>, body: NewItem| async move {
107 let mut s = state.lock().unwrap();
108 let id = s.len() as u32 + 1;
109 let item = Item { id, name: body.name };
110 s.push(item.clone());
111 reply!(json => item, status => Status::Created)
112 })
113 });
114
115 // POST with authenticated-encrypted body (ChaCha20-Poly1305 via SerializationKey).
116 // The body is decrypted before the handler is called; a wrong key returns 403.
117 server.mechanism(
118 ServerMechanism::post("/items/secure")
119 .encryption::<NewItem>(SerializationKey::Default)
120 .onconnect(|body: NewItem| async move {
121 let item = Item { id: 99, name: body.name };
122 // The response must also be sealed so the client can open it.
123 reply!(sealed => item, key => SerializationKey::Default)
124 })
125 );
126
127 // ── Serve with graceful shutdown ──────────────────────────────────────────
128 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
129 let server_handle = tokio::spawn(async move {
130 server.serve_with_graceful_shutdown(
131 ([127, 0, 0, 1], PORT),
132 async { rx.await.ok(); },
133 ).await;
134 });
135
136 // Give the server time to bind before firing requests.
137 tokio::time::sleep(Duration::from_millis(200)).await;
138 println!("Server started on port {PORT}");
139
140 // ── Client requests ───────────────────────────────────────────────────────
141 let client = ClientBuilder::new(Target::Localhost(PORT))
142 .timeout(Duration::from_secs(5))
143 .build_async();
144
145 // GET /health
146 let health: Health = client.get("/health").send().await?;
147 assert!(health.ok);
148 println!("GET /health → ok={}", health.ok);
149
150 // POST /items (JSON body)
151 let created: Item = client
152 .post("/items")
153 .json(NewItem { name: "widget".into() })
154 .send()
155 .await?;
156 assert_eq!(created.name, "widget");
157 println!("POST /items → {:?}", created);
158
159 // POST /items/store (shared state — populates the store)
160 let stored: Item = client
161 .post("/items/store")
162 .json(NewItem { name: "gadget".into() })
163 .send()
164 .await?;
165 println!("POST /items/store → {:?}", stored);
166
167 let stored2: Item = client
168 .post("/items/store")
169 .json(NewItem { name: "gizmo".into() })
170 .send()
171 .await?;
172 println!("POST /items/store → {:?}", stored2);
173
174 // GET /items/search?prefix=ga (query params)
175 let result: SearchResult = client
176 .get("/items/search")
177 .query(Filter { prefix: "ga".into() })
178 .send()
179 .await?;
180 assert!(result.matches.iter().all(|i| i.name.starts_with("ga")));
181 println!("GET /items/search?prefix=ga → {} match(es): {:?}", result.matches.len(), result.matches);
182
183 // POST /items/secure (authenticated-encrypted body)
184 let secure_item = client
185 .post("/items/secure")
186 .encryption(NewItem { name: "secret".into() }, SerializationKey::Default)
187 .send::<Item>()
188 .await?;
189 assert_eq!(secure_item.name, "secret");
190 println!("POST /items/secure → {:?}", secure_item);
191
192 // ── Shutdown ──────────────────────────────────────────────────────────────
193 tx.send(()).ok();
194 server_handle.await?;
195 println!("\nAll requests successful ✓");
196 Ok(())
197}More examples
80async fn main() -> Result<(), Box<dyn std::error::Error>> {
81 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
82 let mut server = Server::default();
83
84 // ── Register routes via #[mechanism] ────────────────────────────────────
85 //
86 // Each attribute expands to:
87 // server.mechanism(ServerMechanism::METHOD(path)[.modifier()].onconnect(fn));
88
89 // Plain GET — no body, no state.
90 #[mechanism(server, GET, "/health")]
91 async fn health_handler() {
92 reply!(json => Health { ok: true })
93 }
94
95 // POST + JSON body — returns the created item.
96 #[mechanism(server, POST, "/items", json)]
97 async fn create_item(body: NewItem) {
98 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
99 }
100
101 // GET + query params — echoes back the search prefix.
102 // (Filtered search requires access to the store; see /items/store below.)
103 #[mechanism(server, GET, "/items/search", state(store.clone()), query)]
104 async fn search_items(state: Arc<Mutex<Vec<Item>>>, f: Filter) {
105 let items = state.lock().unwrap();
106 let matches: Vec<Item> = items
107 .iter()
108 .filter(|i| i.name.starts_with(&f.prefix))
109 .cloned()
110 .collect();
111 reply!(json => SearchResult { matches })
112 }
113
114 // POST + shared state + JSON body — persists the item.
115 #[mechanism(server, POST, "/items/store", state(store.clone()), json)]
116 async fn store_item(state: Arc<Mutex<Vec<Item>>>, body: NewItem) {
117 let mut s = state.lock().unwrap();
118 let id = s.len() as u32 + 1;
119 let item = Item { id, name: body.name };
120 s.push(item.clone());
121 reply!(json => item, status => Status::Created)
122 }
123
124 // POST + AEAD-encrypted body (ChaCha20-Poly1305 via SerializationKey).
125 // The body is decrypted before the handler is called; the response is
126 // sealed so the client's `.send::<Item>()` can open it automatically.
127 #[mechanism(server, POST, "/items/secure", encrypted(SerializationKey::Default))]
128 async fn secure_create(body: NewItem) {
129 let item = Item { id: 99, name: body.name };
130 reply!(sealed => item, key => SerializationKey::Default)
131 }
132
133 // ── Start the server in the background via .background() ────────────────
134 //
135 // Previously this required manually wrapping the `.await` call inside
136 // `tokio::spawn(async move { server.serve_*(…).await; })`.
137 // `.background()` is the idiomatic shorthand for exactly that pattern.
138 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
139 let server_handle = server
140 .serve_with_graceful_shutdown(
141 ([127, 0, 0, 1], PORT),
142 async { rx.await.ok(); },
143 )
144 .background(); // ← non-blocking spawn
145
146 // Give the server a moment to bind before firing requests.
147 tokio::time::sleep(Duration::from_millis(200)).await;
148 println!("Server started on port {PORT}");
149
150 // ── Issue all requests via #[request] ───────────────────────────────────
151 //
152 // Each attribute expands to an expression that sends the request and awaits
153 // the response, binding the decoded value to a local with the function name.
154
155 let client = ClientBuilder::new(Target::Localhost(PORT))
156 .timeout(Duration::from_secs(5))
157 .build_async();
158
159 // Async GET /health — plain request, no body.
160 #[request(client, GET, "/health", async)]
161 async fn health_resp() -> Health {}
162 assert!(health_resp.ok);
163 println!("GET /health → ok={}", health_resp.ok);
164
165 // Async POST /items — JSON body.
166 #[request(client, POST, "/items", json(NewItem { name: "widget".to_string() }), async)]
167 async fn created() -> Item {}
168 assert_eq!(created.name, "widget");
169 println!("POST /items → {:?}", created);
170
171 // Store a couple of items so the search route has data.
172 #[request(client, POST, "/items/store", json(NewItem { name: "gadget".to_string() }), async)]
173 async fn stored1() -> Item {}
174 println!("POST /items/store → {:?}", stored1);
175
176 #[request(client, POST, "/items/store", json(NewItem { name: "gizmo".to_string() }), async)]
177 async fn stored2() -> Item {}
178 println!("POST /items/store → {:?}", stored2);
179
180 // Async GET /items/search?prefix=ga — query params with shared state.
181 #[request(client, GET, "/items/search", query(Filter { prefix: "ga".to_string() }), async)]
182 async fn results() -> SearchResult {}
183 assert!(results.matches.iter().all(|i| i.name.starts_with("ga")));
184 println!(
185 "GET /items/search?prefix=ga → {} match(es): {:?}",
186 results.matches.len(),
187 results.matches
188 );
189
190 // Async POST /items/secure — AEAD-encrypted body.
191 #[request(client, POST, "/items/secure", encrypted(NewItem { name: "secret".to_string() }, SerializationKey::Default), async)]
192 async fn secure_item() -> Item {}
193 assert_eq!(secure_item.name, "secret");
194 println!("POST /items/secure → {:?}", secure_item);
195
196 // ── Graceful shutdown ───────────────────────────────────────────────────
197 tx.send(()).ok();
198 server_handle.await?;
199 println!("\nAll requests successful ✓");
200 Ok(())
201}Sourcepub fn build_async(self) -> Client
pub fn build_async(self) -> Client
Build an async-only Client. Safe to call from any context,
including inside #[tokio::main].
Examples found in repository?
62async fn main() -> Result<(), Box<dyn std::error::Error>> {
63 // ── Build routes ──────────────────────────────────────────────────────────
64 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
65
66 let mut server = Server::default();
67
68 // Plain GET — no body, no state.
69 server.mechanism(
70 ServerMechanism::get("/health")
71 .onconnect(|| async { reply!(json => Health { ok: true }) })
72 );
73
74 // POST with a JSON body — echoes back the created item.
75 server.mechanism(
76 ServerMechanism::post("/items")
77 .json::<NewItem>()
78 .onconnect(|body: NewItem| async move {
79 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
80 })
81 );
82
83 // GET with query parameters — filters the stored items by prefix.
84 server.mechanism({
85 let store = store.clone();
86 ServerMechanism::get("/items/search")
87 .state(store)
88 .query::<Filter>()
89 .onconnect(|state: Arc<Mutex<Vec<Item>>>, f: Filter| async move {
90 let items = state.lock().unwrap();
91 let matches: Vec<Item> = items
92 .iter()
93 .filter(|i| i.name.starts_with(&f.prefix))
94 .cloned()
95 .collect();
96 reply!(json => SearchResult { matches })
97 })
98 });
99
100 // POST with shared state — stores the item and returns it.
101 server.mechanism({
102 let store = store.clone();
103 ServerMechanism::post("/items/store")
104 .state(store)
105 .json::<NewItem>()
106 .onconnect(|state: Arc<Mutex<Vec<Item>>>, body: NewItem| async move {
107 let mut s = state.lock().unwrap();
108 let id = s.len() as u32 + 1;
109 let item = Item { id, name: body.name };
110 s.push(item.clone());
111 reply!(json => item, status => Status::Created)
112 })
113 });
114
115 // POST with authenticated-encrypted body (ChaCha20-Poly1305 via SerializationKey).
116 // The body is decrypted before the handler is called; a wrong key returns 403.
117 server.mechanism(
118 ServerMechanism::post("/items/secure")
119 .encryption::<NewItem>(SerializationKey::Default)
120 .onconnect(|body: NewItem| async move {
121 let item = Item { id: 99, name: body.name };
122 // The response must also be sealed so the client can open it.
123 reply!(sealed => item, key => SerializationKey::Default)
124 })
125 );
126
127 // ── Serve with graceful shutdown ──────────────────────────────────────────
128 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
129 let server_handle = tokio::spawn(async move {
130 server.serve_with_graceful_shutdown(
131 ([127, 0, 0, 1], PORT),
132 async { rx.await.ok(); },
133 ).await;
134 });
135
136 // Give the server time to bind before firing requests.
137 tokio::time::sleep(Duration::from_millis(200)).await;
138 println!("Server started on port {PORT}");
139
140 // ── Client requests ───────────────────────────────────────────────────────
141 let client = ClientBuilder::new(Target::Localhost(PORT))
142 .timeout(Duration::from_secs(5))
143 .build_async();
144
145 // GET /health
146 let health: Health = client.get("/health").send().await?;
147 assert!(health.ok);
148 println!("GET /health → ok={}", health.ok);
149
150 // POST /items (JSON body)
151 let created: Item = client
152 .post("/items")
153 .json(NewItem { name: "widget".into() })
154 .send()
155 .await?;
156 assert_eq!(created.name, "widget");
157 println!("POST /items → {:?}", created);
158
159 // POST /items/store (shared state — populates the store)
160 let stored: Item = client
161 .post("/items/store")
162 .json(NewItem { name: "gadget".into() })
163 .send()
164 .await?;
165 println!("POST /items/store → {:?}", stored);
166
167 let stored2: Item = client
168 .post("/items/store")
169 .json(NewItem { name: "gizmo".into() })
170 .send()
171 .await?;
172 println!("POST /items/store → {:?}", stored2);
173
174 // GET /items/search?prefix=ga (query params)
175 let result: SearchResult = client
176 .get("/items/search")
177 .query(Filter { prefix: "ga".into() })
178 .send()
179 .await?;
180 assert!(result.matches.iter().all(|i| i.name.starts_with("ga")));
181 println!("GET /items/search?prefix=ga → {} match(es): {:?}", result.matches.len(), result.matches);
182
183 // POST /items/secure (authenticated-encrypted body)
184 let secure_item = client
185 .post("/items/secure")
186 .encryption(NewItem { name: "secret".into() }, SerializationKey::Default)
187 .send::<Item>()
188 .await?;
189 assert_eq!(secure_item.name, "secret");
190 println!("POST /items/secure → {:?}", secure_item);
191
192 // ── Shutdown ──────────────────────────────────────────────────────────────
193 tx.send(()).ok();
194 server_handle.await?;
195 println!("\nAll requests successful ✓");
196 Ok(())
197}More examples
80async fn main() -> Result<(), Box<dyn std::error::Error>> {
81 let store: Arc<Mutex<Vec<Item>>> = Arc::new(Mutex::new(vec![]));
82 let mut server = Server::default();
83
84 // ── Register routes via #[mechanism] ────────────────────────────────────
85 //
86 // Each attribute expands to:
87 // server.mechanism(ServerMechanism::METHOD(path)[.modifier()].onconnect(fn));
88
89 // Plain GET — no body, no state.
90 #[mechanism(server, GET, "/health")]
91 async fn health_handler() {
92 reply!(json => Health { ok: true })
93 }
94
95 // POST + JSON body — returns the created item.
96 #[mechanism(server, POST, "/items", json)]
97 async fn create_item(body: NewItem) {
98 reply!(json => Item { id: 1, name: body.name }, status => Status::Created)
99 }
100
101 // GET + query params — echoes back the search prefix.
102 // (Filtered search requires access to the store; see /items/store below.)
103 #[mechanism(server, GET, "/items/search", state(store.clone()), query)]
104 async fn search_items(state: Arc<Mutex<Vec<Item>>>, f: Filter) {
105 let items = state.lock().unwrap();
106 let matches: Vec<Item> = items
107 .iter()
108 .filter(|i| i.name.starts_with(&f.prefix))
109 .cloned()
110 .collect();
111 reply!(json => SearchResult { matches })
112 }
113
114 // POST + shared state + JSON body — persists the item.
115 #[mechanism(server, POST, "/items/store", state(store.clone()), json)]
116 async fn store_item(state: Arc<Mutex<Vec<Item>>>, body: NewItem) {
117 let mut s = state.lock().unwrap();
118 let id = s.len() as u32 + 1;
119 let item = Item { id, name: body.name };
120 s.push(item.clone());
121 reply!(json => item, status => Status::Created)
122 }
123
124 // POST + AEAD-encrypted body (ChaCha20-Poly1305 via SerializationKey).
125 // The body is decrypted before the handler is called; the response is
126 // sealed so the client's `.send::<Item>()` can open it automatically.
127 #[mechanism(server, POST, "/items/secure", encrypted(SerializationKey::Default))]
128 async fn secure_create(body: NewItem) {
129 let item = Item { id: 99, name: body.name };
130 reply!(sealed => item, key => SerializationKey::Default)
131 }
132
133 // ── Start the server in the background via .background() ────────────────
134 //
135 // Previously this required manually wrapping the `.await` call inside
136 // `tokio::spawn(async move { server.serve_*(…).await; })`.
137 // `.background()` is the idiomatic shorthand for exactly that pattern.
138 let (tx, rx) = tokio::sync::oneshot::channel::<()>();
139 let server_handle = server
140 .serve_with_graceful_shutdown(
141 ([127, 0, 0, 1], PORT),
142 async { rx.await.ok(); },
143 )
144 .background(); // ← non-blocking spawn
145
146 // Give the server a moment to bind before firing requests.
147 tokio::time::sleep(Duration::from_millis(200)).await;
148 println!("Server started on port {PORT}");
149
150 // ── Issue all requests via #[request] ───────────────────────────────────
151 //
152 // Each attribute expands to an expression that sends the request and awaits
153 // the response, binding the decoded value to a local with the function name.
154
155 let client = ClientBuilder::new(Target::Localhost(PORT))
156 .timeout(Duration::from_secs(5))
157 .build_async();
158
159 // Async GET /health — plain request, no body.
160 #[request(client, GET, "/health", async)]
161 async fn health_resp() -> Health {}
162 assert!(health_resp.ok);
163 println!("GET /health → ok={}", health_resp.ok);
164
165 // Async POST /items — JSON body.
166 #[request(client, POST, "/items", json(NewItem { name: "widget".to_string() }), async)]
167 async fn created() -> Item {}
168 assert_eq!(created.name, "widget");
169 println!("POST /items → {:?}", created);
170
171 // Store a couple of items so the search route has data.
172 #[request(client, POST, "/items/store", json(NewItem { name: "gadget".to_string() }), async)]
173 async fn stored1() -> Item {}
174 println!("POST /items/store → {:?}", stored1);
175
176 #[request(client, POST, "/items/store", json(NewItem { name: "gizmo".to_string() }), async)]
177 async fn stored2() -> Item {}
178 println!("POST /items/store → {:?}", stored2);
179
180 // Async GET /items/search?prefix=ga — query params with shared state.
181 #[request(client, GET, "/items/search", query(Filter { prefix: "ga".to_string() }), async)]
182 async fn results() -> SearchResult {}
183 assert!(results.matches.iter().all(|i| i.name.starts_with("ga")));
184 println!(
185 "GET /items/search?prefix=ga → {} match(es): {:?}",
186 results.matches.len(),
187 results.matches
188 );
189
190 // Async POST /items/secure — AEAD-encrypted body.
191 #[request(client, POST, "/items/secure", encrypted(NewItem { name: "secret".to_string() }, SerializationKey::Default), async)]
192 async fn secure_item() -> Item {}
193 assert_eq!(secure_item.name, "secret");
194 println!("POST /items/secure → {:?}", secure_item);
195
196 // ── Graceful shutdown ───────────────────────────────────────────────────
197 tx.send(()).ok();
198 server_handle.await?;
199 println!("\nAll requests successful ✓");
200 Ok(())
201}Sourcepub fn build_sync(self) -> Client
pub fn build_sync(self) -> Client
Build a sync-only Client.
§Panics
Panics if called from within an async context (same restriction as
reqwest::blocking::Client). See Client::new_sync for details.
Sourcepub fn build(self) -> Client
pub fn build(self) -> Client
Build a client that supports both async and blocking sends.
§Panics
Panics if called from within an async context. See Client::new for details.