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;