battler_wamprat/
lib.rs

1//! # battler-wamprat
2//! ## `battler-wamp` + **RaT (Reconnection and Typing)**
3//!
4//! **battler-wamprat** is a Rust library and framework for peers communicating over the **Web
5//! Application Message Protocol** (WAMP).
6//!
7//! The library is built on [`battler-wamp`](https://crates.io/crates/battler-wamp) to provide more complex functionality:
8//!
9//! 1. Automatic reconnection and re-registration of procedures and subscriptions when a session is
10//!    dropped.
11//! 1. Strongly-typed procedure handling, procedure calls, event publication, and subscription event
12//!    handling using built-in serialization and deserialization.
13//!
14//! The library uses [`tokio`](https://tokio.rs) as its asynchronous runtime, and is ready for use on top of WebSocket streams.
15//!
16//! ## What is WAMP?
17//!
18//! **WAMP** is an open standard, routed protocol that provides two messaging patterns: Publish &
19//! Subscribe and routed Remote Procedure Calls. It is intended to connect application components in
20//! distributed applications. WAMP uses WebSocket as its default transport, but it can be
21//! transmitted via any other protocol that allows for ordered, reliable, bi-directional, and
22//! message-oriented communications.
23//!
24//! The WAMP protocol specification is described [here](https://wamp-proto.org/spec.html).
25//!
26//! ## Core Features
27//!
28//! ### Reconnection
29//!
30//! When a WAMP peer disconnects from a router, all of its owned resources are discarded. This
31//! includes subscriptions and procedures. When the peer reconnects with the router, all resources
32//! must be manually redefined with the router.
33//!
34//! The philosophy of `battler-wamprat` is that application logic should not need to worry about
35//! this reregistration whatsoever. The peer keeps a record of all resources, so they are quickly
36//! reestablished as soon as possible.
37//!
38//! ### Type Checking
39//!
40//! In general, user-provided types can be type checked using traits with derive macros for ease of
41//! use.
42//!
43//! The [`battler_wamprat_message::WampApplicationMessage`] trait can be used to type check
44//! application messages. This trait is used for pub/sub events, RPC calls, and RPC results.
45//!
46//! The [`battler_wamprat_uri::WampUriMatcher`] trait can be used to type check pattern-matched
47//! URIs. This trait is only required when URI pattern matching is used.
48//!
49//! Additionally [`battler_wamprat_error::WampError`] can be used for generating conversions to and
50//! from [`battler_wamp::core::error::WampError`] for custom error types. These error types are not
51//! necessarily enforced by the framework, though, as other types of errors can generate from the
52//! router during procedure calls.
53//!
54//! ## Usage
55//!
56//! A WAMP peer managed by `battler-wamprat` runs in an asynchronous task, which continually
57//! establishes a connection to the configured WAMP router. On each new session, all known resources
58//! (e.g., procedures and subscriptions) will be recreated, thereby resuming the previous session.
59//!
60//! A new peer can be built using a [`PeerBuilder`][`crate::peer::PeerBuilder`]. The
61//! [`PeerConnectionConfig`][`crate::peer::PeerConnectionConfig`] describes what router to connect
62//! to and how to handle reconnects. Procedures must be preregistered on the builder at this point,
63//! so that they can be registered on the router as soon as a session is established.
64//!
65//! When it is time to build and start the peer, a [`battler_wamp::peer::Peer`] must be passed in.
66//! This allows the underlying peer object to be configured however desired. Once the peer starts in
67//! the background, it can be interacted with through the returned
68//! [`PeerHandle`][`crate::peer::PeerHandle`]. The handle can be used for dynamic resources, (e.g.,
69//! subscribing to a topic) and for one-off calls (e.g., publishing an event, calling a procedure).
70//!
71//! The [`PeerBuilder`][`crate::peer::PeerBuilder`] also returns a [`tokio::task::JoinHandle`] that
72//! can be used to wait for the peer to be fully destroyed.
73//!
74//! See the examples below for all of these things in action.
75//!
76//! ## Examples
77//!
78//! ### Pub/Sub
79//!
80//! Peers can subscribe to topics that other peers can publish events to. When a peer reconnects to
81//! a router, all of its previous subscriptions are restored.
82//!
83//! Subscriptions must be a type implementing one of the following traits:
84//! * [`Subscription`][`crate::subscription::Subscription`] - For events without type checking.
85//! * [`TypedSubscription`][`crate::subscription::TypedSubscription`] - For events with strict type
86//!   checking.
87//! * [`TypedPatternMatchedSubscription`][`crate::subscription::TypedPatternMatchedSubscription`] -
88//!   For events with strict type checking and a pattern-matched URI.
89//!
90//! All of these traits provide methods for handling events matched by the subscription.
91//!
92//! #### Simple Example
93//!
94//! ```
95//! use battler_wamp::{
96//!     peer::{
97//!         PeerConfig,
98//!         ReceivedEvent,
99//!         new_web_socket_peer,
100//!     },
101//!     router::{
102//!         EmptyPubSubPolicies,
103//!         EmptyRpcPolicies,
104//!         RealmAuthenticationConfig,
105//!         RealmConfig,
106//!         RouterConfig,
107//!         RouterHandle,
108//!         new_web_socket_router,
109//!     },
110//! };
111//! use battler_wamp_uri::{
112//!     Uri,
113//!     WildcardUri,
114//! };
115//! use battler_wamp_values::WampList;
116//! use battler_wamprat::{
117//!     peer::{
118//!         PeerBuilder,
119//!         PeerConnectionType,
120//!         PublishOptions,
121//!     },
122//!     subscription::TypedSubscription,
123//! };
124//! use battler_wamprat_message::WampApplicationMessage;
125//! use tokio::{
126//!     sync::broadcast,
127//!     task::JoinHandle,
128//! };
129//!
130//! async fn start_router() -> anyhow::Result<(RouterHandle, JoinHandle<()>)> {
131//!     let mut config = RouterConfig::default();
132//!     config.realms.push(RealmConfig {
133//!         name: "Realm".to_owned(),
134//!         uri: Uri::try_from("com.battler_wamprat.realm").unwrap(),
135//!         authentication: RealmAuthenticationConfig::default(),
136//!     });
137//!     let router = new_web_socket_router(
138//!         config,
139//!         Box::new(EmptyPubSubPolicies::default()),
140//!         Box::new(EmptyRpcPolicies::default()),
141//!     )?;
142//!     router.start().await
143//! }
144//!
145//! #[derive(WampList)]
146//! struct PingEventArgs(String);
147//!
148//! #[derive(WampApplicationMessage)]
149//! struct PingEvent(#[arguments] PingEventArgs);
150//!
151//! struct PingEventHandler {
152//!     ping_tx: broadcast::Sender<String>,
153//! }
154//!
155//! impl TypedSubscription for PingEventHandler {
156//!     type Event = PingEvent;
157//!
158//!     async fn handle_event(&self, event: Self::Event) {
159//!         self.ping_tx.send(event.0.0).unwrap();
160//!     }
161//!
162//!     async fn handle_invalid_event(&self, event: ReceivedEvent, error: anyhow::Error) {
163//!         panic!("invalid event: {event:?}");
164//!     }
165//! }
166//!
167//! async fn publish_event(router_handle: RouterHandle) {
168//!     let (publisher, _) = PeerBuilder::new(PeerConnectionType::Remote(format!(
169//!         "ws://{}",
170//!         router_handle.local_addr()
171//!     )))
172//!     .start(
173//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
174//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
175//!     );
176//!     publisher.wait_until_ready().await.unwrap();
177//!
178//!     publisher
179//!         .publish(
180//!             Uri::try_from("com.battler_wamprat.ping").unwrap(),
181//!             PingEvent(PingEventArgs("Hello, World!".to_owned())),
182//!             PublishOptions::default(),
183//!         )
184//!         .await
185//!         .unwrap();
186//! }
187//!
188//! #[tokio::main]
189//! async fn main() {
190//!     let (router_handle, _) = start_router().await.unwrap();
191//!
192//!     let (subscriber, _) = PeerBuilder::new(PeerConnectionType::Remote(format!(
193//!         "ws://{}",
194//!         router_handle.local_addr()
195//!     )))
196//!     .start(
197//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
198//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
199//!     );
200//!     subscriber.wait_until_ready().await.unwrap();
201//!
202//!     // Subscribe.
203//!     let (ping_tx, mut ping_rx) = broadcast::channel(16);
204//!     subscriber
205//!         .subscribe(
206//!             Uri::try_from("com.battler_wamprat.ping").unwrap(),
207//!             PingEventHandler { ping_tx },
208//!         )
209//!         .await
210//!         .unwrap();
211//!
212//!     publish_event(router_handle.clone()).await;
213//!
214//!     // Wait for the event.
215//!     assert_eq!(ping_rx.recv().await.unwrap(), "Hello, World!");
216//!
217//!     // Unsubscribe.
218//!     subscriber
219//!         .unsubscribe(&WildcardUri::try_from("com.battler_wamprat.ping").unwrap())
220//!         .await
221//!         .unwrap();
222//! }
223//! ```
224//!
225//! #### Pattern-Based Subscription
226//!
227//! ```
228//! use battler_wamp::{
229//!     peer::{
230//!         PeerConfig,
231//!         new_web_socket_peer,
232//!     },
233//!     router::{
234//!         EmptyPubSubPolicies,
235//!         EmptyRpcPolicies,
236//!         RealmAuthenticationConfig,
237//!         RealmConfig,
238//!         RouterConfig,
239//!         RouterHandle,
240//!         new_web_socket_router,
241//!     },
242//! };
243//! use battler_wamp_uri::{
244//!     Uri,
245//!     WildcardUri,
246//! };
247//! use battler_wamp_values::WampList;
248//! use battler_wamprat::{
249//!     peer::{
250//!         PeerBuilder,
251//!         PeerConnectionType,
252//!         PublishOptions,
253//!     },
254//!     subscription::TypedPatternMatchedSubscription,
255//! };
256//! use battler_wamprat_message::WampApplicationMessage;
257//! use battler_wamprat_uri::WampUriMatcher;
258//! use tokio::{
259//!     sync::broadcast,
260//!     task::JoinHandle,
261//! };
262//!
263//! async fn start_router() -> anyhow::Result<(RouterHandle, JoinHandle<()>)> {
264//!     let mut config = RouterConfig::default();
265//!     config.realms.push(RealmConfig {
266//!         name: "Realm".to_owned(),
267//!         uri: Uri::try_from("com.battler_wamprat.realm").unwrap(),
268//!         authentication: RealmAuthenticationConfig::default(),
269//!     });
270//!     let router = new_web_socket_router(
271//!         config,
272//!         Box::new(EmptyPubSubPolicies::default()),
273//!         Box::new(EmptyRpcPolicies::default()),
274//!     )?;
275//!     router.start().await
276//! }
277//!
278//! #[derive(WampList)]
279//! struct PingEventArgs(String);
280//!
281//! #[derive(WampApplicationMessage)]
282//! struct PingEvent(#[arguments] PingEventArgs);
283//!
284//! #[derive(WampUriMatcher)]
285//! #[uri("com.battler_wamprat.ping.v{version}")]
286//! struct PingEventPattern {
287//!     version: u64,
288//! }
289//!
290//! struct PingEventHandler {
291//!     ping_tx: broadcast::Sender<(String, u64)>,
292//! }
293//!
294//! impl TypedPatternMatchedSubscription for PingEventHandler {
295//!     type Pattern = PingEventPattern;
296//!     type Event = PingEvent;
297//!
298//!     async fn handle_event(&self, event: Self::Event, pattern: Self::Pattern) {
299//!         self.ping_tx.send((event.0.0, pattern.version)).unwrap();
300//!     }
301//! }
302//!
303//! async fn publish_event(router_handle: RouterHandle) {
304//!     let (publisher, _) = PeerBuilder::new(PeerConnectionType::Remote(format!(
305//!         "ws://{}",
306//!         router_handle.local_addr()
307//!     )))
308//!     .start(
309//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
310//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
311//!     );
312//!     publisher.wait_until_ready().await.unwrap();
313//!
314//!     publisher
315//!         .publish(
316//!             Uri::try_from("com.battler_wamprat.ping.v1").unwrap(),
317//!             PingEvent(PingEventArgs("foo".to_owned())),
318//!             PublishOptions::default(),
319//!         )
320//!         .await
321//!         .unwrap();
322//!     publisher
323//!         .publish(
324//!             Uri::try_from("com.battler_wamprat.ping.invalid").unwrap(),
325//!             PingEvent(PingEventArgs("bar".to_owned())),
326//!             PublishOptions::default(),
327//!         )
328//!         .await
329//!         .unwrap();
330//!     publisher
331//!         .publish(
332//!             Uri::try_from("com.battler_wamprat.ping.v2").unwrap(),
333//!             PingEvent(PingEventArgs("baz".to_owned())),
334//!             PublishOptions::default(),
335//!         )
336//!         .await
337//!         .unwrap();
338//! }
339//!
340//! #[tokio::main]
341//! async fn main() {
342//!     let (router_handle, _) = start_router().await.unwrap();
343//!
344//!     let (subscriber, _) = PeerBuilder::new(PeerConnectionType::Remote(format!(
345//!         "ws://{}",
346//!         router_handle.local_addr()
347//!     )))
348//!     .start(
349//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
350//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
351//!     );
352//!     subscriber.wait_until_ready().await.unwrap();
353//!
354//!     // Subscribe.
355//!     let (ping_tx, mut ping_rx) = broadcast::channel(16);
356//!     subscriber
357//!         .subscribe_pattern_matched(PingEventHandler { ping_tx })
358//!         .await
359//!         .unwrap();
360//!
361//!     publish_event(router_handle.clone()).await;
362//!
363//!     // Wait for events.
364//!     assert_eq!(ping_rx.recv().await.unwrap(), ("foo".to_owned(), 1));
365//!     assert_eq!(ping_rx.recv().await.unwrap(), ("baz".to_owned(), 2));
366//!
367//!     // Unsubscribe.
368//!     subscriber
369//!         .unsubscribe(&PingEventPattern::uri_for_router())
370//!         .await
371//!         .unwrap();
372//! }
373//! ```
374//!
375//! ### RPC
376//!
377//! #### Simple Example
378//!
379//! ```
380//! use battler_wamp::{
381//!     core::error::WampError,
382//!     peer::{
383//!         PeerConfig,
384//!         new_web_socket_peer,
385//!     },
386//!     router::{
387//!         EmptyPubSubPolicies,
388//!         EmptyRpcPolicies,
389//!         RealmAuthenticationConfig,
390//!         RealmConfig,
391//!         RouterConfig,
392//!         RouterHandle,
393//!         new_web_socket_router,
394//!     },
395//! };
396//! use battler_wamp_uri::{
397//!     Uri,
398//!     WildcardUri,
399//! };
400//! use battler_wamp_values::{
401//!     Integer,
402//!     WampList,
403//! };
404//! use battler_wamprat::{
405//!     peer::{
406//!         CallOptions,
407//!         PeerBuilder,
408//!         PeerConnectionType,
409//!     },
410//!     procedure::{
411//!         Invocation,
412//!         TypedProcedure,
413//!     },
414//! };
415//! use battler_wamprat_error::WampError;
416//! use battler_wamprat_message::WampApplicationMessage;
417//! use tokio::{
418//!     sync::broadcast,
419//!     task::JoinHandle,
420//! };
421//!
422//! async fn start_router() -> anyhow::Result<(RouterHandle, JoinHandle<()>)> {
423//!     let mut config = RouterConfig::default();
424//!     config.realms.push(RealmConfig {
425//!         name: "Realm".to_owned(),
426//!         uri: Uri::try_from("com.battler_wamprat.realm").unwrap(),
427//!         authentication: RealmAuthenticationConfig::default(),
428//!     });
429//!     let router = new_web_socket_router(
430//!         config,
431//!         Box::new(EmptyPubSubPolicies::default()),
432//!         Box::new(EmptyRpcPolicies::default()),
433//!     )?;
434//!     router.start().await
435//! }
436//!
437//! #[derive(WampList)]
438//! struct DivideInputArgs(Integer, Integer);
439//!
440//! #[derive(WampApplicationMessage)]
441//! struct DivideInput(#[arguments] DivideInputArgs);
442//!
443//! #[derive(Debug, PartialEq, WampList)]
444//! struct DivideOutputArgs(Integer, Integer);
445//!
446//! #[derive(Debug, PartialEq, WampApplicationMessage)]
447//! struct DivideOutput(#[arguments] DivideOutputArgs);
448//!
449//! #[derive(Debug, PartialEq, thiserror::Error, WampError)]
450//! enum DivideError {
451//!     #[error("cannot divide by 0")]
452//!     #[uri("com.battler_wamprat.divide.error.divide_by_zero")]
453//!     DivideByZero,
454//! }
455//!
456//! struct DivideHandler;
457//!
458//! impl TypedProcedure for DivideHandler {
459//!     type Input = DivideInput;
460//!     type Output = DivideOutput;
461//!     type Error = DivideError;
462//!
463//!     async fn invoke(
464//!         &self,
465//!         _: Invocation,
466//!         input: Self::Input,
467//!     ) -> Result<Self::Output, Self::Error> {
468//!         if input.0.1 == 0 {
469//!             return Err(DivideError::DivideByZero);
470//!         }
471//!         let q = input.0.0 / input.0.1;
472//!         let r = input.0.0 % input.0.1;
473//!         Ok(DivideOutput(DivideOutputArgs(q, r)))
474//!     }
475//! }
476//!
477//! #[tokio::main]
478//! async fn main() {
479//!     let (router_handle, _) = start_router().await.unwrap();
480//!
481//!     // Set up the peer that provides the procedure definition.
482//!     let mut callee = PeerBuilder::new(PeerConnectionType::Remote(format!(
483//!         "ws://{}",
484//!         router_handle.local_addr()
485//!     )));
486//!     callee.add_procedure(
487//!         Uri::try_from("com.battler_wamprat.divide").unwrap(),
488//!         DivideHandler,
489//!     );
490//!     let (callee, _) = callee.start(
491//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
492//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
493//!     );
494//!     callee.wait_until_ready().await.unwrap();
495//!
496//!     // Set up the caller.
497//!     let (caller, _) = PeerBuilder::new(PeerConnectionType::Remote(format!(
498//!         "ws://{}",
499//!         router_handle.local_addr()
500//!     )))
501//!     .start(
502//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
503//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
504//!     );
505//!     caller.wait_until_ready().await.unwrap();
506//!
507//!     // Call the procedure.
508//!     assert_eq!(
509//!         caller
510//!             .call_and_wait::<DivideInput, DivideOutput>(
511//!                 Uri::try_from("com.battler_wamprat.divide").unwrap(),
512//!                 DivideInput(DivideInputArgs(65, 4)),
513//!                 CallOptions::default(),
514//!             )
515//!             .await
516//!             .unwrap(),
517//!         DivideOutput(DivideOutputArgs(16, 1))
518//!     );
519//!     assert_eq!(
520//!         TryInto::<DivideError>::try_into(
521//!             caller
522//!                 .call_and_wait::<DivideInput, DivideOutput>(
523//!                     Uri::try_from("com.battler_wamprat.divide").unwrap(),
524//!                     DivideInput(DivideInputArgs(2, 0)),
525//!                     CallOptions::default(),
526//!                 )
527//!                 .await
528//!                 .unwrap_err()
529//!                 .downcast::<WampError>()
530//!                 .unwrap()
531//!         )
532//!         .unwrap(),
533//!         DivideError::DivideByZero
534//!     );
535//! }
536//! ```
537//!
538//! #### Pattern-Based Registration
539//!
540//! ```
541//! use battler_wamp::{
542//!     core::error::WampError,
543//!     peer::{
544//!         PeerConfig,
545//!         new_web_socket_peer,
546//!     },
547//!     router::{
548//!         EmptyPubSubPolicies,
549//!         EmptyRpcPolicies,
550//!         RealmAuthenticationConfig,
551//!         RealmConfig,
552//!         RouterConfig,
553//!         RouterHandle,
554//!         new_web_socket_router,
555//!     },
556//! };
557//! use battler_wamp_uri::{
558//!     Uri,
559//!     WildcardUri,
560//! };
561//! use battler_wamp_values::{
562//!     Integer,
563//!     WampList,
564//! };
565//! use battler_wamprat::{
566//!     peer::{
567//!         CallOptions,
568//!         PeerBuilder,
569//!         PeerConnectionType,
570//!     },
571//!     procedure::{
572//!         Invocation,
573//!         ProgressReporter,
574//!         TypedPatternMatchedProgressiveProcedure,
575//!     },
576//! };
577//! use battler_wamprat_error::WampError;
578//! use battler_wamprat_message::WampApplicationMessage;
579//! use battler_wamprat_uri::WampUriMatcher;
580//! use tokio::{
581//!     sync::broadcast,
582//!     task::JoinHandle,
583//! };
584//!
585//! async fn start_router() -> anyhow::Result<(RouterHandle, JoinHandle<()>)> {
586//!     let mut config = RouterConfig::default();
587//!     config.realms.push(RealmConfig {
588//!         name: "Realm".to_owned(),
589//!         uri: Uri::try_from("com.battler_wamprat.realm").unwrap(),
590//!         authentication: RealmAuthenticationConfig::default(),
591//!     });
592//!     let router = new_web_socket_router(
593//!         config,
594//!         Box::new(EmptyPubSubPolicies::default()),
595//!         Box::new(EmptyRpcPolicies::default()),
596//!     )?;
597//!     router.start().await
598//! }
599//!
600//! #[derive(WampApplicationMessage)]
601//! struct UploadInput;
602//!
603//! #[derive(Debug, PartialEq, WampList)]
604//! struct UploadOutputArgs {
605//!     percentage: u64,
606//! }
607//!
608//! #[derive(Debug, PartialEq, WampApplicationMessage)]
609//! struct UploadOutput(#[arguments] UploadOutputArgs);
610//!
611//! #[derive(WampUriMatcher)]
612//! #[uri("com.battler_wamprat.upload.{file_type}.v1")]
613//! struct UploadPattern {
614//!     file_type: String,
615//! }
616//!
617//! #[derive(Debug, PartialEq, thiserror::Error, WampError)]
618//! enum UploadError {
619//!     #[error("unsupported file type")]
620//!     #[uri("com.battler_wamprat.upload.error.unsupported_file_type")]
621//!     UnsupportedFileType,
622//! }
623//!
624//! struct UploadHandler;
625//!
626//! impl TypedPatternMatchedProgressiveProcedure for UploadHandler {
627//!     type Pattern = UploadPattern;
628//!     type Input = UploadInput;
629//!     type Output = UploadOutput;
630//!     type Error = UploadError;
631//!
632//!     async fn invoke<'rpc>(
633//!         &self,
634//!         invocation: Invocation,
635//!         _: Self::Input,
636//!         procedure: Self::Pattern,
637//!         progress: ProgressReporter<'rpc, Self::Output>,
638//!     ) -> Result<Self::Output, Self::Error> {
639//!         if procedure.file_type != "png" {
640//!             return Err(UploadError::UnsupportedFileType);
641//!         }
642//!         progress
643//!             .send(UploadOutput(UploadOutputArgs { percentage: 25 }))
644//!             .await
645//!             .unwrap();
646//!         progress
647//!             .send(UploadOutput(UploadOutputArgs { percentage: 50 }))
648//!             .await
649//!             .unwrap();
650//!         progress
651//!             .send(UploadOutput(UploadOutputArgs { percentage: 75 }))
652//!             .await
653//!             .unwrap();
654//!         Ok(UploadOutput(UploadOutputArgs { percentage: 100 }))
655//!     }
656//! }
657//!
658//! #[tokio::main]
659//! async fn main() {
660//!     let (router_handle, _) = start_router().await.unwrap();
661//!
662//!     // Set up the peer that provides the procedure definition.
663//!     let mut callee = PeerBuilder::new(PeerConnectionType::Remote(format!(
664//!         "ws://{}",
665//!         router_handle.local_addr()
666//!     )));
667//!     callee.add_procedure_pattern_matched_progressive(UploadHandler);
668//!     let (callee, _) = callee.start(
669//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
670//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
671//!     );
672//!     callee.wait_until_ready().await.unwrap();
673//!
674//!     // Set up the caller.
675//!     let (caller, _) = PeerBuilder::new(PeerConnectionType::Remote(format!(
676//!         "ws://{}",
677//!         router_handle.local_addr()
678//!     )))
679//!     .start(
680//!         new_web_socket_peer(PeerConfig::default()).unwrap(),
681//!         Uri::try_from("com.battler_wamprat.realm").unwrap(),
682//!     );
683//!     caller.wait_until_ready().await.unwrap();
684//!
685//!     // Call the procedure.
686//!     let mut rpc = caller
687//!         .call_with_progress::<UploadInput, UploadOutput>(
688//!             UploadPattern {
689//!                 file_type: "png".to_owned(),
690//!             }
691//!             .wamp_generate_uri()
692//!             .unwrap(),
693//!             UploadInput,
694//!             CallOptions::default(),
695//!         )
696//!         .await
697//!         .unwrap();
698//!     let mut results = Vec::new();
699//!     while let Ok(Some(result)) = rpc.next_result().await {
700//!         results.push(result);
701//!     }
702//!     assert_eq!(
703//!         results,
704//!         Vec::from_iter([
705//!             UploadOutput(UploadOutputArgs { percentage: 25 }),
706//!             UploadOutput(UploadOutputArgs { percentage: 50 }),
707//!             UploadOutput(UploadOutputArgs { percentage: 75 }),
708//!             UploadOutput(UploadOutputArgs { percentage: 100 }),
709//!         ])
710//!     );
711//!
712//!     assert_eq!(
713//!         TryInto::<UploadError>::try_into(
714//!             caller
715//!                 .call_and_wait::<UploadInput, UploadOutput>(
716//!                     UploadPattern {
717//!                         file_type: "gif".to_owned(),
718//!                     }
719//!                     .wamp_generate_uri()
720//!                     .unwrap(),
721//!                     UploadInput,
722//!                     CallOptions::default(),
723//!                 )
724//!                 .await
725//!                 .unwrap_err()
726//!                 .downcast::<WampError>()
727//!                 .unwrap()
728//!         )
729//!         .unwrap(),
730//!         UploadError::UnsupportedFileType
731//!     );
732//! }
733//! ```
734pub mod error;
735pub mod peer;
736pub mod procedure;
737pub mod subscription;