battler_wamprat_uri/
lib.rs

1//! # battler-wamprat-uri
2//!
3//! **battler-wamprat-uri** is a utility crate for [`battler-wamprat`](https://crates.io/crates/battler-wamprat). It provides a procedural macro for dynamically matching URIs for WAMP subscriptions and procedure registrations.
4//!
5//! Pattern-based subscriptions and procedure registrations can be complex. They provide an
6//! additional avenue of input for WAMP messages, outside the `arguments` and `arguments_keyword`
7//! fields expressed by [`battler_wamprat_message::WampApplicationMessage`](https://docs.rs/battler-wamprat-message/latest/battler_wamprat_message/) types.
8//!
9//! For example, a callee can register a procedure on the wildcard URI `com.test.add..v2`. A caller
10//! can then call this procedure using any URI that matches this pattern. One caller may call
11//! `com.test.add.integers.v2` while another may call `com.test.add.strings.v2`. The wildcard URI
12//! component (in this case, `integers` or `strings`) is likely very important to the callee!
13//!
14//! This crate provides runtime type checking and pattern matching to incoming and outgoing WAMP URI
15//! patterns through the [`WampUriMatcher`] derive macro. A URI matcher can be thought of as a
16//! *glorified regular expression*: each wildcard component is a *named capture group* that can
17//! be referenced after matching. The end result is that URIs going out from a peer can be formatted
18//! automatically, and URIs coming into a peer can be pattern matched, allowing the extracted URI
19//! components to be easily read by application logic.
20//!
21//! ## Basic Usage
22//!
23//! A struct using the [`WampUriMatcher`] derive macro must have a `uri` attribute. The URI
24//! describes the wildcard pattern, which is used for formatting outgoing URIs and matching incoming
25//! URIs.
26//!
27//! In the simplest case, a struct with no fields can match a static URI:
28//!
29//! ```
30//! use battler_wamprat_uri::WampUriMatcher;
31//!
32//! #[derive(WampUriMatcher)]
33//! #[uri("com.test.add")]
34//! struct AddUri {}
35//! ```
36//!
37//! For each field you add, that field *must* be represented in the URI pattern. Otherwise, the
38//! struct would be impossible to construct for incoming URIs.
39//!
40//! ```
41//! use battler_wamprat_uri::WampUriMatcher;
42//!
43//! #[derive(WampUriMatcher)]
44//! #[uri("com.test.add.{name}.v2")]
45//! struct AddUri {
46//!     name: String,
47//! }
48//! ```
49//!
50//! Each struct field must be convertible to and from a string, using
51//! [`Display`][`core::fmt::Display`] and [`FromStr`][`core::str::FromStr`] respectively.
52//! ```
53//! use battler_wamprat_uri::WampUriMatcher;
54//!
55//! #[derive(WampUriMatcher)]
56//! #[uri("com.test.add.{name}.{count}")]
57//! struct AddUri {
58//!     name: String,
59//!     count: u64,
60//! }
61//! ```
62//! ## Advanced Features
63//!
64//! ### Prefix Matching
65//!
66//! In some cases, a URI pattern may need to match in prefix form. Prefix matching is only possible
67//! if the last field in the struct is marked with the `rest` attribute. This attribute requires
68//! that an iterator of [`String`]s can be collected into its type.
69//!
70//! ```
71//! use battler_wamprat_uri::WampUriMatcher;
72//!
73//! #[derive(WampUriMatcher)]
74//! #[uri("com.test.fn.{a}.{rest}")]
75//! struct PrefixUri {
76//!     a: String,
77//!     #[rest]
78//!     rest: Vec<String>,
79//! }
80//! ```
81//!
82//! ### Field Repetition
83//!
84//! Fields can be repeated in the URI pattern. The first use of the field will be treated as the
85//! source of truth, and all later uses of the field must match this first value.
86//!
87//! ```
88//! use battler_wamprat_uri::WampUriMatcher;
89//!
90//! #[derive(WampUriMatcher)]
91//! #[uri("com.test.fn.{a}.{b}.{a}")]
92//! struct RepeatedUri {
93//!     a: u64,
94//!     b: u64,
95//! }
96//! ```
97//!
98//! ### Regular Expression Component Matching
99//!
100//! In most cases, struct fields should be isolated to their own URI components for simplicity.
101//! However, this is not a strict requirement of the URI matcher macro.
102//!
103//! Strings and struct fields can be combined into the same URI component. If this is done, the URI
104//! pattern matching is less enforceable on the router, but the peer will still validate and match
105//! the URI as expected.
106//!
107//! ```
108//! use battler_wamprat_uri::WampUriMatcher;
109//!
110//! #[derive(WampUriMatcher)]
111//! #[uri("com.test.math.{a}log{b}")]
112//! struct RegExUri {
113//!     a: u64,
114//!     b: u64,
115//! }
116//! ```
117//!
118//! Note that in the above case, the URI registered on the router will be the wildcard
119//! `com.test.math.`, so the last component can be matched by *any* string. However, the `RegExUri`
120//! type will enforce the additional restrictions. For instance, the URI `com.test.math.abc` will
121//! be rejected by the peer, while `com.test.math.2log12` will be accepted.
122//!
123//! *Note: URI such as the one above will generate a dependency on the `regex` crate.*
124//!
125//! ### Generators
126//!
127//! It can sometimes be useful to generate partial-wildcard URIs based on a URI pattern. For
128//! example, one peer may be publishing messages to `com.chat.{version}.{channel}.{author}`, and
129//! another peer may want to subscribe to messages for a specific channel using the
130//! `com.chat.1.main.{author}`. Rather than requiring the subscriber to generate this
131//! wildcard URI manually, it can use a **derived generator struct** from base URI pattern.
132//!
133//! A generator struct implements the [`WampWildcardUriGenerator`] trait and can be generated
134//! automatically with the `generator` macro attribute. You can generate multiple generators for a
135//! single matcher.
136//!
137//! ```
138//! use std::marker::PhantomData;
139//!
140//! use battler_wamprat_uri::{
141//!     WampUriMatcher,
142//!     Wildcard,
143//!     WampWildcardUriGenerator,
144//! };
145//!
146//! #[derive(Clone, WampUriMatcher)]
147//! #[uri("com.chat.{version}.{channel}.{author}")]
148//! #[generator(ChannelMessageUri, fixed(version = 1u64), require(channel), derive(Clone))]
149//! struct MessageUri {
150//!     version: u64,
151//!     channel: String,
152//!     author: String,
153//! }
154//!
155//! fn main() {
156//!     assert_matches::assert_matches!(ChannelMessageUri {
157//!         version: PhantomData,
158//!         channel: "main".to_owned(),
159//!         author: Wildcard::Wildcard,
160//!     }.wamp_generate_wildcard_uri(), Ok(uri) => {
161//!         assert_eq!(uri.as_ref(), "com.chat.1.main.");
162//!     });
163//! }
164//! ```
165//!
166//! You can also create generators for patterns with unnamed fields, but indices must be prefixed
167//! with an underscore (e.g., `_0`, `_1`) in the `generator` attribute.
168//!
169//! **Note:** Generators are limited to wildcard URI patterns only. URI patterns with prefix
170//! matching **cannot** use the `generator` attribute. You may, however, create your own generator
171//! structs manually.
172
173#![feature(slice_concat_trait)]
174use battler_wamp::core::{
175    error::WampError,
176    match_style::MatchStyle,
177};
178use battler_wamp_uri::{
179    InvalidUri,
180    Uri,
181    WildcardUri,
182};
183pub use battler_wamprat_uri_proc_macro::WampUriMatcher;
184use thiserror::Error;
185
186/// An error resulting from attempting to match a [`Uri`] using a [`WampUriMatcher`].
187#[derive(Debug, Error)]
188#[error("{msg}")]
189pub struct WampUriMatchError {
190    msg: String,
191}
192
193impl WampUriMatchError {
194    pub fn new<S>(msg: S) -> Self
195    where
196        S: Into<String>,
197    {
198        Self { msg: msg.into() }
199    }
200}
201
202impl Into<WampError> for WampUriMatchError {
203    fn into(self) -> WampError {
204        WampError::new(
205            Uri::try_from("com.battler_wamprat.uri_match_error").unwrap(),
206            self.to_string(),
207        )
208    }
209}
210
211impl TryFrom<WampError> for WampUriMatchError {
212    type Error = WampError;
213    fn try_from(value: WampError) -> Result<Self, Self::Error> {
214        if value.reason().as_ref() == "com.battler_wamprat.uri_match_error" {
215            Ok(Self {
216                msg: value.message().to_owned(),
217            })
218        } else {
219            Err(value)
220        }
221    }
222}
223
224/// A dynamic WAMP URI matcher, which configures a URI pattern for incoming and outgoing URIs.
225///
226/// This type can receive a WAMP URI as input and parse it to this type (e.g., for callee-side
227/// invocations), or it can generate a URI based on itself (e.g., for caller-side invocations).
228pub trait WampUriMatcher: Sized {
229    /// The wildcard URI for the router.
230    fn uri_for_router() -> WildcardUri;
231
232    /// The match style of the URI matcher.
233    fn match_style() -> Option<MatchStyle>;
234
235    /// Matches an incoming URI to the configured pattern.
236    fn wamp_match_uri(uri: &str) -> Result<Self, WampUriMatchError>;
237
238    /// Generates an outgoing URI for the configured values.
239    fn wamp_generate_uri(&self) -> Result<Uri, InvalidUri>;
240}
241
242/// A field of a custom generator of a [`WampUriMatcher`] implementation (a.k.a., an implementation
243/// of [`WampWildcardUriGenerator`]) where wildcard is allowed.
244#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
245pub enum Wildcard<T> {
246    #[default]
247    Wildcard,
248    Value(T),
249}
250
251impl<T> std::fmt::Display for Wildcard<T>
252where
253    T: std::fmt::Display,
254{
255    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256        match self {
257            Self::Wildcard => write!(f, ""),
258            Self::Value(val) => val.fmt(f),
259        }
260    }
261}
262
263impl<T> std::str::FromStr for Wildcard<T>
264where
265    T: std::str::FromStr,
266{
267    type Err = <T as std::str::FromStr>::Err;
268    fn from_str(s: &str) -> Result<Self, Self::Err> {
269        if s.is_empty() {
270            Ok(Self::Wildcard)
271        } else {
272            Ok(Self::Value(T::from_str(s)?))
273        }
274    }
275}
276
277impl<T> From<T> for Wildcard<T> {
278    fn from(value: T) -> Self {
279        Wildcard::Value(value)
280    }
281}
282
283/// A custom generator of a [`WampUriMatcher`] implementation where wildcards may be used to
284/// generate [`WildcardUri`]s.
285pub trait WampWildcardUriGenerator<T> {
286    /// Generates an outgoing URI for the configured values.
287    fn wamp_generate_wildcard_uri(&self) -> Result<WildcardUri, InvalidUri>;
288}
289
290impl<T> WampWildcardUriGenerator<T> for T
291where
292    T: WampUriMatcher,
293{
294    fn wamp_generate_wildcard_uri(&self) -> Result<WildcardUri, InvalidUri> {
295        Ok(WildcardUri::from(self.wamp_generate_uri()?))
296    }
297}